diff --git a/mbchc-local.user.js b/mbchc-local.user.js index 3df622c..157e7b4 100644 --- a/mbchc-local.user.js +++ b/mbchc-local.user.js @@ -19,700 +19,564 @@ // 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 -(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>>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(ao.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, got ${typeof r}`);for(const[n,i]of Object.entries(r))"string"==typeof i?a.patches.set(n,i):null===i?a.patches.delete(n):e(`Mod '${o}' failed to patch function '${t}': Invalid format of patch '${n}'`);f()},removePatches:o=>{c.loaded||e(`Mod '${c.name}' attempted to call SDK function after being unloaded`);i(o).patches.clear(),f()},callOriginal:(t,n,r)=>(c.loaded||e(`Mod '${c.name}' attempted to call SDK function after being unloaded`),"string"==typeof t&&t||e(`Mod '${o}' failed to call a function: Expected function name string, got ${typeof t}`),Array.isArray(n)||e(`Mod '${o}' failed to call a function: Expected args array, got ${typeof n}`),function(o,e,t=window){return l(o).original.apply(t,e)}(t,n,r)),getOriginalHash:t=>("string"==typeof t&&t||e(`Mod '${o}' failed to get hash: Expected function name string, got ${typeof t}`),l(t).originalHash)},c={name:o,version:t,allowReplace:r,api:d,loaded:!0,patching:new Map};return u.set(o,c),Object.freeze(d)}function m(){const o=[];for(const e of u.values())o.push({name:e.name,version:e.version});return o}let w;const y=void 0===window.bcModSdk?window.bcModSdk=function(){const e={version:o,apiVersion:1,registerMod:g,getModsInfo:m,getPatchingInfo:p,errorReporterHooks:Object.seal({hookEnter:null,hookChainExit:null})};return w=e,Object.freeze(e)}():(n(window.bcModSdk)||e("Failed to init Mod SDK: Name already in use"),1!==window.bcModSdk.apiVersion&&e(`Failed to init Mod SDK: Different version already loaded ('1.0.2' vs '${window.bcModSdk.version}')`),window.bcModSdk.version!==o&&alert(`Mod SDK warning: Loading different but compatible versions ('1.0.2' vs '${window.bcModSdk.version}')\nOne of mods you are using is using an old version of SDK. It will work for now but please inform author to update`),window.bcModSdk);return"undefined"!=typeof exports&&(Object.defineProperty(exports,"__esModule",{value:!0}),exports.default=y),y})(); +;(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>>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(ao.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, got ${typeof r}`);for(const[n,i]of Object.entries(r))"string"==typeof i?a.patches.set(n,i):null===i?a.patches.delete(n):e(`Mod '${o}' failed to patch function '${t}': Invalid format of patch '${n}'`);f()},removePatches:o=>{c.loaded||e(`Mod '${c.name}' attempted to call SDK function after being unloaded`);i(o).patches.clear(),f()},callOriginal:(t,n,r)=>(c.loaded||e(`Mod '${c.name}' attempted to call SDK function after being unloaded`),"string"==typeof t&&t||e(`Mod '${o}' failed to call a function: Expected function name string, got ${typeof t}`),Array.isArray(n)||e(`Mod '${o}' failed to call a function: Expected args array, got ${typeof n}`),function(o,e,t=window){return l(o).original.apply(t,e)}(t,n,r)),getOriginalHash:t=>("string"==typeof t&&t||e(`Mod '${o}' failed to get hash: Expected function name string, got ${typeof t}`),l(t).originalHash)},c={name:o,version:t,allowReplace:r,api:d,loaded:!0,patching:new Map};return u.set(o,c),Object.freeze(d)}function m(){const o=[];for(const e of u.values())o.push({name:e.name,version:e.version});return o}let w;const y=void 0===window.bcModSdk?window.bcModSdk=function(){const e={version:o,apiVersion:1,registerMod:g,getModsInfo:m,getPatchingInfo:p,errorReporterHooks:Object.seal({hookEnter:null,hookChainExit:null})};return w=e,Object.freeze(e)}():(n(window.bcModSdk)||e("Failed to init Mod SDK: Name already in use"),1!==window.bcModSdk.apiVersion&&e(`Failed to init Mod SDK: Different version already loaded ('1.0.2' vs '${window.bcModSdk.version}')`),window.bcModSdk.version!==o&&alert(`Mod SDK warning: Loading different but compatible versions ('1.0.2' vs '${window.bcModSdk.version}')\nOne of mods you are using is using an old version of SDK. It will work for now but please inform author to update`),window.bcModSdk);return"undefined"!=typeof exports&&(Object.defineProperty(exports,"__esModule",{value:!0}),exports.default=y),y})() -(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", - TARGET_VERSION: "R85", - NEXT_MESSAGE: 1, - LOG_MESSAGES: false, - RETHROW: false, - LOADED: false, - AUTOHACK_ENABLED: false, - LAST_HACKED: null, - HISTORY_MODE: false, - RE_TITLE: /^[a-zA-Z]+$/, - RE_PREF_ACTIVITY_ME: /^@/, - RE_PREF_ACTIVITY: /^@@/, - RE_ACT_CIDS: /^<(\d+)?:(\d+)?>/, - RE_TZ: /(?:GMT|UTC)([+-]\d\d?)/i, - RE_ALL_LEFT: /^<+$/, - RE_ALL_RIGHT: /^>+$/, - RE_SPACES: /\s{2,}/g, - RE_LAST_WORD: /(^|\s)([^\s]*)$/, - RE_LAST_LETTER: /[\w]$/, - RGB_MUTE: "#6c2132", - RGB_POLLY: "#81b1e7", - UTC_OFFSET: new Date().getTimezoneOffset() * 60 * 1000, - 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","LeftHand","Mouth","Nipples","Pussy","RightHand"], - 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","ItemTorso2", - "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: "GagKiss|Kiss|GaggedKiss"}, - "Boots,Hands": {self: "PoliteKiss", others: "PoliteKiss|GaggedKiss"}, - "Arms,Breast,Nipples": {self: "Kiss", others: "Kiss|GaggedKiss"}, - "Butt,Ears,Feet,Head,Legs,Neck,Nose,Pelvis,Torso,Vulva,VulvaPiercings": {others: "Kiss|GaggedKiss"} - }, - "smooch": {"Hands,Boots": {all: "Kiss"}}, - "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"}}, //peg? - "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", "forehead"], - }, - FBC_TESTER_PATCHES: [ - [/^\^('s)?( )?/g, "^SourceCharacter$1\\s+"], - [/([^\\])\$/g, "$1\\.?$$"], - ], - SUBCOMMANDS_MBCHC: { - "versions": {desc: "show the mod versions across the room", cb: mbchc => mbchc.inform(mbchc.gather_versions().map(c => `
${c.name} (${c.cid}): ${c.version}
`).join(""))}, - "autohack": {desc: "toggle the autohack feature", cb: mbchc => mbchc.inform(`Autohack is now ${(mbchc.AUTOHACK_ENABLED = !mbchc.AUTOHACK_ENABLED) ? "enabled" : "disabled"}`)}, - "disappear": {desc: "become invisible (requires anal hook -> hair)", cb: mbchc => mbchc.disappear()}, - "donate": {desc: "Buy data and send it to recipient", args: {TARGET: {}}, cb: (mbchc, args) => mbchc.donate_data(args[0])}, - "title": {desc: "set a custom title (WIP)", args: {TITLE: {}}, cb: (mbchc, args) => mbchc.title(args[0])}, - "tz": {desc: "set target's UTC offset", args: {OFFSET: {}, "[TARGET]": {}}, cb: (mbchc, args) => mbchc.set_timezone(args)}, - "purge!": {desc: "delete MBCHC online saved data", cb: mbchc => {if (window.Player.OnlineSettings.MBCHC) {delete window.Player.OnlineSettings.MBCHC; mbchc.save_settings()}}}, - }, - ensure: function(error, callback) { - let result = callback.call(this) - if (!result) throw error - return(result) - }, - 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 zone of zones) this.DO_DATA.zones[zone] = ag - }, - settings: function(setting = null) { - let settings = window.Player.OnlineSettings.MBCHC || {} - return(setting ? settings[setting] : settings) - }, - save_settings: function(cb = null) { - if (cb) { - if (!window.Player.OnlineSettings.MBCHC) window.Player.OnlineSettings.MBCHC = {} - cb.call(this, window.Player.OnlineSettings.MBCHC) - } - window.ServerAccountUpdate.QueueData({OnlineSettings: window.Player.OnlineSettings}) - }, - log: function(level, msg) {console[level]("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(this.RE_LAST_LETTER)) result = `${result}.` - return(result) - }, - tokenise: function(text) { return text.replace(this.RE_SPACES, " ").split(" ") }, - inform: function(html, timeout = 60000) { window.ChatRoomSendLocal(`
${html}
`, timeout) }, - report: function(x) { - this.inform(`Error: ${x.toString()}`) - if (this.RETHROW) throw x - }, - in: function(x, floor, ceiling) { return((x >= floor) && (x <= ceiling)) }, - cid2char: function(cid) { - cid = Number.parseInt(cid) - if (cid === window.Player.cid) return(window.Player) - return(this.ensure(`character ${cid} not found in the room`, () => window.ChatRoomCharacter.find(c => c.cid === cid))) - }, - pos2char: function(pos) { - if (!this.in(pos, 0, window.ChatRoomCharacter.length - 1)) throw `invalid position ${pos}` - return(window.ChatRoomCharacter[pos]) - }, - rel2char: function(target) { - let me = this.ensure("can't find my position", () => window.ChatRoomCharacter.findIndex(char => char.IsPlayer()) + 1) - 1 // 0 is falsy, but is valid index - let pos = null - if (target.match(this.RE_ALL_LEFT)) pos = me - target.length - if (target.match(this.RE_ALL_RIGHT)) pos = me + target.length - if (null === pos) throw `failed to parse target "${target}"` - pos = pos % window.ChatRoomCharacter.length - if (pos < 0) pos = pos + window.ChatRoomCharacter.length - return(this.pos2char(pos)) - }, - target2char: function(target) { // target should be lowcase - let input = target - if (this.empty(target)) return(window.Player) - let int = Number.parseInt(target) - target = String(target) - let found = [] - if (target.startsWith("=")) return(this.cid2char(target.slice(1))) - if (target.startsWith("<") || target.startsWith(">")) return(this.rel2char(target)) - if (!isNaN(int) && int.toString() === target) { // we got a number - if (this.in(int, 0, 9)) return(this.pos2char(int)) - if (this.in(int, 11, 15)) return(this.pos2char(int - 11)) - if (this.in(int, 21, 25)) return(this.pos2char(int - 16)) - found.push(...window.ChatRoomCharacter.filter(c => c.cid.toString().indexOf(target) > -1)) - } - if (target.startsWith("@")) target = target.slice(1) - found.push(...window.ChatRoomCharacter.filter(c => c.Name.toLocaleLowerCase().indexOf(target) > -1)) - found.push(...window.ChatRoomCharacter.filter(c => c.Nickname && (c.Nickname.toLocaleLowerCase().indexOf(target) > -1))) - let map = {} - found.forEach(c => {if (!map[c.cid]) map[c.cid] = c} ) - found = Object.values(map) - if (found.length < 1) throw `target "${input}": no match` - if (found.length > 1) throw `target "${input}": multiple matches (${found.map(c => `${c.cid}|${c.Name}|${c.Nickname || c.Name}`).join(",")})` - return(found[0]) - }, - char2targets: function(char) { - let [result, cid] = [new Set(), char.cid.toString()] - result.add(cid).add(`=${cid}`) - this.tokenise(char.Name).forEach(t => {result.add(t); result.add(`@${t}`)}) - if (char.Nickname) this.tokenise(char.Nickname).forEach(t => {result.add(t); result.add(`@${t}`)}) - return result - }, - donate_data: function(target) { - let char = this.target2char(target) - if (char.IsPlayer()) throw "target must not be you" - if (!char.IsRestrained()) throw "target must be bound" - const cost = Math.round((Math.random() * 10 + 15)) - if (window.Player.Money < cost) throw "not enough money" - window.CharacterChangeMoney(window.Player, -cost) - window.ServerSend("ChatRoomChat", {Content: "ReceiveSuitcaseMoney", Type: "Hidden", Target: char.cid}) - window.ChatRoomMessage({Sender: window.Player.cid, Type: "Action", Content: `You've bought data for $${cost} and sent it to ${char.dn}.`, Dictionary: [{Tag: "MISSING PLAYER DIALOG: ", Text: ""}]}) - }, - run_activity: function(char, ag, action) { try { - if (!window.ActivityAllowed()) throw "activities disabled in this room" - if (!window.ServerChatRoomGetAllowItem(window.Player, char)) throw "no permissions" - 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)) - if (activity.Name.endsWith("Item")) { - const item = this.ensure("no toy found", () => window.Player.Inventory.find(i => i.Asset?.Name === "SpankingToys" && i.Asset.Group?.Name === char.FocusGroup.Name && window.AssetSpankingToys.DynamicActivity(char) === activity.Name)) - window.DialogPublishAction(char, item) - } else window.ActivityRun(char, activity) - } finally { - char.FocusGroup = null - } }, - replace_me: function(match, offset, string) { - let text = string.slice(1) - let suffix = " " - if (text.startsWith("'") || text.startsWith(" ")) suffix = "" - return `${window.MBCHC.PREF_ACTIVITY}<${window.Player.cid}:>SourceCharacter${suffix}` - }, - cid2dict: function(type, cid) { return({Tag: `${type}Character`, MemberNumber: cid, Text: this.cid2char(cid).dn}) }, - send_activity: function(msg) { - let dict = [{Tag: "MISSING PLAYER DIALOG: ", Text: ""}] - let cids = msg.match(this.RE_ACT_CIDS) - if (cids) { - msg = msg.replace(this.RE_ACT_CIDS, "") - if (cids[1]) dict.push(this.cid2dict("Source", cids[1])) - if (cids[2]) dict.push(this.cid2dict("Target", cids[2]), this.cid2dict("Destination", cids[2])) - } - window.ServerSend("ChatRoomChat", {Type: "Action", Content: msg, Dictionary: dict}) - }, - receive: function(data) { - let char = this.cid2char(data.Sender) - if (char.IsPlayer()) return true // this is our own message, sent back to us - let payload = this.ensure("Empty message", () => data.Dictionary[0]) - switch (payload.type) { - case "greetings": case "hello": - char.MBCHC = payload.value - if ("greetings" === payload.type) this.hello(char) - break - default: // if we don't know the type it may be from a newer version - } - return true - }, - hello: function(char = null) { - let payload = {type: "greetings", value: window.Player.MBCHC} - if (char) payload.type = "hello" - let message = {Content: "MBCHC", Type: "Hidden", Dictionary: [payload]} - if (char) message.Target = char.cid - 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(title) { // WIP - 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) - //window.TitleList.push({Name: title, Requirement: () => true}) // check for existing first - }, - patch_handheld: function() { - let options = InventoryItemHandsSpankingToysOptions /* eslint-disable-line no-undef */ // window.InventoryItemHandsSpankingToysOptions is undefined - for (let name of this.HAND_PENETRATORS) { - let option = options.find(o => o.Name === name) - if (option && option.Property) { - if (!option.Property.AllowActivity) option.Property.AllowActivity = [] - if (option.Property.AllowActivity.indexOf("PenetrateItem") < 0) option.Property.AllowActivity.push("PenetrateItem") - } - } - }, - copy_fbc_trigger: function(trigger) { - let result = { - Type: "Action", - Event: trigger.Event, - Matchers: trigger.Matchers.map(m => ({Tester: new RegExp(this.FBC_TESTER_PATCHES.reduce((ax,[f,r]) => ax.replaceAll(f,r), m.Tester.source), "u")})) - } - return(result) - }, - patch_fbc: function() { - this.remove_fbc_hook() - delete this.remove_fbc_hook - window.bce_ActivityTriggers.push(...window.bce_ActivityTriggers.filter(t => "Emote" === t.Type).map(t => this.copy_fbc_trigger(t))) - /* (["anim", "pose"]).forEach(tag => {let cmd = window.Commands.find(c => tag === c.Tag); if (cmd) cmd.AutoComplete = this[`complete_fbc_${tag}`]}) */ // this line explodes, don't ask me why - let cmd = window.Commands.find(c => "anim" === c.Tag) - if (cmd) cmd.AutoComplete = this.complete_fbc_anim - cmd = window.Commands.find(c => "pose" === c.Tag) - if (cmd) cmd.AutoComplete = this.complete_fbc_pose - }, - gather_versions: function() { return(window.ChatRoomCharacter.filter(c => c.MBCHC).map(c => ({name: c.dn, cid: c.cid, version: c.MBCHC.VERSION}))) }, - need_load_hook: function(module, screen) { - if (!module || !screen) return(true) - if (("Character" === module) && ("Login" === screen)) return(true) - return(false) - }, - find_timezone: function(char) { - let timezones = this.settings("timezones") - if (timezones && timezones[char.cid]) return(timezones[char.cid]) - let match = (char.Description) ? char.Description.match(this.RE_TZ) : null - if (match) return(Number.parseInt(match[1])) - return(null) - }, - player_enters_room: function() { // or if the mod is loaded while player is in the room - this.hello() - }, - set_timezone: function(args) { - let tz = Number.parseInt(args[0]) - if (isNaN(tz)) throw `invalid offset "${args[0]}"` - if (!this.in(tz, -12, 12)) throw "offset should be [-12,12]" - let char = this.target2char(args[1]) - char.MBCHC_LOCAL.TZ = tz - this.save_settings(s => {if (!s.timezones) s.timezones = {}; s.timezones[char.cid] = tz}) - }, - update_char: function(char) { - char.cid = char.MemberNumber // Club ID (shorter) - char.dn = window.CharacterNickname(char) // DisplayName (shortcut) - if (!char.MBCHC_LOCAL) char.MBCHC_LOCAL = {} - if (!char.MBCHC_LOCAL.TZ) char.MBCHC_LOCAL.TZ = this.find_timezone(char) - }, - command_mbchc: function(argline, cmdline, args) { const mbchc = window.MBCHC; try { // `this` is command object - if (args.length < 1) return(mbchc.inform(Object.entries(mbchc.SUBCOMMANDS_MBCHC).map(([cmd, sub]) => `
/mbchc ${cmd} ${sub.args ? Object.keys(sub.args).join(" ") : ""}: ${sub.desc}
`).join(""))) - let cmd = String(args.shift()) - let sub = mbchc.ensure(`unknown subcommand "${cmd}"`, () => mbchc.SUBCOMMANDS_MBCHC[cmd]) - sub.cb.call(mbchc, mbchc, args, argline, cmdline) - } catch (x) { mbchc.report(x) } }, - command_activity: function(argline, cmdline, args) { const mbchc = window.MBCHC; if (!mbchc.empty(argline)) { try { // `this` is command object - let message = mbchc.normalise_message(cmdline.replace(mbchc.RE_ACTIVITY, ''), {trim: true, dot: true, up: true}) - mbchc.send_activity(message) - } catch (x) { mbchc.report(x) } } }, - command_do: function(argline, cmdline, args) { const mbchc = window.MBCHC; try { // `this` is command object - if (args.length < 1) return(mbchc.inform("
Usage: /do VERB [ZONE] [TARGET]
Available verbs:
" + Object.keys(mbchc.MAP_ACTIONS).join(", ") + "
Available zones:
" + Object.keys(mbchc.DO_DATA.zones).join(", "))) - let [verb, zone, target] = args - let zones = mbchc.ensure(`unknown verb "${verb}"`, () => mbchc.DO_DATA.verbs[verb]) - if (1 === Object.keys(zones).length) { - if (!target) target = zone - zone = mbchc.MAP_ZONES[Object.keys(zones)[0]][0] - } - if (!zone) throw "zone missing" - let ag = mbchc.ensure(`unknown zone "${zone}"`, () => mbchc.DO_DATA.zones[zone]) - let types = mbchc.ensure(`zone "${zone}" invalid for "${verb}"`, () => zones[ag]) - let char = window.Player - if (target && ((types.self.length < 1) || (types.others.length > 0))) char = mbchc.target2char(target) - let type = char.IsPlayer() ? "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))) - let actions = mbchc.ensure(`zone "${zone}" invalid for ("${verb}" "${type}")`, () => types[type]) - let action = mbchc.ensure(`invalid action (${verb} ${zone} ${target})`, () => actions.find(name => available.find(a => a.Name === name))) - mbchc.run_activity(char, ag, action) - } catch (x) { mbchc.report(x) } }, - bell: function() { - setTimeout(() => {document.getElementById("InputChat").style.outline = ""}, 100) - document.getElementById("InputChat").style.outline = "solid red" - }, - complete: function(options, space = true) { - if (options.length < 1) return(this.bell()) - if (options.length > 1) { - let width = Math.max(...options.map(o => o.length)) - let pref = null - for (let i = width; i > 0; i -= 1) { let test = options[0].slice(0, i); if (options.every(o => o.startsWith(test))) {pref = test; break} } - if (pref) this.complete([pref], false) - this.complete_hint(options) - } else window.ElementValue("InputChat", document.getElementById("InputChat").value.replace(this.RE_LAST_WORD, `$1${options[0]}${space ? " " : ""}`)) - }, - complete_hint: function(options) { - this.COMP_HINT.innerHTML = options.sort().map(s => `
${s}
`).join("") - this.COMP_HINT.style.display = "flex" - window.ElementSetDataAttribute(this.COMP_HINT.id, "colortheme", (window.Player.ChatSettings.ColorTheme || "Light")) - let rescroll = window.ElementIsScrolledToEnd("TextAreaChatLog") - window.ChatRoomResize(false) - if (rescroll) window.ElementScrollToEnd("TextAreaChatLog") - }, - comp_hint_visible: function() {return(this.COMP_HINT.parentElement && "flex" === this.COMP_HINT.style.display)}, - complete_hint_hide: function(options) {if (!this.comp_hint_visible()) return; this.COMP_HINT.style.display = "none"; window.ChatRoomResize(false)}, - complete_target: function(token, me2 = true, check_perms = false) { - let [locase, found] = [token.toLocaleLowerCase(), new Set()] - for (let c of window.ChatRoomCharacter) { - if ((c.IsPlayer() && !me2) || (check_perms && !window.ServerChatRoomGetAllowItem(window.Player, c))) continue - this.char2targets(c).forEach(s => {if (s.toLocaleLowerCase().startsWith(locase)) found.add(s)}) - } - this.complete(Array.from(found)) - }, - complete_common: function() { - let input = document.getElementById("InputChat").value - return([this, input, this.tokenise(input)]) - }, - complete_mbchc: function(args, locase, cmdline) { const [mbchc, input, tokens] = window.MBCHC.complete_common(); // `this` is command object - if (tokens.length < 1) return - if (tokens.length < 2) return(mbchc.complete([`${mbchc.CommandsKey}${this.Tag}`])) - let subname = tokens[1].toLocaleLowerCase() - if (tokens.length < 3) return(mbchc.complete(Object.keys(mbchc.SUBCOMMANDS_MBCHC).filter(c => c.startsWith(subname)))) // complete subcommand name - let sub = mbchc.SUBCOMMANDS_MBCHC[subname] - if (sub && sub.args) { - let argname = Object.keys(sub.args)[tokens.length - 3] - if ("TARGET" === argname) return(mbchc.complete_target(tokens[tokens.length - 1], false)) - if ("[TARGET]" === argname) return(mbchc.complete_target(tokens[tokens.length - 1]), true) - } - }, - complete_do_target: function(actions, token) { - if (!actions) return - let me2 = (actions.self.length > 0) - if (me2 && actions.others.length < 1) return(this.complete([window.Player.cid.toString()])) // target is always the player - this.complete_target(token, me2, true) - }, - complete_do: function(args, locase, cmdline) { const [mbchc, input, tokens] = window.MBCHC.complete_common(); // `this` is command object - if (tokens.length < 1) return - if (tokens.length < 2) return(mbchc.complete([`${mbchc.CommandsKey}${this.Tag}`])) - // now, we *could* run a filter to exclude impossible activities, but it isn't very useful, and also seems like a lot of CPU to iterate over every action on every zone of every char in the room - let low = tokens[1].toLocaleLowerCase() - if (tokens.length < 3) return(mbchc.complete(Object.keys(mbchc.DO_DATA.verbs).filter(c => c.startsWith(low)))) // complete verb - let ags = mbchc.DO_DATA.verbs[low] - if (!ags) return(mbchc.bell()) - low = tokens[2].toLocaleLowerCase() - if (tokens.length < 4) { // complete zone or target - if (Object.keys(ags).length < 2) return(mbchc.complete_do_target(ags[Object.keys(ags)[0]], tokens[2])) // zone implied, complete target - let zones = Object.entries(mbchc.DO_DATA.zones).filter(([zone, ag]) => zone.startsWith(low) && ags[ag]).map(([zone,ag]) => zone) - return(mbchc.complete(zones)) - } - if (tokens.length < 5) { // complete target where it belongs - if (Object.keys(ags).length < 2) return // zone implied, target already given - return(mbchc.complete_do_target(ags[mbchc.DO_DATA.zones[low]], tokens[3])) - } - mbchc.bell() - }, - complete_fbc_anim: function(args, locase, cmdline) { const [mbchc, input, tokens] = window.MBCHC.complete_common(); // `this` is command object - if (tokens.length < 1) return - if (tokens.length < 2) return(mbchc.complete([`${mbchc.CommandsKey}${this.Tag}`])) - if (tokens.length > 2) return(mbchc.bell()) - let anim = tokens[1].toLocaleLowerCase() - return(mbchc.complete(Object.keys(window.bce_EventExpressions).filter(a => a.toLocaleLowerCase().startsWith(anim)))) - }, - complete_fbc_pose: function(args, locase, cmdline) { const [mbchc, input, tokens] = window.MBCHC.complete_common(); // `this` is command object - if (tokens.length < 1) return - if (tokens.length < 2) return(mbchc.complete([`${mbchc.CommandsKey}${this.Tag}`])) - let pose = tokens[tokens.length - 1].toLocaleLowerCase() - return(mbchc.complete(window.PoseFemale3DCG.map(p => p.Name).filter(p => p.toLocaleLowerCase().startsWith(pose)))) - }, - history: function(down) { - let [text, history] = [window.ElementValue("InputChat"), window.ChatRoomLastMessage] - if (!this.HISTORY_MODE) {history.push(text); this.HISTORY_MODE = true} - let ids = history.map((t,i) => [t,i]).filter(([t,i]) => t.startsWith(history[history.length - 1])).map(([t,i]) => i) - if (!down) ids.reverse() - let found = ids.find(id => (down) ? id > window.ChatRoomLastMessageIndex : id < window.ChatRoomLastMessageIndex) - if (!found) return(this.bell()) - window.ElementValue("InputChat", history[found]) - window.ChatRoomLastMessageIndex = found - }, - loader: function() { - if (this.remove_load_hook) { - this.remove_load_hook() - delete this.remove_load_hook - } - if (this.LOADED) return - // Calculated values - const COMMANDS = [ - { Tag: "mbchc", Description: ": Utility functions (\"/mbchc\" for help)", Action: this.command_mbchc, AutoComplete: this.complete_mbchc }, - { Tag: "activity", Description: "[Message]: Send a custom activity (or \"@@Message\", or \"@Message\" as yourself)", Action: this.command_activity }, - { Tag: "do", Description: ": Do an activity, as if clicked on its button (\"/do\" for help)", Action: this.command_do, AutoComplete: this.complete_do }, - ] - 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 ` - this.COMP_HINT = document.createElement("div") - this.COMP_HINT.id = "mbchcCompHint" - let css = document.createElement("style") - css.type = "text/css" - css.textContent = ` - #TextAreaChatLog .mbchc, #TextAreaChatLog .mbchc { - background-color: ${this.RGB_POLLY}; - } - #TextAreaChatLog[data-colortheme="dark"] .mbchc, #TextAreaChatLog[data-colortheme="dark2"] .mbchc { - background-color: ${this.RGB_MUTE}; - } - #${this.COMP_HINT.id} { - flex-flow: column wrap; - overflow: auto; - display: none; - background-color: ${this.RGB_POLLY}; - color: black; - } - #${this.COMP_HINT.id}[data-colortheme="dark"], #${this.COMP_HINT.id}[data-colortheme="dark2"] { - background-color: ${this.RGB_MUTE}; - color: white; - } - #${this.COMP_HINT.id} div { - margin: 0 0.5ex; - } - ` - document.head.appendChild(css) - // Actions - this.calculate_maps() - this.patch_handheld() - window.Player.MBCHC = {VERSION: this.VERSION} - window.CommandCombine(COMMANDS) - this.LOADED = true - this.log("info", `loaded version ${this.VERSION}`) - if (window.GameVersion !== this.TARGET_VERSION) console.warn(`Game version doesn't match the target ("${this.TARGET_VERSION}"), beware of incompatibilities`) // TODO: check betas & cheat - } - } // MBCHC - - // Hooks - window.MBCHC.sdk = window.bcModSdk.registerMod("MBCHC", window.MBCHC.VERSION) - window.MBCHC.sdk.hookFunction("CharacterOnlineRefresh", 0, (nextargs, next) => { - let result = next(nextargs) - window.MBCHC.update_char(nextargs[0]) - return(result) - }) - window.MBCHC.sdk.hookFunction("ChatRoomReceiveSuitcaseMoney", 0, (nextargs, next) => { - let result = next(nextargs) - if (window.MBCHC.AUTOHACK_ENABLED && window.MBCHC.LAST_HACKED) { - window.CurrentCharacter = window.MBCHC.cid2char(window.MBCHC.LAST_HACKED) - window.MBCHC.LAST_HACKED = null - window.ChatRoomTryToTakeSuitcase() - } - return(result) - }) - window.MBCHC.sdk.hookFunction("ChatRoomSendChat", 0, (nextargs, next) => { - let input = window.ElementValue("InputChat") - if (!input.startsWith("@@@") && 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.replace_me) - window.ElementValue("InputChat", input) - } - let result = next(nextargs) - let history = window.ChatRoomLastMessage - if ((history.length > 1) && (history[history.length - 1] === history[history.length - 2])) {history.pop(); window.ChatRoomLastMessageIndex -= 1} - return(result) - }) - window.MBCHC.sdk.hookFunction("ChatRoomDrawCharacterOverlay", 0, (nextargs, next) => { - let [C, CharX, CharY, Zoom, Pos] = nextargs - if ((window.ChatRoomHideIconState < 1) && C.MBCHC) { - let colour = (C.MBCHC.VERSION === window.Player.MBCHC.VERSION) ? window.MBCHC.RGB_POLLY : window.MBCHC.RGB_MUTE - window.DrawRect(CharX + 175 * Zoom, CharY, 50 * Zoom, 50 * Zoom, colour) - } - if ((window.ChatRoomHideIconState < 1) && C.MBCHC_LOCAL && C.MBCHC_LOCAL.TZ) { - let hours = new Date(window.CommonTime() + window.MBCHC.UTC_OFFSET + C.MBCHC_LOCAL.TZ * 60 * 60 * 1000).getHours() - let text = (hours < 10) ? "0" + hours.toString() : hours.toString() - window.DrawTextFit(text, CharX + 200 * Zoom, CharY + 25 * Zoom, 46 * Zoom, "white", "black") - } - return(next(nextargs)) - }) - window.MBCHC.sdk.hookFunction("ElementValue", 0, (nextargs, next) => { // TODO: layer priority will be locked too if it's the same as difficulty - let [ID, Value] = nextargs - let result = next(nextargs) - if (("bce_LayerPriority" === ID) && window.CurrentCharacter?.FocusGroup && (window.InventoryGet(window.CurrentCharacter, window.CurrentCharacter.FocusGroup.Name)?.Difficulty.toString() === Value) && window.InventoryLocked(window.CurrentCharacter, window.CurrentCharacter.FocusGroup.Name, true)) window.ElementSetAttribute(ID, "disabled", true) - return(result) - }) - window.MBCHC.sdk.hookFunction("ChatRoomCreateElement", 0, (nextargs, next) => { - let result = next(nextargs) - if (!window.MBCHC.COMP_HINT.parentElement) document.body.appendChild(window.MBCHC.COMP_HINT) - return(result) - }) - window.MBCHC.sdk.hookFunction("ChatRoomClearAllElements", 0, (nextargs, next) => { - window.MBCHC.complete_hint_hide() - window.MBCHC.COMP_HINT.remove() - return(next(nextargs)) - }) - window.MBCHC.sdk.hookFunction("ChatRoomResize", 0, (nextargs, next) => { - let result = next(nextargs) - if (window.CharacterGetCurrent() == null && window.CurrentScreen == "ChatRoom" && document.getElementById("InputChat") && document.getElementById("TextAreaChatLog") && window.MBCHC.comp_hint_visible()) { // upstream - let fontsize = ChatRoomFontSize /* eslint-disable-line no-undef */ // window.ChatRoomFontSize is undefined - window.ElementPositionFix("TextAreaChatLog", fontsize, 1005, 66, 988, 630) - window.ElementPositionFix(window.MBCHC.COMP_HINT.id, fontsize, 1005, 701, 988, 200) - window.MBCHC.COMP_HINT.style.display = "flex" - } - return(result) - }) - window.MBCHC.sdk.hookFunction("DocumentKeyDown", 0, (nextargs, next) => { - let [event] = nextargs - if ("InputChat" === document.activeElement.id || "bce-message-input" === document.activeElement.id) return(next(nextargs)) - if ("inline" === document.getElementById("InputChat")?.style.display && [event.altKey, event.ctrlKey, event.metaKey].every(i => !i)) window.ElementFocus("InputChat") // alt, ctrl and meta should all be false - // TODO: this is not ideal, but it will have to do for now - return(next(nextargs)) - }) - window.MBCHC.sdk.hookFunction("ChatRoomKeyDown", 0, (nextargs, next) => { - let [event] = nextargs - window.MBCHC.complete_hint_hide() - if ((window.KeyPress == 33) || (window.KeyPress == 34)) { // better history - event.preventDefault() - return(window.MBCHC.history(window.KeyPress - 33)) - } - if (window.MBCHC.HISTORY_MODE) { - window.ChatRoomLastMessage.pop() - window.MBCHC.HISTORY_MODE = false - } - return(next(nextargs)) - }) - window.MBCHC.sdk.hookFunction("ChatRoomClick", 0, (nextargs, next) => { - window.MBCHC.complete_hint_hide() - return(next(nextargs)) - }) - window.MBCHC.remove_fbc_hook = window.MBCHC.sdk.hookFunction("MainRun", 0, (nextargs, next) => { - if (window.bce_ActivityTriggers) window.MBCHC.patch_fbc() - return(next(nextargs)) - }) - - // Chat room handlers - window.ChatRoomRegisterMessageHandler({ Priority: -220, Description: "MBCHC preprocessor", - Callback: (data, sender, msg, metadata) => { - data.MBCHC_ID = window.MBCHC.NEXT_MESSAGE - window.MBCHC.NEXT_MESSAGE += 1 - if (window.MBCHC.LOG_MESSAGES) console.debug({data, sender, msg, metadata}) - } - }) - window.ChatRoomRegisterMessageHandler({ Priority: -219, Description: "MBCHC room enter hook", - Callback: (data, sender, msg, metadata) => { if (("Action" === data.Type) && ("ServerEnter" === data.Content) && (data.Sender === window.Player.cid)) window.MBCHC.player_enters_room() } - }) - window.ChatRoomRegisterMessageHandler({ Priority: -219, Description: "MBCHC specific consumer", - Callback: (data, sender, msg, metadata) => { if (("Hidden" === data.Type) && ("MBCHC" === data.Content)) return window.MBCHC.receive(data) } - }) - window.ChatRoomRegisterMessageHandler({ Priority: -219, Description: "MBCHC autohack lookup", - Callback: (data, sender, msg, metadata) => { if (("Hidden" === data.Type) && ("ReceiveSuitcaseMoney" === data.Content)) window.MBCHC.LAST_HACKED = data.Sender } - }) - - // MAIN SCREEN TURN ON - if (window.MBCHC.need_load_hook(window.CurrentModule, window.CurrentScreen)) { - window.MBCHC.remove_load_hook = window.MBCHC.sdk.hookFunction("AsylumGGTSSAddItems", 0, (nextargs, next) => {window.MBCHC.loader(); return(next(nextargs))}) - } else { - window.MBCHC.loader() - if (("Online" === window.CurrentModule) && ("ChatRoom" === window.CurrentScreen)) { - window.ChatRoomCharacter.forEach(c => window.MBCHC.update_char(c)) - window.MBCHC.player_enters_room() - } +;(function() { +"use strict"; +if (!window.AsylumGGTSSAddItems) throw new Error("AsylumGGTSSAddItems() not found, aborting MBCHC loading") +if (window.MBCHC) throw new Error("MBCHC found, aborting loading") +window.MBCHC = { + LOADED: false, + VERSION: "trunk", + TARGET_VERSION: "R85", + NEXT_MESSAGE: 1, + LOG_MESSAGES: false, + RETHROW: false, + AUTOHACK_ENABLED: false, + LAST_HACKED: null, + HISTORY_MODE: false, + DO_DATA: {}, + RGB_MUTE: "#6c2132", + RGB_POLLY: "#81b1e7", + UTC_OFFSET: new Date().getTimezoneOffset() * 60 * 1000, + 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","LeftHand","Mouth","Nipples","Pussy","RightHand"], + 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","ItemTorso2", + "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: "GagKiss|Kiss|GaggedKiss"}, + "Boots,Hands": {self: "PoliteKiss", others: "PoliteKiss|GaggedKiss"}, + "Arms,Breast,Nipples": {self: "Kiss", others: "Kiss|GaggedKiss"}, + "Butt,Ears,Feet,Head,Legs,Neck,Nose,Pelvis,Torso,Vulva,VulvaPiercings": {others: "Kiss|GaggedKiss"} + }, + smooch: {"Hands,Boots": {all: "Kiss"}}, + "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"}}, //peg? + pound: {"Mouth,Vulva,Butt": {others: "PenetrateFast"}}, + tongue: {"Vulva,VulvaPiercings": {others: "MasturbateTongue"}}, + finger: {"Breast,Butt,Vulva,VulvaPiercings": {all: "MasturbateHand"}}, + }, + MAP_ZONES: { + Boots: "foot,feet,boot,boots,shoe,shoes,toes,toenails,sole,soles,heel,heels", + Feet: "leg,legs,ankle,ankles", + Legs: "hip,hips,thigh,thighs", + Vulva: "vulva,pussy", + VulvaPiercings: "clit,clitoris", + Butt: "butt,ass", + Pelvis: "tummy,pelvis", + Torso: "body,torso,back,ribs", + Breast: "breast,breasts,boob,boobs,booby,boobie,boobies,tit,tits,titty,tittie,titties", + Nipples: "nip,nips,nipple,nipples", + Hands: "hand,hands,fingers,fingernails,nails", + Arms: "arm,arms,elbow,elbows", + Neck: "neck", + Mouth: "mouth,lip,lips,teeth,tongue,gag,cheek,cheeks", + Nose: "nose,nostrils", + Ears: "ear,ears,earlobe,earlobes", + Head: "head,face,hair,eyes,forehead", + }, + FBC_TESTER_PATCHES: [ + [/^\^('s)?( )?/g, "^SourceCharacter$1\\s+"], + [/([^\\])\$/g, "$1\\.?$$"], + ], + SUBCOMMANDS_MBCHC: { + "versions": {desc: "show the mod versions across the room", cb: mbchc => mbchc.inform(mbchc.gather_versions().map(c => `
${c.name} (${c.cid}): ${c.version}
`).join(""))}, + "autohack": {desc: "toggle the autohack feature", cb: mbchc => mbchc.inform(`Autohack is now ${(mbchc.AUTOHACK_ENABLED = !mbchc.AUTOHACK_ENABLED) ? "enabled" : "disabled"}`)}, + "disappear": {desc: "become invisible (requires anal hook -> hair)", cb: mbchc => mbchc.disappear()}, + "donate": {desc: "Buy data and send it to recipient", args: {TARGET: {}}, cb: (mbchc, args) => mbchc.donate_data(args[0])}, + "title": {desc: "set a custom title (WIP)", args: {TITLE: {}}, cb: (mbchc, args) => mbchc.title(args[0])}, + "tz": {desc: "set target's UTC offset", args: {OFFSET: {}, "[TARGET]": {}}, cb: (mbchc, args) => mbchc.set_timezone(args)}, + "purge!": {desc: "delete MBCHC online saved data", cb: mbchc => window.Player.OnlineSettings.MBCHC && mbchc.save_settings(s => delete window.Player.OnlineSettings.MBCHC)}, + }, + fiasco: new Proxy({}, {has(_, error) {throw new Error(error)}}), + settings: () => window.Player.OnlineSettings.MBCHC || {}, + save_settings: (cb = null) => { + cb?.(window.Player.OnlineSettings.MBCHC ||= {}) + window.ServerAccountUpdate.QueueData({OnlineSettings: window.Player.OnlineSettings}) + }, + log: (level, msg) => console[level]("MBCHC: " + String(msg)), + empty: text => !text || String(text).trim().length < 1, + normalise_message: (text, options = {}) => { + let result = text + if (options.trim) result = result.trim() + if (options.low) result = result.toLocaleLowerCase() + if (options.up) result = result.at(0).toLocaleUpperCase() + result.slice(1) + if (options.dot && result.match(/[\w]$/)) result += "." + return result + }, + tokenise: text => text.replaceAll(/\s{2,}/g, " ").split(" "), + inform: (html, timeout = 60000) => window.ChatRoomSendLocal(`
${html}
`, timeout), + report: x => { + this.inform(x.toString()) + if (this.RETHROW) throw x + }, + in: (x, floor, ceiling) => x >= floor && x <= ceiling, + cid2char: cid => cid === window.Player.cid ? window.Player : window.ChatRoomCharacter.find(c => c.cid === cid) || `character ${cid} not found in the room` in this.fiasco, + target2char: target => { // target should be lowcase + if (this.empty(target)) return window.Player + const position = pos => window.ChatRoomCharacter.at(pos) || `invalid position ${pos}` in this.fiasco + const input = target, int = Number.parseInt(target) + target = String(target) + let found = [] + if (target.startsWith("=")) return this.cid2char(target.slice(1)) + if (target.startsWith("<") || target.startsWith(">")) { + let pos = (window.ChatRoomCharacter.findIndex(char => char.IsPlayer()) + 1 || "can't find my position" in this.fiasco) - 1 // findIndex returns -1 if not found + pos += target.match(/^<+$/) ? -target.length : target.match(/^>+$/) ? target.length : `failed to parse target "${target}"` in this.fiasco + return position(pos % window.ChatRoomCharacter.length) } + if (!isNaN(int) && int.toString() === target) { // we got a number + if (this.in(int, 0, 9)) return position(int) + if (this.in(int, 11, 15)) return position(int - 11) + if (this.in(int, 21, 25)) return position(int - 16) + found.push(...window.ChatRoomCharacter.filter(c => c.cid.toString().includes(target))) + } + if (target.startsWith("@")) target = target.slice(1) + found.push(...window.ChatRoomCharacter.filter(c => c.Name.toLocaleLowerCase().includes(target))) + found.push(...window.ChatRoomCharacter.filter(c => c.Nickname?.toLocaleLowerCase().includes(target))) + let map = {} + found.forEach(c => {if (!map[c.cid]) map[c.cid] = c} ) + found = Object.values(map) + found.length < 1 && `target "${input}": no match` in this.fiasco + found.length > 1 && `target "${input}": multiple matches (${found.map(c => `${c.cid}|${c.Name}|${c.Nickname || c.Name}`).join(",")})` in this.fiasco + return found[0] + }, + char2targets: char => { + const result = new Set(), cid = char.cid.toString() + result.add(cid).add(`=${cid}`) + this.tokenise(char.Name).forEach(t => {result.add(t); result.add(`@${t}`)}) + if (char.Nickname) this.tokenise(char.Nickname).forEach(t => {result.add(t); result.add(`@${t}`)}) + return result + }, + donate_data: target => { + let char = this.target2char(target) + char.IsPlayer() && "target must not be you" in this.fiasco + char.IsRestrained() || "target must be bound" in this.fiasco + const cost = Math.round((Math.random() * 10 + 15)) + window.Player.Money < cost && "not enough money" in this.fiasco + window.CharacterChangeMoney(window.Player, -cost) + window.ServerSend("ChatRoomChat", {Content: "ReceiveSuitcaseMoney", Type: "Hidden", Target: char.cid}) + window.ChatRoomMessage({Sender: window.Player.cid, Type: "Action", Content: `You've bought data for $${cost} and sent it to ${char.dn}.`, Dictionary: [{Tag: "MISSING PLAYER DIALOG: ", Text: ""}]}) + }, + run_activity: (char, ag, action) => { try { + window.ActivityAllowed() || "activities disabled in this room" in this.fiasco + window.ServerChatRoomGetAllowItem(window.Player, char) || "no permissions" in this.fiasco + char.FocusGroup = window.AssetGroupGet(char.AssetFamily, ag) || "invalid AssetGroup" in this.fiasco + const activity = window.ActivityAllowedForGroup(char, char.FocusGroup.Name, true).find(a => a.Name === action) || "invalid activity" in this.fiasco + if (activity.Name.endsWith("Item")) { + const item = window.Player.Inventory.find(i => i.Asset?.Name === "SpankingToys" && i.Asset.Group?.Name === char.FocusGroup.Name && window.AssetSpankingToys.DynamicActivity(char) === activity.Name) || "no toy found" in this.fiasco + window.DialogPublishAction(char, item) + } else window.ActivityRun(char, activity) + } finally { + char.FocusGroup = null + } }, + send_activity: msg => { + const RE_ACT_CIDS = /^<(\d+)?:(\d+)?>/ + const cid2dict = (type, cid) => ({Tag: `${type}Character`, MemberNumber: cid, Text: this.cid2char(cid).dn}) + const dict = [{Tag: "MISSING PLAYER DIALOG: ", Text: ""}] + const cids = msg.match(RE_ACT_CIDS) + if (cids) { + msg = msg.replace(RE_ACT_CIDS, "") + cids.length > 0 && dict.push(cid2dict("Source", cids[1])) + cids.length > 1 && dict.push(cid2dict("Target", cids[2]), cid2dict("Destination", cids[2])) + } + window.ServerSend("ChatRoomChat", {Type: "Action", Content: msg, Dictionary: dict}) + }, + receive: data => { + let char = this.cid2char(data.Sender) + if (char.IsPlayer()) return // this is our own message, sent back to us + const payload = data.Dictionary[0] || "Empty MBCHC message" in this.fiasco + switch (payload.type) { + case "greetings": case "hello": + char.MBCHC = payload.value + if ("greetings" === payload.type) this.hello(char) + break + default: // if we don't know the type it may be from a newer version + } + }, + hello: (char = null) => { + let payload = {type: "greetings", value: window.Player.MBCHC} + if (char) payload.type = "hello" + let message = {Content: "MBCHC", Type: "Hidden", Dictionary: [payload]} + if (char) message.Target = char.cid + window.ServerSend("ChatRoomChat", message) + }, + disappear: () => { + const item = window.InventoryGet(window.Player, "ItemButt") + item?.Asset?.Name || "butt seems empty" in this.fiasco + "AnalHook" === item.Asset.Name || "butt seems occupied by something other than the anal hook" in this.fiasco + "Hair" === item.Property?.Type || "anal hook seems not tied to hair" in this.fiasco + item.Property = {Type: "Hair", Hide: this.HIDE_ALL} + window.CharacterRefresh(window.Player, true, true) + }, + title: title => { // WIP + this.empty(title) && "empty title" in this.fiasco + title = this.normalise_message(title, {trim: true, up: true, low: true}) + title.length > 16 && "title too long" in this.fiasco + title.match(/^[a-zA-Z]+$/) || "invalid title" in this.fiasco + window.TitleSet(title) + //window.TitleList.push({Name: title, Requirement: () => true}) // check for existing first + }, + patch_fbc: () => { + void this.remove_fbc_hook() || delete this.remove_fbc_hook + if (!window.bce_ActivityTriggers) return + const copy_fbc_trigger = trigger => ({Type: "Action", Event: trigger.Event, Matchers: trigger.Matchers.map(m => ({Tester: new RegExp(this.FBC_TESTER_PATCHES.reduce((ax,[f,r]) => ax.replaceAll(f,r), m.Tester.source), "u")}))}) + window.bce_ActivityTriggers.push(...window.bce_ActivityTriggers.filter(t => "Emote" === t.Type).map(t => copy_fbc_trigger(t))) + ;["anim", "pose"].forEach(tag => (window.Commands.filter(c => tag === c.Tag).forEach(c => (c.AutoComplete = this[`complete_fbc_${tag}`])))) + }, + gather_versions: () => window.ChatRoomCharacter.filter(c => c.MBCHC).map(c => ({name: c.dn, cid: c.cid, version: c.MBCHC.VERSION})), + find_timezone: char => { + let timezones = this.settings.timezones + if (timezones && timezones[char.cid]) return(timezones[char.cid]) + let match = (char.Description) ? char.Description.match(/(?:GMT|UTC)([+-]\d\d?)/i) : null + if (match) return(Number.parseInt(match[1])) + return(null) + }, + player_enters_room: () => { // or if the mod is loaded while player is in the room + this.hello() + }, + set_timezone: args => { + const tz = Number.parseInt(args[0]) + isNaN(tz) && `invalid offset "${args[0]}"` in this.fiasco + this.in(tz, -12, +12) || "offset should be from -12 to +12" in this.fiasco + const char = this.target2char(args[1]) + char.MBCHC_LOCAL.TZ = tz + this.save_settings(s => (s.timezones ??= {})[char.cid] = tz) /* eslint-disable-line no-return-assign */ + }, + update_char: char => { + char.cid = char.MemberNumber // Club ID (shorter) + char.dn = window.CharacterNickname(char) // DisplayName (shortcut) + char.MBCHC_LOCAL ||= {} + char.MBCHC_LOCAL.TZ ||= this.find_timezone(char) + }, + command_mbchc: (argline, cmdline, args) => { const mbchc = window.MBCHC; try { // `this` is command object + if (args.length < 1) return(mbchc.inform(Object.entries(mbchc.SUBCOMMANDS_MBCHC).map(([cmd, sub]) => `
/mbchc ${cmd} ${sub.args ? Object.keys(sub.args).join(" ") : ""}: ${sub.desc}
`).join(""))) + const cmd = String(args.shift()) + (mbchc.SUBCOMMANDS_MBCHC[cmd] || `unknown subcommand "${cmd}"` in mbchc.fiasco).cb.call(mbchc, mbchc, args, argline, cmdline) + } catch (x) { mbchc.report(x) } }, + command_activity: (argline, cmdline, args) => { const mbchc = window.MBCHC; if (!mbchc.empty(argline)) { try { // `this` is command object + let message = mbchc.normalise_message(cmdline.replace(mbchc.RE_ACTIVITY, ''), {trim: true, dot: true, up: true}) + mbchc.send_activity(message) + } catch (x) { mbchc.report(x) } } }, + command_do: (argline, cmdline, args) => { const mbchc = window.MBCHC; try { // `this` is command object + if (args.length < 1) return(mbchc.inform("
Usage: /do VERB [ZONE] [TARGET]
Available verbs:
" + Object.keys(mbchc.MAP_ACTIONS).join(", ") + "
Available zones:
" + Object.keys(mbchc.DO_DATA.zones).join(", "))) + let [verb, zone, target] = args + const zones = mbchc.DO_DATA.verbs[verb] || `unknown verb "${verb}"` in mbchc.fiasco + if (1 === Object.keys(zones).length) { + if (!target) target = zone + zone = mbchc.MAP_ZONES[Object.keys(zones)[0]][0] + } + zone || "zone missing" in this.fiasco + const ag = mbchc.DO_DATA.zones[zone] || `unknown zone "${zone}"` in mbchc.fiasco + const types = zones[ag] || `zone "${zone}" invalid for "${verb}"` in mbchc.fiasco + let char = window.Player + if (target && ((types.self.length < 1) || (types.others.length > 0))) char = mbchc.target2char(target) + let type = char.IsPlayer() ? "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))) + const actions = types[type] || `zone "${zone}" invalid for ("${verb}" "${type}")` in mbchc.fiasco + const action = actions.find(name => available.find(a => a.Name === name)) || `invalid action (${verb} ${zone} ${target})` in mbchc.fiasco + mbchc.run_activity(char, ag, action) + } catch (x) { mbchc.report(x) } }, + bell: () => { + setTimeout(() => document.getElementById("InputChat").style.outline = "", 100) /* eslint-disable-line no-return-assign */ + document.getElementById("InputChat").style.outline = "solid red" + }, + complete: (options, space = true) => { + if (options.length < 1) return(this.bell()) + if (options.length > 1) { + let width = Math.max(...options.map(o => o.length)) + let pref = null + for (let i = width; i > 0; i -= 1) { let test = options[0].slice(0, i); if (options.every(o => o.startsWith(test))) {pref = test; break} } + if (pref) this.complete([pref], false) + this.complete_hint(options) + } else window.ElementValue("InputChat", document.getElementById("InputChat").value.replace(/(^|\s)([^\s]*)$/, `$1${options[0]}${space ? " " : ""}`)) + }, + complete_hint: options => { + this.COMP_HINT.innerHTML = options.sort().map(s => `
${s}
`).join("") + this.COMP_HINT.style.display = "flex" + window.ElementSetDataAttribute(this.COMP_HINT.id, "colortheme", (window.Player.ChatSettings.ColorTheme || "Light")) + let rescroll = window.ElementIsScrolledToEnd("TextAreaChatLog") + window.ChatRoomResize(false) + if (rescroll) window.ElementScrollToEnd("TextAreaChatLog") + }, + comp_hint_visible: () => {return(this.COMP_HINT.parentElement && "flex" === this.COMP_HINT.style.display)}, + complete_hint_hide: options => {if (!this.comp_hint_visible()) return; this.COMP_HINT.style.display = "none"; window.ChatRoomResize(false)}, + complete_target: (token, me2 = true, check_perms = false) => { + let [locase, found] = [token.toLocaleLowerCase(), new Set()] + for (let c of window.ChatRoomCharacter) { + if ((c.IsPlayer() && !me2) || (check_perms && !window.ServerChatRoomGetAllowItem(window.Player, c))) continue + this.char2targets(c).forEach(s => {if (s.toLocaleLowerCase().startsWith(locase)) found.add(s)}) + } + this.complete(Array.from(found)) + }, + complete_common: () => { + const input = document.getElementById("InputChat").value + return [this, input, this.tokenise(input)] + }, + complete_mbchc: (args, locase, cmdline) => { const [mbchc, input, tokens] = window.MBCHC.complete_common(); // `this` is command object + if (tokens.length < 1) return + if (tokens.length < 2) return(mbchc.complete([`${mbchc.CommandsKey}${this.Tag}`])) + let subname = tokens[1].toLocaleLowerCase() + if (tokens.length < 3) return(mbchc.complete(Object.keys(mbchc.SUBCOMMANDS_MBCHC).filter(c => c.startsWith(subname)))) // complete subcommand name + let sub = mbchc.SUBCOMMANDS_MBCHC[subname] + if (sub && sub.args) { + let argname = Object.keys(sub.args)[tokens.length - 3] + if ("TARGET" === argname) return(mbchc.complete_target(tokens[tokens.length - 1], false)) + if ("[TARGET]" === argname) return(mbchc.complete_target(tokens[tokens.length - 1]), true) + } + }, + complete_do_target: (actions, token) => { + if (!actions) return + let me2 = (actions.self.length > 0) + if (me2 && actions.others.length < 1) return(this.complete([window.Player.cid.toString()])) // target is always the player + this.complete_target(token, me2, true) + }, + complete_do: (args, locase, cmdline) => { const [mbchc, input, tokens] = window.MBCHC.complete_common(); // `this` is command object + if (tokens.length < 1) return + if (tokens.length < 2) return(mbchc.complete([`${mbchc.CommandsKey}${this.Tag}`])) + // now, we *could* run a filter to exclude impossible activities, but it isn't very useful, and also seems like a lot of CPU to iterate over every action on every zone of every char in the room + let low = tokens[1].toLocaleLowerCase() + if (tokens.length < 3) return(mbchc.complete(Object.keys(mbchc.DO_DATA.verbs).filter(c => c.startsWith(low)))) // complete verb + let ags = mbchc.DO_DATA.verbs[low] + if (!ags) return(mbchc.bell()) + low = tokens[2].toLocaleLowerCase() + if (tokens.length < 4) { // complete zone or target + if (Object.keys(ags).length < 2) return(mbchc.complete_do_target(ags[Object.keys(ags)[0]], tokens[2])) // zone implied, complete target + let zones = Object.entries(mbchc.DO_DATA.zones).filter(([zone, ag]) => zone.startsWith(low) && ags[ag]).map(([zone,ag]) => zone) + return(mbchc.complete(zones)) + } + if (tokens.length < 5) { // complete target where it belongs + if (Object.keys(ags).length < 2) return // zone implied, target already given + return(mbchc.complete_do_target(ags[mbchc.DO_DATA.zones[low]], tokens[3])) + } + mbchc.bell() + }, + complete_fbc_anim: (args, locase, cmdline) => { const [mbchc, input, tokens] = window.MBCHC.complete_common(); // `this` is command object + if (tokens.length < 1) return + if (tokens.length < 2) return(mbchc.complete([`${mbchc.CommandsKey}${this.Tag}`])) + if (tokens.length > 2) return(mbchc.bell()) + let anim = tokens[1].toLocaleLowerCase() + return(mbchc.complete(Object.keys(window.bce_EventExpressions).filter(a => a.toLocaleLowerCase().startsWith(anim)))) + }, + complete_fbc_pose: (args, locase, cmdline) => { const [mbchc, input, tokens] = window.MBCHC.complete_common(); // `this` is command object + if (tokens.length < 1) return + if (tokens.length < 2) return(mbchc.complete([`${mbchc.CommandsKey}${this.Tag}`])) + let pose = tokens[tokens.length - 1].toLocaleLowerCase() + return(mbchc.complete(window.PoseFemale3DCG.map(p => p.Name).filter(p => p.toLocaleLowerCase().startsWith(pose)))) + }, + history: down => { + let [text, history] = [window.ElementValue("InputChat"), window.ChatRoomLastMessage] + if (!this.HISTORY_MODE) {history.push(text); this.HISTORY_MODE = true} + let ids = history.map((t,i) => [t,i]).filter(([t,i]) => t.startsWith(history[history.length - 1])).map(([t,i]) => i) + if (!down) ids.reverse() + let found = ids.find(id => (down) ? id > window.ChatRoomLastMessageIndex : id < window.ChatRoomLastMessageIndex) + if (!found) return(this.bell()) + window.ElementValue("InputChat", history[found]) + window.ChatRoomLastMessageIndex = found + }, + create_element: (tag, properties = {}) => Object.assign(document.createElement(tag), properties), + loader: () => { + void this.remove_load_hook?.() || delete this.remove_load_hook + if (this.LOADED) return + // 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 ` + this.COMP_HINT = this.create_element("div", {id: "mbchcCompHint"}) + document.head.appendChild(this.create_element("css", {type: "text/css", textContent: ` + #TextAreaChatLog .mbchc {background-color: ${this.RGB_POLLY}} + #TextAreaChatLog[data-colortheme="dark"] .mbchc, #TextAreaChatLog[data-colortheme="dark2"] .mbchc {background-color: ${this.RGB_MUTE}} + #${this.COMP_HINT.id} {flex-flow: column wrap; overflow: auto; display: none; background-color: ${this.RGB_POLLY}; color: black} + #${this.COMP_HINT.id}[data-colortheme="dark"], #${this.COMP_HINT.id}[data-colortheme="dark2"] {background-color: ${this.RGB_MUTE}; color: white} + #${this.COMP_HINT.id} div {margin: 0 0.5ex} + `})) + const map = (obj, cb) => new Map(Object.entries(obj).flatMap(cb)) + const split = (str, val, cb = null) => str.split(/[|,]/).map(x => [cb?.(x) ?? x, val]) + const extract = (actions, k) => [[k, new Set((actions[k]?.split("|") ?? []).concat(actions.all?.split("|") ?? []))]] + this.DO_DATA.verbs = map(this.MAP_ACTIONS, ([verbs,data]) => split(verbs, map(data, ([zones,actions]) => split(zones, map(["self","others"], ([_,k]) => extract(actions, k)), zone => `Item${zone}`)))) + this.DO_DATA.zones = map(this.MAP_ZONES, ([zone,labels]) => split(labels, `Item${zone}`)) + // Actions + this.HAND_PENETRATORS.map(name => InventoryItemHandsSpankingToysOptions.find(o => o.Name === name)).forEach(o => o?.Property && !(o.Property.AllowActivity ||= []).includes("PenetrateItem") && o.Property.AllowActivity.push("PenetrateItem")) /* eslint-disable-line no-undef */ // window.InventoryItemHandsSpankingToysOptions is undefined + window.Player.MBCHC = {VERSION: this.VERSION} + window.CommandCombine([ + {Tag: "mbchc", Description: ": Utility functions (\"/mbchc\" for help)", Action: this.command_mbchc, AutoComplete: this.complete_mbchc}, + {Tag: "activity", Description: "[Message]: Send a custom activity (or \"@@Message\", or \"@Message\" as yourself)", Action: this.command_activity}, + {Tag: "do", Description: ": Do an activity, as if clicked on its button (\"/do\" for help)", Action: this.command_do, AutoComplete: this.complete_do}, + ]) + this.LOADED = !void this.log("info", `loaded version ${this.VERSION}`) + window.GameVersion === this.TARGET_VERSION || this.log("warn", `Game version doesn't match the target ("${this.TARGET_VERSION}"), beware of incompatibilities`) // TODO: check betas & cheat + } +} // MBCHC + +// Hooks +window.MBCHC.sdk = window.bcModSdk.registerMod("MBCHC", window.MBCHC.VERSION) +window.MBCHC.sdk.hookFunction("CharacterOnlineRefresh", 0, (nextargs, next) => { + const result = next(nextargs) + window.MBCHC.update_char(nextargs[0]) + return result +}) +window.MBCHC.sdk.hookFunction("ChatRoomReceiveSuitcaseMoney", 0, (nextargs, next) => { + let result = next(nextargs) + if (window.MBCHC.AUTOHACK_ENABLED && window.MBCHC.LAST_HACKED) { + window.CurrentCharacter = window.MBCHC.cid2char(window.MBCHC.LAST_HACKED) + window.MBCHC.LAST_HACKED = null + window.ChatRoomTryToTakeSuitcase() + } + return(result) +}) +window.MBCHC.sdk.hookFunction("ChatRoomSendChat", 0, (nextargs, next) => { + const input = window.ElementValue("InputChat") + input.startsWith("@") && !input.startsWith("@@@") && window.ElementValue("InputChat", input.replace(/^@@/, window.MBCHC.PREF_ACTIVITY).replace(/^@(['\s])?/, (_,suf) => `${window.MBCHC.PREF_ACTIVITY}<${window.Player.cid}:>SourceCharacter${suf || " "}`)) + const result = next(nextargs) + const history = window.ChatRoomLastMessage + history.length > 1 && history.at(-1) === history.at(-2) && !void history.pop() && window.ChatRoomLastMessageIndex-- + return result +}) +window.MBCHC.sdk.hookFunction("ChatRoomDrawCharacterOverlay", 0, (nextargs, next) => { + let [C, CharX, CharY, Zoom, Pos] = nextargs + if (window.ChatRoomHideIconState < 1 && C.MBCHC) { + let colour = (C.MBCHC.VERSION === window.Player.MBCHC.VERSION) ? window.MBCHC.RGB_POLLY : window.MBCHC.RGB_MUTE + window.DrawRect(CharX + 175 * Zoom, CharY, 50 * Zoom, 50 * Zoom, colour) + } + if (window.ChatRoomHideIconState < 1 && C.MBCHC_LOCAL && C.MBCHC_LOCAL.TZ) { + let hours = new Date(window.CommonTime() + window.MBCHC.UTC_OFFSET + C.MBCHC_LOCAL.TZ * 60 * 60 * 1000).getHours() + let text = (hours < 10) ? "0" + hours.toString() : hours.toString() + window.DrawTextFit(text, CharX + 200 * Zoom, CharY + 25 * Zoom, 46 * Zoom, "white", "black") + } + return(next(nextargs)) +}) +window.MBCHC.sdk.hookFunction("ElementValue", 0, (nextargs, next) => { // FIXME: layer priority will be locked too if it's the same as difficulty + const [ID, Value] = nextargs, result = next(nextargs) + "bce_LayerPriority" === ID && window.CurrentCharacter?.FocusGroup && window.InventoryGet(window.CurrentCharacter, window.CurrentCharacter.FocusGroup.Name)?.Difficulty.toString() === Value && window.InventoryLocked(window.CurrentCharacter, window.CurrentCharacter.FocusGroup.Name, true) && window.ElementSetAttribute(ID, "disabled", true) + return result +}) +window.MBCHC.sdk.hookFunction("ChatRoomCreateElement", 0, (nextargs, next) => { + const result = next(nextargs) + window.MBCHC.COMP_HINT.parentElement || document.body.appendChild(window.MBCHC.COMP_HINT) + return result +}) +window.MBCHC.sdk.hookFunction("ChatRoomClearAllElements", 0, (nextargs, next) => void window.MBCHC.complete_hint_hide() || void window.MBCHC.COMP_HINT.remove() || next(nextargs)) +window.MBCHC.sdk.hookFunction("ChatRoomResize", 0, (nextargs, next) => { + let result = next(nextargs) + if (window.CharacterGetCurrent() == null && window.CurrentScreen == "ChatRoom" && document.getElementById("InputChat") && document.getElementById("TextAreaChatLog") && window.MBCHC.comp_hint_visible()) { // upstream + let fontsize = ChatRoomFontSize /* eslint-disable-line no-undef */ // window.ChatRoomFontSize is undefined + window.ElementPositionFix("TextAreaChatLog", fontsize, 1005, 66, 988, 630) + window.ElementPositionFix(window.MBCHC.COMP_HINT.id, fontsize, 1005, 701, 988, 200) + window.MBCHC.COMP_HINT.style.display = "flex" + } + return(result) +}) +window.MBCHC.sdk.hookFunction("DocumentKeyDown", 0, (nextargs, next) => { + let [event] = nextargs + if ("InputChat" === document.activeElement.id || "bce-message-input" === document.activeElement.id) return(next(nextargs)) + if ("inline" === document.getElementById("InputChat")?.style.display && [event.altKey, event.ctrlKey, event.metaKey].every(i => !i)) window.ElementFocus("InputChat") // alt, ctrl and meta should all be false + // TODO: this is not ideal, but it will have to do for now + // TODO: Ctrl+V should still paste + return(next(nextargs)) +}) +window.MBCHC.sdk.hookFunction("ChatRoomKeyDown", 0, (nextargs, next) => { + let [event] = nextargs + window.MBCHC.complete_hint_hide() + if (33 == window.KeyPress || 34 == window.KeyPress) { // better history + event.preventDefault() + return(window.MBCHC.history(window.KeyPress - 33)) + } + if (window.MBCHC.HISTORY_MODE) { + window.ChatRoomLastMessage.pop() + window.MBCHC.HISTORY_MODE = false + } + return(next(nextargs)) +}) +window.MBCHC.sdk.hookFunction("ChatRoomClick", 0, (nextargs, next) => void window.MBCHC.complete_hint_hide() || next(nextargs)) +window.MBCHC.remove_fbc_hook = window.MBCHC.sdk.hookFunction("MainRun", 0, (nextargs, next) => void window.MBCHC.patch_fbc() || next(nextargs)) + +// Chat room handlers +window.ChatRoomRegisterMessageHandler({ Priority: -220, Description: "MBCHC preprocessor", Callback: (data, sender, msg, metadata) => { + data.MBCHC_ID = window.MBCHC.NEXT_MESSAGE++ + window.MBCHC.LOG_MESSAGES && console.debug({data, sender, msg, metadata}) + "Action" === data.Type && "ServerEnter" === data.Content && data.Sender === window.Player.cid && window.MBCHC.player_enters_room() + "Hidden" === data.Type && "ReceiveSuitcaseMoney" === data.Content && (window.MBCHC.LAST_HACKED = data.Sender) + return "Hidden" === data.Type && "MBCHC" === data.Content && !void window.MBCHC.receive(data) +}}) + +// MAIN SCREEN TURN ON +if (!window.CurrentModule || !window.CurrentScreen || "Character" === window.CurrentModule && "Login" === window.CurrentScreen) { // we need a load hook + window.MBCHC.remove_load_hook = window.MBCHC.sdk.hookFunction("AsylumGGTSSAddItems", 0, (nextargs, next) => void window.MBCHC.loader() || next(nextargs)) +} else { + void window.MBCHC.loader() || "Online" === window.CurrentModule && "ChatRoom" === window.CurrentScreen && !void window.ChatRoomCharacter.forEach(c => window.MBCHC.update_char(c)) && window.MBCHC.player_enters_room() +} })()