diff --git a/mbchc-dev.user.js b/mbchc-dev.user.js
index 2bcf54c..23932ea 100644
--- a/mbchc-dev.user.js
+++ b/mbchc-dev.user.js
@@ -17,655 +17,655 @@
// ==/UserScript==
(function() {
- "use strict";
- if (!window.AsylumGGTSSAddItems) throw "AsylumGGTSSAddItems() not found, aborting MBCHC loading"
- if (window.MBCHC) throw "MBCHC found, aborting loading"
- window.MBCHC = {
- VERSION: "dev.7",
- TARGET_VERSION: "R86",
- 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)\s*([+-])\s*(\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,
- 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"}},
+"use strict";
+if (!window.AsylumGGTSSAddItems) throw "AsylumGGTSSAddItems() not found, aborting MBCHC loading"
+if (window.MBCHC) throw "MBCHC found, aborting loading"
+window.MBCHC = {
+ VERSION: "dev.7",
+ TARGET_VERSION: "R86",
+ 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)\s*([+-])\s*(\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,
+ 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 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 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"}},
+ // 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"},
},
- 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"],
+ "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"}
},
- 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
+ "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 [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 || a.Activity?.Name === action))
- if ((activity.Name || activity.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 || activity.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: "\u200C"}] // zero-width non-joiner, used to break up ligatures, does nothing here, but an empty string is a falsy value
- 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
- },
- 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}))) },
- find_timezone: function(char) {
- const timezones = this.settings("timezones")
- if (timezones && "number" === typeof timezones[char.cid]) return(timezones[char.cid])
- const match = (char.Description) ? char.Description.match(this.RE_TZ) : null
- const int = match ? Number.parseInt(match[1] + match[2]) : 42
- if (this.in(int, -12, 12)) return(int)
- 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 || a.Activity?.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
- },
- focus_chat_whitelist(event) {
- if (event.ctrlKey && "v" === event.key) return true // ctrl+V should paste
- return false
- },
- focus_chat(event) { // TODO: this is not ideal, but it will have to do for now
- if (event.repeat) return // only unique presses please
- if ("inline" !== document.getElementById("InputChat")?.style.display) return // input chat missing
- if (["InputChat","bce-message-input"].includes(document.activeElement.id)) return // focus already set
- if ([event.altKey, event.ctrlKey, event.metaKey].some(i => i) && !this.focus_chat_whitelist(event)) return // alt, ctrl and meta should all be false
- window.ElementFocus("InputChat")
- },
- loader() {
- 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()
- window.Player.MBCHC = {VERSION: this.VERSION}
- window.CommandCombine(COMMANDS)
-
- // Hooks
- this.remove_fbc_hook = this.before("MainRun", () => window.bce_ActivityTriggers && this.patch_fbc())
- this.after("CharacterOnlineRefresh", char => this.update_char(char))
- this.after("ChatRoomReceiveSuitcaseMoney", () => {
- if (this.AUTOHACK_ENABLED && this.LAST_HACKED) {
- window.CurrentCharacter = this.cid2char(this.LAST_HACKED)
- this.LAST_HACKED = null
- window.ChatRoomTryToTakeSuitcase()
- }
- })
- this.before("ChatRoomSendChat", () => {
- let input = window.ElementValue("InputChat")
- if (!input.startsWith("@@@") && input.startsWith("@")) {
- input = input.replace(this.RE_PREF_ACTIVITY, this.PREF_ACTIVITY)
- input = input.replace(this.RE_PREF_ACTIVITY_ME, this.replace_me)
- window.ElementValue("InputChat", input)
- }
- })
- this.after("ChatRoomSendChat", () => {
- const history = window.ChatRoomLastMessage
- if ((history.length > 1) && (history[history.length - 1] === history[history.length - 2])) {history.pop(); window.ChatRoomLastMessageIndex -= 1}
- })
- this.before("ChatRoomDrawCharacterOverlay", (C, CharX, CharY, Zoom, Pos) => {
- window.ChatRoomHideIconState < 1 && C.MBCHC && window.DrawRect(CharX + 175 * Zoom, CharY, 50 * Zoom, 50 * Zoom, C.MBCHC.VERSION === window.Player.MBCHC.VERSION ? this.RGB_POLLY : this.RGB_MUTE)
- if (window.ChatRoomHideIconState < 1 && C.MBCHC_LOCAL && "number" === typeof C.MBCHC_LOCAL.TZ) {
- const hours = new Date(window.CommonTime() + this.UTC_OFFSET + C.MBCHC_LOCAL.TZ * 60 * 60 * 1000).getHours()
- window.DrawTextFit(hours < 10 ? "0" + hours.toString() : hours.toString(), CharX + 200 * Zoom, CharY + 25 * Zoom, 46 * Zoom, "white", "black")
- }
- })
- this.after("ElementValue", (ID, Value, cc = window.CurrentCharacter) => "bce_LayerPriority" === ID && cc?.FocusGroup && window.InventoryGet(cc, cc.FocusGroup.Name)?.Difficulty.toString() === Value && window.InventoryLocked(cc, cc.FocusGroup.Name, true) && window.ElementSetAttribute(ID, "disabled", true))
- this.after("ChatRoomCreateElement", () => this.COMP_HINT.parentElement || document.body.appendChild(this.COMP_HINT))
- this.before("ChatRoomClearAllElements", () => !void this.complete_hint_hide() && this.COMP_HINT.remove())
- this.before("ChatRoomClick", () => this.complete_hint_hide())
- this.after("ChatRoomResize", () => {
- if (window.CharacterGetCurrent() == null && window.CurrentScreen == "ChatRoom" && document.getElementById("InputChat") && document.getElementById("TextAreaChatLog") && this.comp_hint_visible()) { // upstream
- const fontsize = ChatRoomFontSize /* eslint-disable-line no-undef */ // window.ChatRoomFontSize is undefined
- window.ElementPositionFix("TextAreaChatLog", fontsize, 1005, 66, 988, 630)
- window.ElementPositionFix(this.COMP_HINT.id, fontsize, 1005, 701, 988, 200)
- this.COMP_HINT.style.display = "flex"
- }
- })
- document.addEventListener("keydown", event => this.focus_chat(event))
- this.SDK.hookFunction("ChatRoomKeyDown", 0, (nextargs, next) => { // this fires on chat input events
- 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))
- })
-
- // Chat room handlers
- window.ChatRoomRegisterMessageHandler({ Priority: -220, Description: "MBCHC preprocessor",
- Callback: (data, sender, msg, metadata) => {
- data.MBCHC_ID = this.NEXT_MESSAGE
- this.NEXT_MESSAGE += 1
- if (this.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)) this.player_enters_room() }
- })
- window.ChatRoomRegisterMessageHandler({ Priority: -219, Description: "MBCHC specific consumer",
- Callback: (data, sender, msg, metadata) => { if (("Hidden" === data.Type) && ("MBCHC" === data.Content)) return this.receive(data) }
- })
- window.ChatRoomRegisterMessageHandler({ Priority: -219, Description: "MBCHC autohack lookup",
- Callback: (data, sender, msg, metadata) => { if (("Hidden" === data.Type) && ("ReceiveSuitcaseMoney" === data.Content)) this.LAST_HACKED = data.Sender }
- })
-
- // footer
- this.LOADED = true
- this.log("info", `loaded version ${this.VERSION}`)
- if (window.GameVersion !== this.TARGET_VERSION) this.log("warn", `Game version doesn't match the target ("${this.TARGET_VERSION}"), beware of incompatibilities`) // TODO: betas are like R86Beta1; cheat?
- if (("Online" === window.CurrentModule) && ("ChatRoom" === window.CurrentScreen)) {
- window.ChatRoomCharacter.forEach(c => this.update_char(c))
- this.player_enters_room()
- }
- },
- preloader() {
- this.SDK = window.bcModSdk.registerMod({name:"MBCHC",fullName:"Mute's Bondage Club Hacks Collection",version:this.VERSION,repository:"https://code.fleshless.org/mute/MBCHC/"})
- this.before = (name, cb) => this.SDK.hookFunction(name, 0, (nextargs,next) => {try {cb?.(...nextargs)} catch (x) {console.error(x)} finally {return next(nextargs)}})
- this.after = (name, cb) => this.SDK.hookFunction(name, 0, (nextargs,next) => {const result = next(nextargs); try {cb?.(...nextargs)} catch (x) {console.error(x)} finally {return result}})
- if (window.CurrentModule && window.CurrentScreen && !("Character" === window.CurrentModule && "Login" === window.CurrentScreen)) return this.loader()
- this.remove_load_hook = this.before("AsylumGGTSSAddItems", () => this.loader())
+ for (let verb of verbs.split("|")) this.DO_DATA.verbs[verb] = unwound
}
- } // MBCHC
+ 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 || a.Activity?.Name === action))
+ if ((activity.Name || activity.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 || activity.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: "\u200C"}] // zero-width non-joiner, used to break up ligatures, does nothing here, but an empty string is a falsy value
+ 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
+ },
+ 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}))) },
+ find_timezone: function(char) {
+ const timezones = this.settings("timezones")
+ if (timezones && "number" === typeof timezones[char.cid]) return(timezones[char.cid])
+ const match = (char.Description) ? char.Description.match(this.RE_TZ) : null
+ const int = match ? Number.parseInt(match[1] + match[2]) : 42
+ if (this.in(int, -12, 12)) return(int)
+ 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 || a.Activity?.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
+ },
+ focus_chat_whitelist(event) {
+ if (event.ctrlKey && "v" === event.key) return true // ctrl+V should paste
+ return false
+ },
+ focus_chat(event) { // TODO: this is not ideal, but it will have to do for now
+ if (event.repeat) return // only unique presses please
+ if ("inline" !== document.getElementById("InputChat")?.style.display) return // input chat missing
+ if (["InputChat","bce-message-input"].includes(document.activeElement.id)) return // focus already set
+ if ([event.altKey, event.ctrlKey, event.metaKey].some(i => i) && !this.focus_chat_whitelist(event)) return // alt, ctrl and meta should all be false
+ window.ElementFocus("InputChat")
+ },
+ loader() {
+ 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()
+ window.Player.MBCHC = {VERSION: this.VERSION}
+ window.CommandCombine(COMMANDS)
- fetch("https://code.fleshless.org/mute/MBCHC/raw/branch/master/bondage-club-mod-sdk-1.1.0.js").then(r=>r.text()).then(r=>eval(r)).then(_=>window.MBCHC.preloader()).catch(x=>console.error(x)) /* eslint-disable-line no-eval */
+ // Hooks
+ this.remove_fbc_hook = this.before("MainRun", () => window.bce_ActivityTriggers && this.patch_fbc())
+ this.after("CharacterOnlineRefresh", char => this.update_char(char))
+ this.after("ChatRoomReceiveSuitcaseMoney", () => {
+ if (this.AUTOHACK_ENABLED && this.LAST_HACKED) {
+ window.CurrentCharacter = this.cid2char(this.LAST_HACKED)
+ this.LAST_HACKED = null
+ window.ChatRoomTryToTakeSuitcase()
+ }
+ })
+ this.before("ChatRoomSendChat", () => {
+ let input = window.ElementValue("InputChat")
+ if (!input.startsWith("@@@") && input.startsWith("@")) {
+ input = input.replace(this.RE_PREF_ACTIVITY, this.PREF_ACTIVITY)
+ input = input.replace(this.RE_PREF_ACTIVITY_ME, this.replace_me)
+ window.ElementValue("InputChat", input)
+ }
+ })
+ this.after("ChatRoomSendChat", () => {
+ const history = window.ChatRoomLastMessage
+ if ((history.length > 1) && (history[history.length - 1] === history[history.length - 2])) {history.pop(); window.ChatRoomLastMessageIndex -= 1}
+ })
+ this.before("ChatRoomDrawCharacterOverlay", (C, CharX, CharY, Zoom, Pos) => {
+ window.ChatRoomHideIconState < 1 && C.MBCHC && window.DrawRect(CharX + 175 * Zoom, CharY, 50 * Zoom, 50 * Zoom, C.MBCHC.VERSION === window.Player.MBCHC.VERSION ? this.RGB_POLLY : this.RGB_MUTE)
+ if (window.ChatRoomHideIconState < 1 && C.MBCHC_LOCAL && "number" === typeof C.MBCHC_LOCAL.TZ) {
+ const hours = new Date(window.CommonTime() + this.UTC_OFFSET + C.MBCHC_LOCAL.TZ * 60 * 60 * 1000).getHours()
+ window.DrawTextFit(hours < 10 ? "0" + hours.toString() : hours.toString(), CharX + 200 * Zoom, CharY + 25 * Zoom, 46 * Zoom, "white", "black")
+ }
+ })
+ this.after("ElementValue", (ID, Value, cc = window.CurrentCharacter) => "bce_LayerPriority" === ID && cc?.FocusGroup && window.InventoryGet(cc, cc.FocusGroup.Name)?.Difficulty.toString() === Value && window.InventoryLocked(cc, cc.FocusGroup.Name, true) && window.ElementSetAttribute(ID, "disabled", true))
+ this.after("ChatRoomCreateElement", () => this.COMP_HINT.parentElement || document.body.appendChild(this.COMP_HINT))
+ this.before("ChatRoomClearAllElements", () => !void this.complete_hint_hide() && this.COMP_HINT.remove())
+ this.before("ChatRoomClick", () => this.complete_hint_hide())
+ this.after("ChatRoomResize", () => {
+ if (window.CharacterGetCurrent() == null && window.CurrentScreen == "ChatRoom" && document.getElementById("InputChat") && document.getElementById("TextAreaChatLog") && this.comp_hint_visible()) { // upstream
+ const fontsize = ChatRoomFontSize /* eslint-disable-line no-undef */ // window.ChatRoomFontSize is undefined
+ window.ElementPositionFix("TextAreaChatLog", fontsize, 1005, 66, 988, 630)
+ window.ElementPositionFix(this.COMP_HINT.id, fontsize, 1005, 701, 988, 200)
+ this.COMP_HINT.style.display = "flex"
+ }
+ })
+ document.addEventListener("keydown", event => this.focus_chat(event))
+ this.SDK.hookFunction("ChatRoomKeyDown", 0, (nextargs, next) => { // this fires on chat input events
+ 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))
+ })
+
+ // Chat room handlers
+ window.ChatRoomRegisterMessageHandler({ Priority: -220, Description: "MBCHC preprocessor",
+ Callback: (data, sender, msg, metadata) => {
+ data.MBCHC_ID = this.NEXT_MESSAGE
+ this.NEXT_MESSAGE += 1
+ if (this.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)) this.player_enters_room() }
+ })
+ window.ChatRoomRegisterMessageHandler({ Priority: -219, Description: "MBCHC specific consumer",
+ Callback: (data, sender, msg, metadata) => { if (("Hidden" === data.Type) && ("MBCHC" === data.Content)) return this.receive(data) }
+ })
+ window.ChatRoomRegisterMessageHandler({ Priority: -219, Description: "MBCHC autohack lookup",
+ Callback: (data, sender, msg, metadata) => { if (("Hidden" === data.Type) && ("ReceiveSuitcaseMoney" === data.Content)) this.LAST_HACKED = data.Sender }
+ })
+
+ // footer
+ this.LOADED = true
+ this.log("info", `loaded version ${this.VERSION}`)
+ if (window.GameVersion !== this.TARGET_VERSION) this.log("warn", `Game version doesn't match the target ("${this.TARGET_VERSION}"), beware of incompatibilities`) // TODO: betas are like R86Beta1; cheat?
+ if (("Online" === window.CurrentModule) && ("ChatRoom" === window.CurrentScreen)) {
+ window.ChatRoomCharacter.forEach(c => this.update_char(c))
+ this.player_enters_room()
+ }
+ },
+ preloader() {
+ this.SDK = window.bcModSdk.registerMod({name:"MBCHC",fullName:"Mute's Bondage Club Hacks Collection",version:this.VERSION,repository:"https://code.fleshless.org/mute/MBCHC/"})
+ this.before = (name, cb) => this.SDK.hookFunction(name, 0, (nextargs,next) => {try {cb?.(...nextargs)} catch (x) {console.error(x)} finally {return next(nextargs)}})
+ this.after = (name, cb) => this.SDK.hookFunction(name, 0, (nextargs,next) => {const result = next(nextargs); try {cb?.(...nextargs)} catch (x) {console.error(x)} finally {return result}})
+ if (window.CurrentModule && window.CurrentScreen && !("Character" === window.CurrentModule && "Login" === window.CurrentScreen)) return this.loader()
+ this.remove_load_hook = this.before("AsylumGGTSSAddItems", () => this.loader())
+ }
+} // MBCHC
+
+fetch("https://code.fleshless.org/mute/MBCHC/raw/branch/master/bondage-club-mod-sdk-1.1.0.js").then(r=>r.text()).then(r=>eval(r)).then(_=>window.MBCHC.preloader()).catch(x=>console.error(x)) /* eslint-disable-line no-eval */
})()