MBCHC/mbchc-local.user.js

583 lines
41 KiB
JavaScript

// ==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-local.user.js
// @downloadURL https://code.fleshless.org/mute/MBCHC/raw/branch/master/mbchc-local.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*
// @match http://localhost:*/*
// @match http://127.0.0.1:*/*
// @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
;(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, 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 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 => `<div><b>${c.name}</b> (${c.cid}): ${c.version}</div>`).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 (<b>WIP</b>)", 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(`<div class="mbchc">${html}</div>`, 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]) => `<div>/mbchc ${cmd} ${sub.args ? Object.keys(sub.args).join(" ") : ""}: ${sub.desc}</div>`).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("<div>Usage: /do VERB [ZONE] [TARGET]</div><div>Available verbs:</div>" + Object.keys(mbchc.MAP_ACTIONS).join(", ") + "<div>Available zones:</div>" + 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 => `<div>${s}</div>`).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()
}
})()