MBCHC/mbchc-local.user.js

468 lines
33 KiB
JavaScript
Raw Normal View History

2022-07-04 22:07:57 +00:00
// ==UserScript==
// @name MBCHC-local
// @version trunk
// @description Mute's Bondage Club Hacks Collection (development version)
// @author codename.mute@proton.me
// @namespace https://code.fleshless.org/mute/
// @homepage https://code.fleshless.org/mute/MBCHC
// @updateURL https://code.fleshless.org/mute/MBCHC/raw/branch/master/mbchc-dev.user.js
// @downloadURL https://code.fleshless.org/mute/MBCHC/raw/branch/master/mbchc-dev.user.js
// @match https://bondageprojects.elementfx.com/R*
// @match https://www.bondageprojects.elementfx.com/R*
// @match https://bondage-europe.com/R*
// @match https://www.bondage-europe.com/R*
// @grant none
// ==/UserScript==
// Bondage Club Mod Development Kit (1.0.2)
// For more info see: https://github.com/Jomshir98/bondage-club-mod-sdk
/** @type {ModSDKGlobalAPI} */ // eslint-disable-next-line
var bcModSdk=function(){"use strict";const o="1.0.2";function e(o){alert("Mod ERROR:\n"+o);const e=new Error(o);throw console.error(e),e}const t=new TextEncoder;function n(o){return!!o&&"object"==typeof o&&!Array.isArray(o)}function r(o){const e=new Set;return o.filter((o=>!e.has(o)&&e.add(o)))}const a=new Map,i=new Set;function d(o){i.has(o)||(i.add(o),console.warn(o))}function c(o,e){if(0===e.size)return o;let t=o.toString().replaceAll("\r\n","\n");for(const[n,r]of e.entries())t.includes(n)||d(`ModSDK: Patching ${o.name}: Patch ${n} not applied`),t=t.replaceAll(n,r);return(0,eval)(`(${t})`)}function s(o){const e=[],t=new Map,n=new Set;for(const r of u.values()){const a=r.patching.get(o.name);if(a){e.push(...a.hooks);for(const[e,i]of a.patches.entries())t.has(e)&&t.get(e)!==i&&d(`ModSDK: Mod '${r.name}' is patching function ${o.name} with same pattern that is already applied by different mod, but with different pattern:\nPattern:\n${e}\nPatch1:\n${t.get(e)||""}\nPatch2:\n${i}`),t.set(e,i),n.add(r.name)}}return e.sort(((o,e)=>e.priority-o.priority)),{hooks:e,patches:t,patchesSources:n,final:c(o.original,t)}}function l(o,e=!1){let r=a.get(o);if(r)e&&(r.precomputed=s(r));else{let e=window;const i=o.split(".");for(let t=0;t<i.length-1;t++)if(e=e[i[t]],!n(e))throw new Error(`ModSDK: Function ${o} to be patched not found; ${i.slice(0,t+1).join(".")} is not object`);const d=e[i[i.length-1]];if("function"!=typeof d)throw new Error(`ModSDK: Function ${o} to be patched not found`);const c=function(o){let e=-1;for(const n of t.encode(o)){let o=255&(e^n);for(let e=0;e<8;e++)o=1&o?-306674912^o>>>1:o>>>1;e=e>>>8^o}return((-1^e)>>>0).toString(16).padStart(8,"0").toUpperCase()}(d.toString().replaceAll("\r\n","\n")),l={name:o,original:d,originalHash:c};r=Object.assign(Object.assign({},l),{precomputed:s(l)}),a.set(o,r),e[i[i.length-1]]=function(o){return function(...e){const t=o.precomputed,n=t.hooks,r=t.final;let a=0;const i=d=>{var c,s,l,f;if(a<n.length){const e=n[a];a++;const t=null===(s=(c=w.errorReporterHooks).hookEnter)||void 0===s?void 0:s.call(c,o.name,e.mod),r=e.hook(d,i);return null==t||t(),r}{const n=null===(f=(l=w.errorReporterHooks).hookChainExit)||void 0===f?void 0:f.call(l,o.name,t.patchesSources),a=r.apply(this,e);return null==n||n(),a}};return i(e)}}(r)}return r}function f(){const o=new Set;for(const e of u.values())for(const t of e.patching.keys())o.add(t);for(const e of a.keys())o.add(e);for(const e of o)l(e,!0)}function p(){const o=new Map;for(const[e,t]of a)o.set(e,{name:e,originalHash:t.originalHash,hookedByMods:r(t.precomputed.hooks.map((o=>o.mod))),patchedByMods:Array.from(t.precomputed.patchesSources)});return o}const u=new Map;function h(o){u.get(o.name)!==o&&e(`Failed to unload mod '${o.name}': Not registered`),u.delete(o.name),o.loaded=!1}function g(o,t,r){"string"==typeof o&&o||e("Failed to register mod: Expected non-empty name string, got "+typeof o),"string"!=typeof t&&e(`Failed to register mod '${o}': Expected version string, got ${typeof t}`),r=!0===r;const a=u.get(o);a&&(a.allowReplace&&r||e(`Refusing to load mod '${o}': it is already loaded and doesn't allow being replaced.\nWas the mod loaded multiple times?`),h(a));const i=t=>{"string"==typeof t&&t||e(`Mod '${o}' failed to patch a function: Expected function name string, got ${typeof t}`);let n=c.patching.get(t);return n||(n={hooks:[],patches:new Map},c.patching.set(t,n)),n},d={unload:()=>h(c),hookFunction:(t,n,r)=>{c.loaded||e(`Mod '${c.name}' attempted to call SDK function after being unloaded`);const a=i(t);"number"!=typeof n&&e(`Mod '${o}' failed to hook function '${t}': Expected priority number, got ${typeof n}`),"function"!=typeof r&&e(`Mod '${o}' failed to hook function '${t}': Expected hook function, got ${typeof r}`);const d={mod:c.name,priority:n,hook:r};return a.hooks.push(d),f(),()=>{const o=a.hooks.indexOf(d);o>=0&&(a.hooks.splice(o,1),f())}},patchFunction:(t,r)=>{c.loaded||e(`Mod '${c.name}' attempted to call SDK function after being unloaded`);const a=i(t);n(r)||e(`Mod '${o}' failed to patch function '${t}': Expected patches object, go
2022-07-06 07:02:59 +00:00
// FIXME read chars as they enter
// TODO BCE emotes detector -> activities
2022-07-04 22:07:57 +00:00
(function() {
"use strict";
if (!window.AsylumGGTSSAddItems) throw "AsylumGGTSSAddItems() not found, aborting MBCHC loading"
if (window.MBCHC) throw "MBCHC found, aborting loading"
window.MBCHC = {
VERSION: 'trunk',
NEXT_MESSAGE: 1,
LOG_MESSAGES: false,
LOADED: false,
AUTOHACK_ENABLED: false,
LAST_HACKED: null,
RE_TITLE: /^[a-zA-Z]+$/,
RE_PREF_ACTIVITY_ME: /^@/,
RE_PREF_ACTIVITY: /^@@/,
RE_ACT_CHARS: /^<(\d+)?:(\d+)?>/,
2022-07-05 17:53:00 +00:00
RE_TZ: /(?:GMT|UTC)([+-]\d\d?)/i,
2022-07-04 22:07:57 +00:00
RGB_MUTE: "#6c2132",
RGB_POLLY: "#81b1e7",
2022-07-05 01:04:07 +00:00
UTC_OFFSET: new Date().getTimezoneOffset() * 60 * 1000,
2022-07-04 22:07:57 +00:00
HAND_PENETRATORS: ["Flogger", "Whip", "TennisRacket", "Gavel", "SmallVibratingWand", "LargeDildo", "Vibrator", "Hairbrush", "SmallDildo", "Baguette", "Spatula", "Broom"],
HIDE_SPECIAL: ["Activity","Emoticon"],
HIDE_BODY: ["Blush","BodyLower","BodyUpper","Eyebrows","Eyes","Eyes2","Face","Fluids","HairBack","HairFront","Hands","Head","Mouth","Nipples","Pussy"],
HIDE_CLOTHES: [
"Cloth","ClothAccessory","Necklace","Suit","ClothLower","SuitLower","Bra","Corset","Panties",
"Socks","RightAnklet","LeftAnklet","Garters","Shoes","Hat","HairAccessory3","HairAccessory1","HairAccessory2",
"Gloves","Bracelet","Glasses","Mask","TailStraps","Wings"
],
HIDE_ITEMS: [
"ItemMisc","ItemEars","ItemHead","ItemNose","ItemHood","ItemAddon","ItemMouth","ItemMouth2","ItemMouth3",
"ItemArms","ItemNeckAccessories","ItemNeck","ItemNeckRestraints","ItemNipples","ItemNipplesPiercings","ItemBreast","ItemTorso",
"ItemHands","ItemPelvis","ItemVulva","ItemVulvaPiercings",
"ItemDevices","ItemLegs","ItemFeet","ItemBoots"
],
MAP_ACTIONS: { //ActivityFemale3DCG
// action
"nod|yes": {Head: {self: "Nod"}},
"no": {Head: {self: "Wiggle"}},
"moan": {Mouth: {self: "MoanGag"}},
"mumble": {Mouth: {self: "MoanGagTalk"}},
"whimper": {Mouth: {self: "MoanGagWhimper"}},
"groan": {Mouth: {self: "MoanGagGroan"}},
"scream": {Mouth: {self: "MoanGagAngry"}},
"giggle": {Mouth: {self: "MoanGagGiggle"}},
"struggle": {Arms: {self: "StruggleArms"}},
"thrash": {Legs: {self: "StruggleLegs"}},
// action zone
"wiggle|shake": {"Arms,Breast,Boots,Butt,Ears,Feet,Hands,Nose,Pelvis,Torso": {self: "Wiggle"}},
// action target
"whisper": {Ears: {others: "Whisper"}},
"choke": {Neck: {all: "Choke"}},
"brush": {Head: {all: "TakeCare"}},
"french": {Mouth: {others: "FrenchKiss"}},
"sit": {Legs: {others: "Sit"}},
"rim": {Butt: {others: "MasturbateTongue"}},
"press": {Butt: {others: "Step"}},
"rest": {Torso: {others: "Step"}},
"pet": {Head: {all: "Pet"}},
"boop": {Nose: {all: "Pet"}},
"cuddle": {Arms: {others: "Cuddle"}},
"nuzzle": {Nose: {others: "Cuddle"}},
"grab": {Arms: {others: "Grope"}},
"clean": {Mouth: {all: "Caress"}},
"lap": {Legs: {others: "RestHead"}},
"lean": {Breast: {others: "RestHead"}},
"peck": {Mouth: {others: "PoliteKiss"}},
// action zone target
"item": {
"Breast,Butt,Feet,Legs": {all: "SpankItem|TickleItem|RubItem|RollItem|MasturbateItem|PourItem|ShockItem|Inject"},
"Nipples,Pelvis": {all: "SpankItem|TickleItem|RubItem|RollItem|MasturbateItem|PourItem|ShockItem"},
Arms: {all: "SpankItem|TickleItem|RubItem|RollItem|PourItem|ShockItem|Inject"},
Boots: {all: "SpankItem|TickleItem|RubItem|RollItem|PourItem|ShockItem"},
"Ears,Mouth": {all: "TickleItem|RubItem|RollItem"},
"Hood,Nose": {all: "TickleItem|RubItem"},
Neck: {all: "TickleItem|RubItem|RollItem|Inject"},
Torso: {all: "SpankItem|TickleItem|RubItem|RollItem|PourItem|ShockItem"},
Vulva: {all: "SpankItem|TickleItem|RubItem|MasturbateItem|ShockItem"},
VulvaPiercings: {all: "SpankItem|TickleItem|RubItem|MasturbateItem|ShockItem|Inject"},
},
"kiss": {
Mouth: {others: "Kiss|GagKiss|GaggedKiss"},
2022-07-05 01:04:07 +00:00
"Boots,Hands": {self: "PoliteKiss", others: "PoliteKiss|GaggedKiss"},
"Arms,Breast,Nipples": {self: "Kiss", others: "Kiss|GaggedKiss"},
2022-07-04 22:07:57 +00:00
"Butt,Ears,Feet,Head,Legs,Neck,Nose,Pelvis,Torso,Vulva,VulvaPiercings": {others: "Kiss|GaggedKiss"}
},
2022-07-05 01:04:07 +00:00
"smooch": {"Hands,Boots": {all: "Kiss"}},
2022-07-04 22:07:57 +00:00
"nibble|chew": {"Arms,Hands,Boots,Mouth,Nipples": {all: "Nibble"}, "Ears,Feet,Legs,Neck,Nose,Pelvis,Torse,Vulva,VulvaPiercings": {others: "Nibble"}},
"slap|spank": {"Head,Breast,Vulva,VulvaPiercings": {all: "Slap"}, "Arms,Boots,Butt,Feet,Hands,Legs,Pelvis,Torso": {all: "Spank"}},
"tickle": {"Arms,Boots,Breast,Feet,Legs,Neck,Pelvis,Torso": {all: "Tickle"}},
"massage": {"Arms,Boots,Feet,Legs,Neck,Pelvis,Torso": {all: "MassageHands"}},
"lick": {"Arms,Boots,Breast,Hands,Mouth,Nipples": {all: "Lick"}, "Ears,Feet,Legs,Neck,Nose,Pelvis,Torso,Vulva,VulvaPiercings": {others: "Lick"}},
"suck": {"Nipples,Hands,Boots": {all: "Suck"}},
"bite": {"Arms,Boots,Feet,Hands,Legs,Mouth": {all: "Bite"}, "Breast,Butt,Ears,Head,Neck,Nipples,Nose,Torso": {others: "Bite"}},
"pinch": {"Arms,Ears,Nipples,Nose,Pelvis": {all: "Pinch"}},
"clamp": {Mouth: {all: "HandGag"}, Nose: {all: "Choke"}},
"step": {"Breast,Neck,Pelvis": {others: "Step"}},
"pull": {"Head,Nose,Nipples": {all: "Pull"}},
"grope": {"Butt,Breast": {all: "Grope"}, "Feet,Legs,Pelvis": {others: "Grope"}},
"rub": {"Head,Torso": {others: "Rub"}, Nose: {all: "Rub"}, Legs: {self: "Wiggle"}, Hands: {self: "Caress"}},
"caress": {Hands: {others: "Caress"}, "Arms,Breast,Butt,Ears,Feet,Head,Legs,Neck,Nipples,Nose,Pelvis,Torso,Vulva,VulvaPiercings": {all: "Caress"}},
"polish": {"Hands,Boots": {all: "TakeCare"}},
"foot": {"Head,Nose": {others: "Step"}, "Torso,Boots": {others: "MassageFeet"}, "Vulva,VulvaPiercings": {others: "MasturbateFoot"}},
"fist": {"Vulva,Butt": {all: "MasturbateFist"}},
"fuck": {"Mouth,Vulva,Butt": {others: "PenetrateSlow"}},
"pound": {"Mouth,Vulva,Butt": {others: "PenetrateFast"}},
"tongue": {"Vulva,VulvaPiercings": {others: "MasturbateTongue"}},
"finger": {"Breast,Butt,Vulva,VulvaPiercings": {all: "MasturbateHand"}},
},
MAP_ZONES: {
"ItemBoots": ["foot", "feet", "boot", "boots", "shoe", "shoes", "toes", "toenails", "sole", "soles", "heel", "heels"],
"ItemFeet": ["leg", "legs", "ankle", "ankles"],
"ItemLegs": ["hips", "hip", "thighs", "thigh"],
"ItemVulva": ["vulva", "pussy"],
"ItemVulvaPiercings": ["clit", "clitoris"],
"ItemButt": ["butt", "ass"],
"ItemPelvis": ["tummy", "pelvis"],
"ItemTorso": ["body", "torso", "back", "ribs"],
"ItemBreast": ["breast", "breasts", "boob", "boobs", "booby", "boobie", "boobies", "tit", "tits", "titty", "tittie", "titties"],
"ItemNipples": ["nip", "nips", "nipple", "nipples"],
"ItemHands": ["hand", "hands", "fingers", "fingernails", "nails"],
"ItemArms": ["arm", "arms", "elbow", "elbows"],
"ItemNeck": ["neck"],
"ItemMouth": ["mouth", "lip", "lips", "teeth", "tongue", "gag", "cheek", "cheeks"],
"ItemNose": ["nose", "nostrils"],
"ItemEars": ["ear", "ears", "earlobe", "earlobes"],
"ItemHead": ["head", "face", "hair", "eyes"],
},
USAGE_MBCHC_LINES: [
"<div>Usage: /mbchc SUBCOMMAND [ARGS...]</div>",
"<div>/mbchc versions : show the mod versions across the room</div>",
"<div>/mbchc autohack : toggle the autohack feature</div>",
"<div>/mbchc disappear : become invisible (requires anal hook -> hair)</div>",
2022-07-06 07:02:59 +00:00
"<div>/mbchc donate TARGET : Buy data and send it to recipient</div>",
2022-07-04 22:07:57 +00:00
"<div>/mbchc title TITLE : set a custom title (<b>WIP</b>)</div>",
2022-07-05 01:04:07 +00:00
"<div>/mbchc tz TARGET NUM : set target's UTC offset</div>",
"<div>/mbchc purge! : delete MBCHC online saved data</div>",
2022-07-04 22:07:57 +00:00
],
COMMANDS: [
{ Tag: "mbchc", Description: ": Utility functions (\"/mbchc\" for help)", Action: (argline, cmdline, args) => { window.MBCHC.command_mbchc(argline, cmdline, args) } },
{ Tag: "activity", Description: "[Message]: Send a custom activity (or \"@@Message\", or \"@Message\" as yourself)", Action: (argline, cmdline, args) => { window.MBCHC.command_activity(argline, cmdline, args) } },
{ Tag: "do", Description: ": Do an activity, as if clicked on its button (\"/do\" for help)", Action: (argline, cmdline, args) => { window.MBCHC.command_do(argline, cmdline, args) } },
],
2022-07-06 07:02:59 +00:00
ensure: function(error, callback) {
let result = callback.call(this)
if (!result) throw error
return(result)
},
2022-07-05 17:53:00 +00:00
calculate_maps: function() {
this.DO_DATA = {verbs: {}, zones: {}}
for (let [verbs, data] of Object.entries(this.MAP_ACTIONS)) {
let unwound = {}
for (let [zones, actions] of Object.entries(data)) {
let all = (actions.all) ? actions.all.split("|") : []
let processed = {self: (actions.self) ? actions.self.split("|").concat(all) : all, others: (actions.others) ? actions.others.split("|").concat(all) : all}
for (let zone of zones.split(",")) unwound[`Item${zone}`] = processed
}
for (let verb of verbs.split("|")) this.DO_DATA.verbs[verb] = unwound
}
for (let [ag, zones] of Object.entries(this.MAP_ZONES)) for (let i in zones) this.DO_DATA.zones[zones[i]] = ag
},
2022-07-05 01:04:07 +00:00
settings: function(setting = null) {
let settings = window.Player.OnlineSettings.MBCHC || {}
return(setting ? settings[setting] : settings)
},
save_settings: function(cb = null) {
if (!window.Player.OnlineSettings.MBCHC) window.Player.OnlineSettings.MBCHC = {}
if (cb) cb.call(this, window.Player.OnlineSettings.MBCHC)
window.ServerAccountUpdate.QueueData({OnlineSettings: window.Player.OnlineSettings})
},
2022-07-04 22:07:57 +00:00
log: function(msg) {return("MBCHC: " + String(msg))},
empty: function(text) {
if (!text) return(true)
if (String(text).trim().length < 1) return(true)
return(false)
},
normalise_message: function(text, options = {}) {
let result = text
if (options.trim) result = result.trim()
if (options.low) result = result.toLocaleLowerCase()
if (options.up) {
let first = result.at(0).toLocaleUpperCase()
let rest = result.slice(1)
result = first + rest
}
if (options.dot && result.match(/[\w]$/)) result = `${result}.`
return(result)
},
bg_colour: function() { return(document.getElementById("TextAreaChatLog").dataset.colortheme.startsWith("light") ? this.RGB_POLLY : this.RGB_MUTE) },
inform: function(html) { window.ChatRoomSendLocal(`<div style="background:${this.bg_colour()}">${html}</div>`, 60000) },
2022-07-04 22:07:57 +00:00
report: function(x) { this.inform(`Error: ${x.toString()}`) },
id2char: function(id) { return(window.ChatRoomCharacter.find( c => c.MemberNumber === Number.parseInt(id) )) },
donate_data: function(recipient) {
let id = Number.parseInt(recipient)
2022-07-06 07:02:59 +00:00
if (isNaN(id)) throw "empty or invalid target"
if (id === window.Player.MemberNumber) throw "target must not be you"
const char = this.ensure("target not found", () => this.id2char(id))
if (!char.IsRestrained()) throw "target must be bound"
2022-07-04 22:07:57 +00:00
const cost = (Math.random() * 10 + 15).toFixed(0)
if (window.Player.Money < cost) throw "not enough money"
window.CharacterChangeMoney(window.Player, -cost)
window.ServerSend("ChatRoomChat", {Content: "ReceiveSuitcaseMoney", Type: "Hidden", Target: id})
return({cost: cost, name: window.CharacterNickname(char)})
},
run_activity: function(char, ag, action) { try {
2022-07-06 07:02:59 +00:00
char.FocusGroup = this.ensure("invalid AssetGroup", () => window.AssetGroupGet(char.AssetFamily, ag))
let activity = this.ensure("invalid activity", () => window.ActivityAllowedForGroup(char, char.FocusGroup.Name, true).find( a => a.Name === action))
2022-07-05 17:53:00 +00:00
if (activity.Name.endsWith("Item")) {
var dict = [{Tag: "NextAsset", AssetName: "SpankingToys"}, {Tag: "FocusAssetGroup", AssetGroupName: ag}, {Tag: "SourceCharacter", Text: window.CharacterNickname(window.Player), MemberNumber: window.Player.MemberNumber}]
dict.push({Tag: "DestinationCharacter", Text: window.CharacterNickname(char), MemberNumber: char.MemberNumber})
window.ServerSend("ChatRoomChat", {Type: "Action", Content: `ActionActivity${activity.Name}`, Dictionary: dict})
} else {
window.ActivityRun(char, activity)
}
2022-07-04 22:07:57 +00:00
} finally {
char.FocusGroup = null
} },
char2dict: function(type, id) { return({Tag: `${type}Character`, MemberNumber: id, Text: window.CharacterNickname(this.id2char(id))}) },
send_activity: function(msg) {
let dict = [{Tag: "MISSING PLAYER DIALOG: ", Text: ""}]
let chars = msg.match(this.RE_ACT_CHARS)
if (chars) {
msg = msg.replace(this.RE_ACT_CHARS, "")
if (chars[1]) dict.push(this.char2dict("Source", chars[1]))
if (chars[2]) dict.push(this.char2dict("Target", chars[2]), this.char2dict("Destination", chars[2]))
}
window.ServerSend("ChatRoomChat", {Type: "Action", Content: msg, Dictionary: dict})
},
receive: function(data) {
if (data.Sender === window.Player.MemberNumber) return // this is our own message, sent back to us
2022-07-06 07:02:59 +00:00
let char = this.ensure(`Invalid message sender: ${data.Sender}`, () => this.id2char(data.Sender))
let payload = this.ensure("Empty message", () => data.Dictionary[0])
2022-07-04 22:07:57 +00:00
switch (payload.type) {
case "greetings": case "hello":
char.MBCHC = payload.value
if ("greetings" === payload.type) this.hello(char.MemberNumber)
break
default: // if we don't know the type it may be from a newer version
}
},
hello: function(target = null) {
let payload = {type: "greetings", value: window.Player.MBCHC}
if (target) payload.type = "hello"
let message = {Content: "MBCHC", Type: "Hidden", Dictionary: [payload]}
if (target) message.Target = target
window.ServerSend("ChatRoomChat", message)
},
disappear: function() {
let item = window.InventoryGet(window.Player, "ItemButt")
if (!item || !item.Asset || !item.Asset.Name) throw "butt seems empty"
if (item.Asset.Name !== "AnalHook") throw "butt seems occupied by something other than the anal hook"
if (!item.Property.Type || item.Property.Type !== "Hair") throw "anal hook seems not tied to hair"
item.Property = {Type: "Hair", Hide: this.HIDE_ALL}
window.CharacterRefresh(window.Player, true, true)
},
title: function(args) {
let title = args.shift()
if (this.empty(title)) throw "empty title"
title = this.normalise_message(title, {trim: true, up: true, low: true})
if (title.length > 16) throw "title too long"
if (!title.match(this.RE_TITLE)) throw "invalid title"
window.TitleSet(title)
// TODO: this needs much more work. at least don't push a second title
// we need to patch the text cache
// we need to check for other players' custom titles
//window.TitleList.push({Name: title, Requirement: () => {return true}})
},
patch_handheld: function() {
let options = InventoryItemHandsSpankingToysOptions /* eslint-disable-line no-undef */ // window.InventoryItemHandsSpankingToysOptions is undefined
for (let i in this.HAND_PENETRATORS) {
let option = options.find(o => o.Name === this.HAND_PENETRATORS[i])
if (option && option.Property) {
if (!option.Property.Attribute) option.Property.Attribute = []
if (option.Property.Attribute.indexOf("PenetrateItem") < 0) option.Property.Attribute.push("PenetrateItem")
}
}
},
gather_versions: function() {
let result = []
for (let i in window.ChatRoomCharacter) {
let c = window.ChatRoomCharacter[i]
if (c.MBCHC) result.push({name: window.CharacterNickname(c), id: c.MemberNumber, version: c.MBCHC.VERSION})
}
return(result)
},
2022-07-04 22:19:38 +00:00
need_load_hook: function(module, screen) {
if (!module || !screen) return(true)
if (("Character" === module) && ("Login" === screen)) return(true)
return(false)
},
2022-07-05 01:04:07 +00:00
find_timezone: function(char) {
let timezones = this.settings("timezones")
if (timezones && timezones[char.MemberNumber]) return(timezones[char.MemberNumber])
2022-07-05 17:53:00 +00:00
let match = (char.Description) ? char.Description.match(this.RE_TZ) : null
2022-07-05 01:04:07 +00:00
if (match) return(Number.parseInt(match[1]))
return(null)
},
2022-07-05 17:53:00 +00:00
player_enters_room: function() { // FIXME: Description may not be ready yet
2022-07-05 01:04:07 +00:00
this.hello()
for (let i in window.ChatRoomCharacter) {
let char = window.ChatRoomCharacter[i]
if (!char.MBCHC_LOCAL) char.MBCHC_LOCAL = {}
if (!char.MBCHC_LOCAL.TZ) {
let tz = this.find_timezone(char)
if (tz) char.MBCHC_LOCAL.TZ = tz
}
}
},
set_timezone: function(args) {
2022-07-06 07:02:59 +00:00
let char = this.ensure("invalid target", ()=> this.id2char(args[0]))
2022-07-05 01:04:07 +00:00
let tz = Number.parseInt(args[1])
if (isNaN(tz)) throw "invalid offset"
if ((tz < -12) || (tz > 12)) throw "offset should be [-12,12]"
if (!char.MBCHC_LOCAL) char.MBCHC_LOCAL = {}
char.MBCHC_LOCAL.TZ = tz
this.save_settings((s) => { if (!s.timezones) s.timezones = {}; s.timezones[char.MemberNumber] = tz })
},
2022-07-04 22:07:57 +00:00
// Command actions
command_mbchc: function(argline, cmdline, args) { try {
if (args.length < 1) return(this.inform(this.USAGE_MBCHC_LINES.join("")))
let cmd = String(args.shift()).toLocaleLowerCase()
switch (cmd) {
case "versions": this.inform(this.gather_versions().map(c => { return(`<div><b>${c.name}</b> (${c.id}): ${c.version}</div>`) }).join("")); break
case "disappear": this.disappear(); break
case "title": this.title(args); break
case "autohack":
this.AUTOHACK_ENABLED = !this.AUTOHACK_ENABLED
this.inform(`Autohack is now ${this.AUTOHACK_ENABLED ? "enabled" : "disabled"}`)
break
case "donate":
var result = this.donate_data(args[0])
window.ChatRoomMessage({Sender: window.Player.MemberNumber, Type: "Action", Content: `You've bought data for $${result.cost} and sent it to ${result.name}.`, Dictionary: [{Tag: "MISSING PLAYER DIALOG: ", Text: ""}]})
break
2022-07-05 01:04:07 +00:00
case "tz": this.set_timezone(args); break
case "purge!":
if (window.Player.OnlineSettings.MBCHC) {
delete window.Player.OnlineSettings.MBCHC
this.save_settings()
}
break
2022-07-04 22:07:57 +00:00
default: throw `unknown subcommand "${cmd}"`
}
} catch (x) { this.report(x) } },
command_activity: function(argline, cmdline, args) { if (!this.empty(argline)) { try {
2022-07-05 17:53:00 +00:00
let message = this.normalise_message(cmdline.replace(this.RE_ACTIVITY, ''), {trim: true, dot: true, up: true})
this.send_activity(message)
2022-07-04 22:07:57 +00:00
} catch (x) { this.report(x) } } },
command_do: function(argline, cmdline, args) { try {
2022-07-06 07:02:59 +00:00
// FIXME arousal for items
2022-07-05 17:53:00 +00:00
if (args.length < 1) return(this.inform("<div>Usage: /do VERB [ZONE] [TARGET]</div><div>Available verbs:</div>" + Object.keys(this.MAP_ACTIONS).join(", ") + "<div>Available zones:</div>" + Object.keys(this.DO_DATA.zones).join(", ")))
let [verb, zone, target] = args
if (!this.DO_DATA.verbs[verb]) throw `unknown verb "${verb}"`
let zones = this.DO_DATA.verbs[verb]
2022-07-06 07:02:59 +00:00
if (1 === Object.keys(zones).length) {
if (!target) target = zone
zone = this.MAP_ZONES[Object.keys(zones)[0]][0]
}
2022-07-05 17:53:00 +00:00
if (!zone) throw "zone missing"
2022-07-06 07:02:59 +00:00
let ag = this.ensure(`unknown zone "${zone}"`, () => this.DO_DATA.zones[zone])
let types = this.ensure(`zone "${zone}" invalid for "${verb}"`, () => zones[ag])
2022-07-05 17:53:00 +00:00
let char = window.Player
2022-07-06 07:02:59 +00:00
if (target && ((types.self.length < 1) || (types.others.length > 0))) char = this.ensure("invalid target", () => this.id2char(target))
2022-07-05 17:53:00 +00:00
let type = (char.MemberNumber === window.Player.MemberNumber) ? "self" : "others"
let available = window.ActivityAllowedForGroup(char, ag)
let toy = window.InventoryGet(window.Player, "ItemHands")
if (toy && toy.Asset.Name === "SpankingToys") available.push(window.AssetAllActivities(char.AssetFamily).find(a => a.Name === window.InventorySpankingToysGetActivity(window.Player)))
2022-07-06 07:02:59 +00:00
let actions = this.ensure(`zone "${zone}" invalid for ("${verb}" "${type}")`, () => types[type])
let action = this.ensure(`invalid action (${verb} ${zone} ${target})`, () => actions.find(name => available.find(a => a.Name === name)))
2022-07-05 17:53:00 +00:00
this.run_activity(char, ag, action)
2022-07-04 22:07:57 +00:00
} catch (x) { this.report(x) } },
loader: function() {
if (this.LOADED) return //FIXME: unhook if hooked, no need to call this every time
// Calculated values
this.HIDE_ALL = this.HIDE_SPECIAL.concat(this.HIDE_BODY).concat(this.HIDE_CLOTHES).concat(this.HIDE_ITEMS)
this.CommandsKey = CommandsKey /* eslint-disable-line no-undef */ // window.CommandsKey is undefined
this.RE_ACTIVITY = RegExp(`^${this.CommandsKey}activity `)
this.PREF_ACTIVITY = `${this.CommandsKey}activity `
// Actions
2022-07-05 17:53:00 +00:00
this.calculate_maps()
2022-07-04 22:07:57 +00:00
this.patch_handheld()
window.Player.MBCHC = {VERSION: this.VERSION}
window.CommandCombine(this.COMMANDS)
this.LOADED = true
console.info(this.log(`loaded version ${this.VERSION}`))
}
} // MBCHC
// Hooks
window.MBCHC.sdk = window.bcModSdk.registerMod("MBCHC", window.MBCHC.VERSION)
window.MBCHC.sdk.hookFunction("ChatRoomMessageInvolvesPlayer", 0, (args, next) => {
let data = args[0]
if (!data.MBCHC_ID) {
data.MBCHC_ID = window.MBCHC.NEXT_MESSAGE
window.MBCHC.NEXT_MESSAGE += 1
if (window.MBCHC.LOG_MESSAGES) console.debug(data)
if (("ReceiveSuitcaseMoney" === data.Content) && ("Hidden" === data.Type)) { window.MBCHC.LAST_HACKED = data.Sender }
2022-07-05 01:04:07 +00:00
if (("ServerEnter" === data.Content) && ("Action" === data.Type) && (data.Sender === window.Player.MemberNumber)) { window.MBCHC.player_enters_room() }
2022-07-04 22:07:57 +00:00
if (("MBCHC" === data.Content) && ("Hidden" === data.Type)) { window.MBCHC.receive(data) }
}
return(next(args))
})
window.MBCHC.sdk.hookFunction("ChatRoomReceiveSuitcaseMoney", 0, (args, next) => {
let result = next(args)
if (window.MBCHC.AUTOHACK_ENABLED && window.MBCHC.LAST_HACKED) {
window.CurrentCharacter = {MemberNumber: window.MBCHC.LAST_HACKED}
window.MBCHC.LAST_HACKED = null
window.ChatRoomTryToTakeSuitcase()
}
return(result)
})
window.MBCHC.sdk.hookFunction("ChatRoomSendChat", 0, (args, next) => {
let input = window.ElementValue("InputChat")
if (!input.startsWith("@@@")) {
input = input.replace(window.MBCHC.RE_PREF_ACTIVITY, window.MBCHC.PREF_ACTIVITY)
input = input.replace(window.MBCHC.RE_PREF_ACTIVITY_ME, `${window.MBCHC.PREF_ACTIVITY} <${window.Player.MemberNumber}:>SourceCharacter `)
}
window.ElementValue("InputChat", input)
return(next(args))
})
window.MBCHC.sdk.hookFunction("ChatRoomDrawCharacterOverlay", 0, (args, next) => {
let [C, CharX, CharY, Zoom, Pos] = args
if ((window.ChatRoomHideIconState < 1) && C.MBCHC) {
let colour = (C.MBCHC.VERSION === window.Player.MBCHC.VERSION) ? window.MBCHC.RGB_POLLY : window.MBCHC.RGB_MUTE
2022-07-05 01:04:07 +00:00
window.DrawRect(CharX + 175 * Zoom, CharY, 50 * Zoom, 50 * Zoom, colour)
}
if (C.MBCHC_LOCAL && C.MBCHC_LOCAL.TZ) {
let localtime = new Date(window.CommonTime() + window.MBCHC.UTC_OFFSET + C.MBCHC_LOCAL.TZ * 60 * 60 * 1000)
let text = localtime.toLocaleTimeString([], {hourCycle: "h24", hour: "2-digit"})
window.DrawTextFit(text, CharX + 200 * Zoom, CharY + 25 * Zoom, 46 * Zoom, "white", "black")
2022-07-04 22:07:57 +00:00
}
return(next(args))
})
2022-07-04 22:19:38 +00:00
// MAIN SCREEN TURN ON
if (window.MBCHC.need_load_hook(window.CurrentModule, window.CurrentScreen)) {
window.MBCHC.sdk.hookFunction("AsylumGGTSSAddItems", 0, (args, next) => { window.MBCHC.loader(); return(next(args)) })
} else {
window.MBCHC.loader()
2022-07-05 01:04:07 +00:00
if(("Online" === window.CurrentModule) && ("ChatRoom" === window.CurrentScreen)) window.MBCHC.player_enters_room()
2022-07-04 22:19:38 +00:00
}
2022-07-04 22:07:57 +00:00
})()