Compare commits

...

65 Commits
v.1 ... master

Author SHA1 Message Date
Mute d30945201d dev.10
* Removed the patch to disable FBC tighten/loosen input, it wasn't doing anything for a while now.
* Come on peeps, deprecation doesn't mean what you think it means.
2024-02-29 18:17:49 +00:00
Mute 378d0af8d3 dev.9
* Fixed MISSING PLAYER DIALOG in donate subcommand
* Removed the club version check, it really doesn't do anything
* Removed the broken disappear subcommand
* Removed the mostly useless title subcommand
* Removed the versions subcommand, it was superceded by /versions
* Took another dive in the keystroke catcher, should fix all known conflicts
* Disabled the indicator square
* Thought about moving the time digits to a proper place, but ultimately decided to leave them be for now
2023-12-19 01:15:39 +00:00
Mute 526b51e158 ASHWGA, might as well make it a module.
Included is a minimal linter infrastructure
and a server script because [epic rant removed]
2023-12-17 03:52:43 +00:00
Mute ff377fb709 Update 'README.md' 2023-06-18 02:15:22 +00:00
Mute e4f7ce0560 dev.8
* R93
* /do fix
2023-06-18 02:07:49 +00:00
Mute 2bf6695af0 no changes, unindent by one level 2023-06-18 02:00:47 +00:00
Mute 0f1195d92b no changes beyond indentation 2023-06-18 01:51:26 +00:00
Mute d3852d6e63 massive diff: no changes, just reindent 2022-12-04 14:16:28 +00:00
Mute 2f87b283ed dev.7
* R86
* external sdk
* sdk 1.1.0
* /activity fix
2022-11-30 12:10:04 +00:00
Mute 89eafdfab3 trunk: sdk 1.1.0 2022-11-28 23:48:08 +00:00
Mute 0d1f074c43 sdk 1.1.0 2022-11-28 23:17:26 +00:00
Mute 247134e04b external sdk; /activity fix 2022-11-21 21:50:38 +00:00
Mute 46fb29bcec let's factor out sdk into an external js 2022-11-21 09:25:52 +00:00
Mute 45f6f80493 dev.6
* R86Beta1 compatibility
* patch by Lyra for /do item actions
* removed handheld penetration patch until R86 release
* time zone regexp accepts spaces now
* fixed a bug with UTC+0
* better global keydown interception
* exception handlers for hooks, shouldn't break the whole club now if something goes wrong
* general cleanups and refactor
2022-11-13 16:40:33 +00:00
Mute eaf4e3f26f so refactor, much changes
mostly improved syntax
added exception handling for hooks
hopefully fixed keydown interceptor
2022-11-12 13:06:54 +00:00
Mute 337eb83d47 Update 'mbchc-local.user.js'
/me grumbles
2022-11-11 02:14:59 +00:00
Mute 3a97857fc1 more beta fixes, removed the penetration patch for now 2022-11-11 00:03:09 +00:00
Mute a9c6e5fd35 R86Beta1 2022-11-10 22:22:12 +00:00
Mute 247c908f46 tz fix + allow spaces
0 is falsy, who would have thunk. Will now accept spaces like "UTC - 7" because people do write like this.
2022-11-10 12:07:51 +00:00
Mute 1cc9ac18d7 Update 'mbchc-local.user.js' 2022-11-03 18:39:43 +00:00
Mute e690960782 Update 'mbchc-local.user.js' 2022-11-03 18:20:44 +00:00
Mute 634eddbd2a Update 'mbchc-local.user.js'
patch by Lyra, fixes `/do item` behaviour
2022-11-03 18:17:20 +00:00
Mute d43e3cdeed Update 'AUTHORS' 2022-11-03 18:05:13 +00:00
Mute 5c1d2f6397 dev.5
I thought I was being clever with Intl API, but it was a mistake, had to make it dumb and simple instead. Fixes time display with non-US locales.
2022-10-27 22:50:08 +00:00
Mute 5c534b1537 Update 'mbchc-local.user.js'
locales are bullshit
2022-10-27 17:27:42 +00:00
Mute 41b2029efd dev.4
chat room handlers instead of a function hook, this fixes communication breakdown
2022-10-24 18:03:26 +00:00
Mute f911c05073 Update 'mbchc-local.user.js'
chat room handlers
2022-10-22 12:07:11 +00:00
Mute 8d1516f1a7 dev.3
backported trunk version
no new functionality
fixes and cleanups
2022-10-20 11:02:02 +00:00
Mute 3e3f891016 R85 update 2022-10-18 11:46:16 +00:00
Mute ef41dd1dce maintenance update
* s/bce/fbc where is makes sense
* quick and dirty keydown filtering
* R84
* penetrators now use AllowActivity instead of Attribute
2022-09-29 18:00:14 +00:00
Mute 4930e5111b Update 'mbchc-local.user.js' 2022-08-11 00:56:40 +00:00
Mute c76ad63d96 Update 'mbchc-dev.user.js'
disallow /do in rooms with blocked activities; autocomplete improvements
2022-07-20 23:30:36 +00:00
Mute 18095495bb Update 'mbchc-dev.user.js'
BCE expressions needed more love; also stripped a space in chats like "@'s ... "
2022-07-20 21:27:20 +00:00
Mute faf4e95499 Update 'mbchc-dev.user.js'
well this is embarrassing
2022-07-20 20:06:24 +00:00
Mute 839ad8f0b3 Update 'mbchc-dev.user.js'
let's try this again
2022-07-20 20:01:35 +00:00
Mute 62def6bb3e sdk conflict hopefully fixed 2022-07-20 11:48:24 +00:00
Mute 3f4e43adbc Update 'README.md' 2022-07-20 08:21:10 +00:00
Mute 29e9d84ceb Update 'mbchc-dev.user.js'
BCE IM support
2022-07-20 08:14:32 +00:00
Mute b99e28502b Update 'mbchc-dev.user.js'
well fuck
2022-07-20 08:01:47 +00:00
Mute 621dea9959 dev.2
an entirely new script, very little in common with dev.1
2022-07-20 06:22:21 +00:00
Mute 0145147333 Update 'mbchc-local.user.js' 2022-07-18 04:25:30 +00:00
Mute 6fbe13b17a Update 'mbchc-local.user.js'
* bugfix (Nickname undefined)
* history will now search by input
2022-07-16 15:03:53 +00:00
Mute c73bffc4bb Update 'mbchc-local.user.js' 2022-07-15 16:28:26 +00:00
Mute 537c538211 Update 'mbchc-local.user.js' 2022-07-15 00:09:12 +00:00
Mute faf2714810 Update 'mbchc-local.user.js' 2022-07-14 04:28:37 +00:00
Mute 87cf188c64 Update 'mbchc-local.user.js'
hint needs more work, won't show at all for now
2022-07-12 03:53:07 +00:00
Mute c872444b14 Update 'mbchc-local.user.js' 2022-07-11 19:20:43 +00:00
Mute 69193e0861 Update 'mbchc-local.user.js' 2022-07-11 15:40:03 +00:00
Mute 18702fa607 Update 'mbchc-local.user.js' 2022-07-11 02:42:17 +00:00
Mute f7e571b26a Update 'mbchc-local.user.js' 2022-07-10 07:07:09 +00:00
Mute 561bedc606 Update 'mbchc-local.user.js' 2022-07-09 17:52:03 +00:00
Mute a0e482c536 Update 'mbchc-local.user.js' 2022-07-09 02:33:08 +00:00
Mute 071ca9a8e1 Update 'mbchc-local.user.js' 2022-07-09 01:56:25 +00:00
Mute 1fa1c8c951 Update 'mbchc-local.user.js'
also cleanup
2022-07-08 19:32:31 +00:00
Mute d30d0c1408 Update 'README.md' 2022-07-08 03:10:05 +00:00
Mute 8d6669795b Update 'mbchc-local.user.js' 2022-07-07 23:34:05 +00:00
Mute a140c3a301 Update 'mbchc-local.user.js' 2022-07-07 23:10:18 +00:00
Mute 7611721459 select background colour depending on current theme 2022-07-06 21:56:18 +00:00
Mute 1d8e3b100b Update 'mbchc-local.user.js' 2022-07-06 07:02:59 +00:00
Mute e6a35024c8 Update 'mbchc-local.user.js' 2022-07-05 17:53:00 +00:00
Mute 6663c2eede Update 'mbchc-local.user.js' 2022-07-05 01:04:07 +00:00
Mute 1f45cb04e0 Update 'mbchc-local.user.js' 2022-07-04 22:22:47 +00:00
Mute c29d264520 Update 'mbchc-local.user.js' 2022-07-04 22:19:38 +00:00
Mute ee52aa45a8 mbchc-local.user.js (trunk) 2022-07-04 22:07:57 +00:00
Mute a06791c039 Update 'README.md' 2022-07-02 16:21:27 +00:00
11 changed files with 7615 additions and 230 deletions

1
.gitignore vendored
View File

@ -0,0 +1 @@
node_modules

View File

@ -1 +1,2 @@
Mute
Lyra

View File

@ -1,3 +1,7 @@
Mute's Bondage Club Hacks Collection
# Mute's Bondage Club Hacks Collection
This document is updated with stable releases, for additional documentation please consult [[the wiki|https://code.fleshless.org/mute/MBCHC/wiki/Home]].
[[https://code.fleshless.org/mute/MBCHC/wiki]]
* Unstable doc: https://code.fleshless.org/mute/MBCHC/wiki/unstable
## PSA
The only supported version is unstable, don't use any other.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,248 +1,667 @@
// ==UserScript==
// @name MBCHC
// @version dev.1
// @version dev.8
// @description Mute's Bondage Club Hacks Collection
// @author codename.mute@proton.me
// @homepage https://code.fleshless.org/mute/MBCHC
// @namespace https://code.fleshless.org/mute/
// @homepage https://code.fleshless.org/mute/MBCHC
// @updateURL https://code.fleshless.org/mute/MBCHC/raw/branch/master/mbchc-dev.user.js
// @downloadURL https://code.fleshless.org/mute/MBCHC/raw/branch/master/mbchc-dev.user.js
// @match https://bondageprojects.elementfx.com/R*
// @match https://www.bondageprojects.elementfx.com/R*
// @match https://bondage-europe.com/R*
// @match https://www.bondage-europe.com/R*
// @match http://localhost:*/*
// @match http://127.0.0.1:*/*
// @grant none
// ==/UserScript==
(function() {
'use strict';
if (!window.AsylumGGTSSAddItems) throw "AsylumGGTSSAddItems() not found, aborting"
"use strict";
if (!window.AsylumGGTSSAddItems) throw "AsylumGGTSSAddItems() not found, aborting MBCHC loading"
if (window.MBCHC) throw "MBCHC found, aborting loading"
window.MBCHC = {
VERSION: "dev.8",
TARGET_VERSION: "R93",
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"}},
// Static data
window.MBCHC = {
NEXT_MESSAGE: 1,
LOG_MESSAGES: false,
LOADED: false,
VERSION: 'dev',
AUTOHACK_ENABLED: false,
LAST_HACKED: null,
RE_TITLE: /^[a-zA-Z]+$/,
HIDE_SPECIAL: ["Activity","Emoticon"],
HIDE_BODY: ["Blush","BodyLower","BodyUpper","Eyebrows","Eyes","Eyes2","Face","Fluids","HairBack","HairFront","Hands","Head","Mouth","Nipples","Pussy"],
HIDE_CLOTHES: [
"Cloth","ClothAccessory","Necklace","Suit","ClothLower","SuitLower","Bra","Corset","Panties",
"Socks","RightAnklet","LeftAnklet","Garters","Shoes","Hat","HairAccessory3","HairAccessory1","HairAccessory2",
"Gloves","Bracelet","Glasses","Mask","TailStraps","Wings"
],
HIDE_ITEMS: [
"ItemMisc","ItemEars","ItemHead","ItemNose","ItemHood","ItemAddon","ItemMouth","ItemMouth2","ItemMouth3",
"ItemArms","ItemNeckAccessories","ItemNeck","ItemNeckRestraints","ItemNipples","ItemNipplesPiercings","ItemBreast","ItemTorso",
"ItemHands","ItemPelvis","ItemVulva","ItemVulvaPiercings",
"ItemDevices","ItemLegs","ItemFeet","ItemBoots"
],
COMMANDS: [
{ Tag: "disappear",
Description: ": Become invisible; requires anal hook (hair)",
Action: (argline, cmdline, args) => {
try {
window.MBCHC.make_my_anal_hook_hide_body()
} catch (x) { window.ChatRoomSendLocal(`(Error: ${x.toString()})`) }
},
},
{ Tag: "title",
Description: "[Title]: (WIP) Set a custom title (one short word, letters only)",
Action: (argline, cmdline, args) => {
try {
window.MBCHC.action_title(args)
} catch (x) { window.ChatRoomSendLocal(`(Error: ${x.toString()})`) }
},
},
{ Tag: "donate",
Description: "[MemberNumber]: Buy data and send it to recipient",
Action: (argline, cmdline, args) => {
try {
window.MBCHC.action_donate(args)
} catch (x) { window.ChatRoomSendLocal(`(Error: ${x.toString()})`) }
},
},
{ Tag: "autohack",
Description: ": Toggle autohack mode",
Action: (argline, cmdline, args) => {
try {
window.MBCHC.action_autohack()
} catch (x) { window.ChatRoomSendLocal(`(Error: ${x.toString()})`) }
},
},
{ Tag: "nod",
Description: ": Nod",
Action: (argline, cmdline, args) => {
try {
window.MBCHC.run_activity(window.Player, "ItemHead", "Nod")
} catch (x) { window.ChatRoomSendLocal(`(Error: ${x.toString()})`) }
},
},
{ Tag: "shake",
Description: ": Shake your head",
Action: (argline, cmdline, args) => {
try {
window.MBCHC.run_activity(window.Player, "ItemHead", "Wiggle")
} catch (x) { window.ChatRoomSendLocal(`(Error: ${x.toString()})`) }
},
},
{ Tag: "shrug",
Description: ": Shrug",
Action: (argline, cmdline, args) => {
try {
window.MBCHC.send_activity(`${window.CharacterNickname(window.Player)} shrugs.`)
} catch (x) { window.ChatRoomSendLocal(`(Error: ${x.toString()})`) }
},
},
{ Tag: "myself",
Description: "[Message]: Send a custom activity as yourself (or \"@Message\")",
Action: (argline, cmdline, args) => {
if (!window.MBCHC.empty(argline)) {
try {
let message = window.MBCHC.add_full_stop(argline)
window.MBCHC.send_activity(`${window.CharacterNickname(window.Player)} ${message}`)
} catch (x) { window.ChatRoomSendLocal(`(Error: ${x.toString()})`) }
}
},
},
{ Tag: "activity",
Description: "[Message]: Send a custom activity (or \"@@Message\")",
Action: (argline, cmdline, args) => {
if (!window.MBCHC.empty(argline)) {
try {
let message = window.MBCHC.add_full_stop(window.MBCHC.capitalise(argline))
window.MBCHC.send_activity(message)
} catch (x) { window.ChatRoomSendLocal(`(Error: ${x.toString()})`) }
}
},
},
],
log: function(msg) {return("MBCHC: " + msg.toString())},
empty: function(text) {
if (!text) return(true)
if (String(text).trim().length < 1) return(true)
return(false)
},
add_full_stop: function(text) {
if (text.endsWith('.')) return(text)
return(`${text}.`)
},
capitalise: function(text, lower = false) {
let first = text.at(0).toLocaleUpperCase()
let rest = text.slice(1)
if (lower) rest = rest.toLocaleLowerCase()
return(first + rest)
},
// we need this one here, this is our main loading hook
orig_AsylumGGTSSAddItems: window.AsylumGGTSSAddItems,
} // MBCHC
// action zone
"wiggle|shake": {"Arms,Breast,Boots,Butt,Ears,Feet,Hands,Nose,Pelvis,Torso": {self: "Wiggle"}},
// Loader
window.AsylumGGTSSAddItems = function() {
if (!window.MBCHC.LOADED) {
// 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"}},
// Save originals hopefully after patching
window.MBCHC.orig_ChatRoomMessageInvolvesPlayer = window.ChatRoomMessageInvolvesPlayer
window.MBCHC.orig_ChatRoomReceiveSuitcaseMoney = window.ChatRoomReceiveSuitcaseMoney
window.MBCHC.orig_ChatRoomSendChat = window.ChatRoomSendChat
// action zone target
"item": {
"Breast,Butt,Feet,Legs": {all: "SpankItem|TickleItem|RubItem|RollItem|MasturbateItem|PourItem|ShockItem|Inject"},
"Nipples,Pelvis": {all: "SpankItem|TickleItem|RubItem|RollItem|MasturbateItem|PourItem|ShockItem"},
Arms: {all: "SpankItem|TickleItem|RubItem|RollItem|PourItem|ShockItem|Inject"},
Boots: {all: "SpankItem|TickleItem|RubItem|RollItem|PourItem|ShockItem"},
"Ears,Mouth": {all: "TickleItem|RubItem|RollItem"},
"Hood,Nose": {all: "TickleItem|RubItem"},
Neck: {all: "TickleItem|RubItem|RollItem|Inject"},
Torso: {all: "SpankItem|TickleItem|RubItem|RollItem|PourItem|ShockItem"},
Vulva: {all: "SpankItem|TickleItem|RubItem|MasturbateItem|ShockItem"},
VulvaPiercings: {all: "SpankItem|TickleItem|RubItem|MasturbateItem|ShockItem|Inject"},
},
"kiss": {
Mouth: {others: "GagKiss|Kiss|GaggedKiss"},
"Boots,Hands": {self: "PoliteKiss", others: "PoliteKiss|GaggedKiss"},
"Arms,Breast,Nipples": {self: "Kiss", others: "Kiss|GaggedKiss"},
"Butt,Ears,Feet,Head,Legs,Neck,Nose,Pelvis,Torso,Vulva,VulvaPiercings": {others: "Kiss|GaggedKiss"}
},
"smooch": {"Hands,Boots": {all: "Kiss"}},
"nibble|chew": {"Arms,Hands,Boots,Mouth,Nipples": {all: "Nibble"}, "Ears,Feet,Legs,Neck,Nose,Pelvis,Torse,Vulva,VulvaPiercings": {others: "Nibble"}},
"slap|spank": {"Head,Breast,Vulva,VulvaPiercings": {all: "Slap"}, "Arms,Boots,Butt,Feet,Hands,Legs,Pelvis,Torso": {all: "Spank"}},
"tickle": {"Arms,Boots,Breast,Feet,Legs,Neck,Pelvis,Torso": {all: "Tickle"}},
"massage": {"Arms,Boots,Feet,Legs,Neck,Pelvis,Torso": {all: "MassageHands"}},
"lick": {"Arms,Boots,Breast,Hands,Mouth,Nipples": {all: "Lick"}, "Ears,Feet,Legs,Neck,Nose,Pelvis,Torso,Vulva,VulvaPiercings": {others: "Lick"}},
"suck": {"Nipples,Hands,Boots": {all: "Suck"}},
"bite": {"Arms,Boots,Feet,Hands,Legs,Mouth": {all: "Bite"}, "Breast,Butt,Ears,Head,Neck,Nipples,Nose,Torso": {others: "Bite"}},
"pinch": {"Arms,Ears,Nipples,Nose,Pelvis": {all: "Pinch"}},
"clamp": {Mouth: {all: "HandGag"}, Nose: {all: "Choke"}},
"step": {"Breast,Neck,Pelvis": {others: "Step"}},
"pull": {"Head,Nose,Nipples": {all: "Pull"}},
"grope": {"Butt,Breast": {all: "Grope"}, "Feet,Legs,Pelvis": {others: "Grope"}},
"rub": {"Head,Torso": {others: "Rub"}, Nose: {all: "Rub"}, Legs: {self: "Wiggle"}, Hands: {self: "Caress"}},
"caress": {Hands: {others: "Caress"}, "Arms,Breast,Butt,Ears,Feet,Head,Legs,Neck,Nipples,Nose,Pelvis,Torso,Vulva,VulvaPiercings": {all: "Caress"}},
"polish": {"Hands,Boots": {all: "TakeCare"}},
"foot": {"Head,Nose": {others: "Step"}, "Torso,Boots": {others: "MassageFeet"}, "Vulva,VulvaPiercings": {others: "MasturbateFoot"}},
"fist": {"Vulva,Butt": {all: "MasturbateFist"}},
"fuck": {"Mouth,Vulva,Butt": {others: "PenetrateSlow"}}, //peg?
"pound": {"Mouth,Vulva,Butt": {others: "PenetrateFast"}},
"tongue": {"Vulva,VulvaPiercings": {others: "MasturbateTongue"}},
"finger": {"Breast,Butt,Vulva,VulvaPiercings": {all: "MasturbateHand"}},
},
MAP_ZONES: {
"ItemBoots": ["foot", "feet", "boot", "boots", "shoe", "shoes", "toes", "toenails", "sole", "soles", "heel", "heels"],
"ItemFeet": ["leg", "legs", "ankle", "ankles"],
"ItemLegs": ["hips", "hip", "thighs", "thigh"],
"ItemVulva": ["vulva", "pussy"],
"ItemVulvaPiercings": ["clit", "clitoris"],
"ItemButt": ["butt", "ass"],
"ItemPelvis": ["tummy", "pelvis"],
"ItemTorso": ["body", "torso", "back", "ribs"],
"ItemBreast": ["breast", "breasts", "boob", "boobs", "booby", "boobie", "boobies", "tit", "tits", "titty", "tittie", "titties"],
"ItemNipples": ["nip", "nips", "nipple", "nipples"],
"ItemHands": ["hand", "hands", "fingers", "fingernails", "nails"],
"ItemArms": ["arm", "arms", "elbow", "elbows"],
"ItemNeck": ["neck"],
"ItemMouth": ["mouth", "lip", "lips", "teeth", "tongue", "gag", "cheek", "cheeks"],
"ItemNose": ["nose", "nostrils"],
"ItemEars": ["ear", "ears", "earlobe", "earlobes"],
"ItemHead": ["head", "face", "hair", "eyes", "forehead"],
},
FBC_TESTER_PATCHES: [
[/^\^('s)?( )?/g, "^SourceCharacter$1\\s+"],
[/([^\\])\$/g, "$1\\.?$$"],
],
SUBCOMMANDS_MBCHC: {
"versions": {desc: "show the mod versions across the room", cb: mbchc => mbchc.inform(mbchc.gather_versions().map(c => `<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 => {if (window.Player.OnlineSettings.MBCHC) {delete window.Player.OnlineSettings.MBCHC; mbchc.save_settings()}}},
},
ensure: function(error, callback) {
let result = callback.call(this)
if (!result) throw error
return(result)
},
calculate_maps: function() {
this.DO_DATA = {verbs: {}, zones: {}}
for (let [verbs, data] of Object.entries(this.MAP_ACTIONS)) {
let unwound = {}
for (let [zones, actions] of Object.entries(data)) {
let all = (actions.all) ? actions.all.split("|") : []
let processed = {self: (actions.self) ? actions.self.split("|").concat(all) : all, others: (actions.others) ? actions.others.split("|").concat(all) : all}
for (let zone of zones.split(",")) unwound[`Item${zone}`] = processed
}
for (let verb of verbs.split("|")) this.DO_DATA.verbs[verb] = unwound
}
for (let [ag, zones] of Object.entries(this.MAP_ZONES)) for (let zone of zones) this.DO_DATA.zones[zone] = ag
},
settings: function(setting = null) {
let settings = window.Player.OnlineSettings.MBCHC || {}
return(setting ? settings[setting] : settings)
},
save_settings: function(cb = null) {
if (cb) {
if (!window.Player.OnlineSettings.MBCHC) window.Player.OnlineSettings.MBCHC = {}
cb.call(this, window.Player.OnlineSettings.MBCHC)
}
window.ServerAccountUpdate.QueueData({OnlineSettings: window.Player.OnlineSettings})
},
log: function(level, msg) {console[level]("MBCHC: " + String(msg))},
empty: function(text) {
if (!text) return(true)
if (String(text).trim().length < 1) return(true)
return(false)
},
normalise_message: function(text, options = {}) {
let result = text
if (options.trim) result = result.trim()
if (options.low) result = result.toLocaleLowerCase()
if (options.up) {
let first = result.at(0).toLocaleUpperCase()
let rest = result.slice(1)
result = first + rest
}
if (options.dot && result.match(this.RE_LAST_LETTER)) result = `${result}.`
return(result)
},
tokenise: function(text) { return text.replace(this.RE_SPACES, " ").split(" ") },
inform: function(html, timeout = 60000) { window.ChatRoomSendLocal(`<div class="mbchc">${html}</div>`, 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(window.Player, char, char.FocusGroup, 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]) => `<div>/mbchc ${cmd} ${sub.args ? Object.keys(sub.args).join(" ") : ""}: ${sub.desc}</div>`).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("<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
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 => `<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: 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)
// Functions
window.MBCHC.HIDE_ALL = window.MBCHC.HIDE_SPECIAL.concat(window.MBCHC.HIDE_BODY).concat(window.MBCHC.HIDE_CLOTHES).concat(window.MBCHC.HIDE_ITEMS)
window.MBCHC.make_my_anal_hook_hide_body = 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)
}
window.MBCHC.donate_data = function(id) {
if (id == window.Player.MemberNumber) throw "recipient must not be you"
const char = window.ChatRoomCharacter.find( c => c.MemberNumber == id )
if (!char) throw "recipient not found"
if (!char.IsRestrained()) throw "recipient must be bound"
const cost = (Math.random() * 10 + 15).toFixed(0)
if (window.Player.Money < cost) throw "not enough money"
window.CharacterChangeMoney(window.Player, -cost)
window.ServerSend("ChatRoomChat", {Content: "ReceiveSuitcaseMoney", Type: "Hidden", Target: id})
return({cost: cost, name: (char.Nickname || char.Name)})
}
window.MBCHC.run_activity = function(char, ag, action) {
try {
char.FocusGroup = window.AssetGroupGet(char.AssetFamily, ag)
if (!char.FocusGroup) throw "invalid AssetGroup"
let activity = window.ActivityAllowedForGroup(char, char.FocusGroup.Name).find( a => a.Name === action)
if (!activity) throw "invalid activity"
window.ActivityRun(char, activity)
} finally {
char.FocusGroup = null
}
}
window.MBCHC.send_activity = function(msg) {
window.ServerSend("ChatRoomChat", {Type: "Action", Content: "lef_wie234jf_owhwi", Dictionary: [{Tag: 'MISSING PLAYER DIALOG: lef_wie234jf_owhwi', Text: msg}]})
}
// Command actions
window.MBCHC.action_title = function(args) {
let title = args.shift()
if (!title || !title.length || title.length < 1) throw "empty title"
if (title.length > 16) throw "title too long"
if (!title.match(window.MBCHC.RE_TITLE)) throw "invalid title"
let first = title.at(0).toLocaleUpperCase()
let rest = title.slice(1).toLocaleLowerCase()
title = first + rest
window.TitleSet(title)
// TODO: this needs much more work. at least don't push a second title
// we need to patch the text cache
// we need to check for other players' custom titles
window.TitleList.push({Name: title, Requirement: () => {return true}})
}
window.MBCHC.action_donate = function(args) {
let id = Number.parseInt(args.shift())
if (isNaN(id)) throw "empty or invalid member number"
let result = window.MBCHC.donate_data(id)
window.ChatRoomSendLocal(`(You've bought data for $${result.cost} and sent it to ${result.name})`)
}
window.MBCHC.action_autohack = function() {
window.MBCHC.AUTOHACK_ENABLED = !window.MBCHC.AUTOHACK_ENABLED
window.ChatRoomSendLocal(`(Autohack is now ${window.MBCHC.AUTOHACK_ENABLED ? "enabled" : "disabled"})`)
}
// 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))
})
// Hooks
window.ChatRoomMessageInvolvesPlayer = function(data) {
if (!data.MBCHC_ID) {
data.MBCHC_ID = window.MBCHC.NEXT_MESSAGE
window.MBCHC.NEXT_MESSAGE += 1
if (window.MBCHC.LOG_MESSAGES) console.debug(data)
if (("ReceiveSuitcaseMoney" === data.Content) && ("Hidden" === data.Type)) { window.MBCHC.LAST_HACKED = data.Sender }
}
return(window.MBCHC.orig_ChatRoomMessageInvolvesPlayer(data))
}
window.ChatRoomReceiveSuitcaseMoney = function() {
let result = window.MBCHC.orig_ChatRoomReceiveSuitcaseMoney()
if (window.MBCHC.AUTOHACK_ENABLED && window.MBCHC.LAST_HACKED) {
window.CurrentCharacter = {MemberNumber: window.MBCHC.LAST_HACKED}
window.MBCHC.LAST_HACKED = null
window.ChatRoomTryToTakeSuitcase()
}
return(result)
}
window.ChatRoomSendChat = function() {
let input = window.ElementValue("InputChat")
if (!input.startsWith("@@@")) {
input = input.replace(/^@@/, "/activity ")
input = input.replace(/^@/, "/myself ")
window.ElementValue("InputChat", input)
}
return(window.MBCHC.orig_ChatRoomSendChat())
}
// 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 }
})
// Actions
window.CommandCombine(window.MBCHC.COMMANDS)
if (window.bcModSdk) window.bcModSdk.registerMod("MBCHC", window.MBCHC.VERSION)
window.MBCHC.LOADED = true
console.info(window.MBCHC.log("loaded version " + window.MBCHC.VERSION))
}
return(window.MBCHC.orig_AsylumGGTSSAddItems())
} // Loader
// 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 */
})()

667
mbchc-local.user.js Normal file
View File

@ -0,0 +1,667 @@
// ==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==
(function() {
"use strict";
if (!window.AsylumGGTSSAddItems) throw "AsylumGGTSSAddItems() not found, aborting MBCHC loading"
if (window.MBCHC) throw "MBCHC found, aborting loading"
window.MBCHC = {
VERSION: "trunk",
TARGET_VERSION: "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 target
"whisper": {Ears: {others: "Whisper"}},
"choke": {Neck: {all: "Choke"}},
"brush": {Head: {all: "TakeCare"}},
"french": {Mouth: {others: "FrenchKiss"}},
"sit": {Legs: {others: "Sit"}},
"rim": {Butt: {others: "MasturbateTongue"}},
"press": {Butt: {others: "Step"}},
"rest": {Torso: {others: "Step"}},
"pet": {Head: {all: "Pet"}},
"boop": {Nose: {all: "Pet"}},
"cuddle": {Arms: {others: "Cuddle"}},
"nuzzle": {Nose: {others: "Cuddle"}},
"grab": {Arms: {others: "Grope"}},
"clean": {Mouth: {all: "Caress"}},
"lap": {Legs: {others: "RestHead"}},
"lean": {Breast: {others: "RestHead"}},
"peck": {Mouth: {others: "PoliteKiss"}},
// action zone target
"item": {
"Breast,Butt,Feet,Legs": {all: "SpankItem|TickleItem|RubItem|RollItem|MasturbateItem|PourItem|ShockItem|Inject"},
"Nipples,Pelvis": {all: "SpankItem|TickleItem|RubItem|RollItem|MasturbateItem|PourItem|ShockItem"},
Arms: {all: "SpankItem|TickleItem|RubItem|RollItem|PourItem|ShockItem|Inject"},
Boots: {all: "SpankItem|TickleItem|RubItem|RollItem|PourItem|ShockItem"},
"Ears,Mouth": {all: "TickleItem|RubItem|RollItem"},
"Hood,Nose": {all: "TickleItem|RubItem"},
Neck: {all: "TickleItem|RubItem|RollItem|Inject"},
Torso: {all: "SpankItem|TickleItem|RubItem|RollItem|PourItem|ShockItem"},
Vulva: {all: "SpankItem|TickleItem|RubItem|MasturbateItem|ShockItem"},
VulvaPiercings: {all: "SpankItem|TickleItem|RubItem|MasturbateItem|ShockItem|Inject"},
},
"kiss": {
Mouth: {others: "GagKiss|Kiss|GaggedKiss"},
"Boots,Hands": {self: "PoliteKiss", others: "PoliteKiss|GaggedKiss"},
"Arms,Breast,Nipples": {self: "Kiss", others: "Kiss|GaggedKiss"},
"Butt,Ears,Feet,Head,Legs,Neck,Nose,Pelvis,Torso,Vulva,VulvaPiercings": {others: "Kiss|GaggedKiss"}
},
"smooch": {"Hands,Boots": {all: "Kiss"}},
"nibble|chew": {"Arms,Hands,Boots,Mouth,Nipples": {all: "Nibble"}, "Ears,Feet,Legs,Neck,Nose,Pelvis,Torse,Vulva,VulvaPiercings": {others: "Nibble"}},
"slap|spank": {"Head,Breast,Vulva,VulvaPiercings": {all: "Slap"}, "Arms,Boots,Butt,Feet,Hands,Legs,Pelvis,Torso": {all: "Spank"}},
"tickle": {"Arms,Boots,Breast,Feet,Legs,Neck,Pelvis,Torso": {all: "Tickle"}},
"massage": {"Arms,Boots,Feet,Legs,Neck,Pelvis,Torso": {all: "MassageHands"}},
"lick": {"Arms,Boots,Breast,Hands,Mouth,Nipples": {all: "Lick"}, "Ears,Feet,Legs,Neck,Nose,Pelvis,Torso,Vulva,VulvaPiercings": {others: "Lick"}},
"suck": {"Nipples,Hands,Boots": {all: "Suck"}},
"bite": {"Arms,Boots,Feet,Hands,Legs,Mouth": {all: "Bite"}, "Breast,Butt,Ears,Head,Neck,Nipples,Nose,Torso": {others: "Bite"}},
"pinch": {"Arms,Ears,Nipples,Nose,Pelvis": {all: "Pinch"}},
"clamp": {Mouth: {all: "HandGag"}, Nose: {all: "Choke"}},
"step": {"Breast,Neck,Pelvis": {others: "Step"}},
"pull": {"Head,Nose,Nipples": {all: "Pull"}},
"grope": {"Butt,Breast": {all: "Grope"}, "Feet,Legs,Pelvis": {others: "Grope"}},
"rub": {"Head,Torso": {others: "Rub"}, Nose: {all: "Rub"}, Legs: {self: "Wiggle"}, Hands: {self: "Caress"}},
"caress": {Hands: {others: "Caress"}, "Arms,Breast,Butt,Ears,Feet,Head,Legs,Neck,Nipples,Nose,Pelvis,Torso,Vulva,VulvaPiercings": {all: "Caress"}},
"polish": {"Hands,Boots": {all: "TakeCare"}},
"foot": {"Head,Nose": {others: "Step"}, "Torso,Boots": {others: "MassageFeet"}, "Vulva,VulvaPiercings": {others: "MasturbateFoot"}},
"fist": {"Vulva,Butt": {all: "MasturbateFist"}},
"fuck": {"Mouth,Vulva,Butt": {others: "PenetrateSlow"}}, //peg?
"pound": {"Mouth,Vulva,Butt": {others: "PenetrateFast"}},
"tongue": {"Vulva,VulvaPiercings": {others: "MasturbateTongue"}},
"finger": {"Breast,Butt,Vulva,VulvaPiercings": {all: "MasturbateHand"}},
},
MAP_ZONES: {
"ItemBoots": ["foot", "feet", "boot", "boots", "shoe", "shoes", "toes", "toenails", "sole", "soles", "heel", "heels"],
"ItemFeet": ["leg", "legs", "ankle", "ankles"],
"ItemLegs": ["hips", "hip", "thighs", "thigh"],
"ItemVulva": ["vulva", "pussy"],
"ItemVulvaPiercings": ["clit", "clitoris"],
"ItemButt": ["butt", "ass"],
"ItemPelvis": ["tummy", "pelvis"],
"ItemTorso": ["body", "torso", "back", "ribs"],
"ItemBreast": ["breast", "breasts", "boob", "boobs", "booby", "boobie", "boobies", "tit", "tits", "titty", "tittie", "titties"],
"ItemNipples": ["nip", "nips", "nipple", "nipples"],
"ItemHands": ["hand", "hands", "fingers", "fingernails", "nails"],
"ItemArms": ["arm", "arms", "elbow", "elbows"],
"ItemNeck": ["neck"],
"ItemMouth": ["mouth", "lip", "lips", "teeth", "tongue", "gag", "cheek", "cheeks"],
"ItemNose": ["nose", "nostrils"],
"ItemEars": ["ear", "ears", "earlobe", "earlobes"],
"ItemHead": ["head", "face", "hair", "eyes", "forehead"],
},
FBC_TESTER_PATCHES: [
[/^\^('s)?( )?/g, "^SourceCharacter$1\\s+"],
[/([^\\])\$/g, "$1\\.?$$"],
],
SUBCOMMANDS_MBCHC: {
"versions": {desc: "show the mod versions across the room", cb: mbchc => mbchc.inform(mbchc.gather_versions().map(c => `<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 => {if (window.Player.OnlineSettings.MBCHC) {delete window.Player.OnlineSettings.MBCHC; mbchc.save_settings()}}},
},
ensure: function(error, callback) {
let result = callback.call(this)
if (!result) throw error
return(result)
},
calculate_maps: function() {
this.DO_DATA = {verbs: {}, zones: {}}
for (let [verbs, data] of Object.entries(this.MAP_ACTIONS)) {
let unwound = {}
for (let [zones, actions] of Object.entries(data)) {
let all = (actions.all) ? actions.all.split("|") : []
let processed = {self: (actions.self) ? actions.self.split("|").concat(all) : all, others: (actions.others) ? actions.others.split("|").concat(all) : all}
for (let zone of zones.split(",")) unwound[`Item${zone}`] = processed
}
for (let verb of verbs.split("|")) this.DO_DATA.verbs[verb] = unwound
}
for (let [ag, zones] of Object.entries(this.MAP_ZONES)) for (let zone of zones) this.DO_DATA.zones[zone] = ag
},
settings: function(setting = null) {
let settings = window.Player.OnlineSettings.MBCHC || {}
return(setting ? settings[setting] : settings)
},
save_settings: function(cb = null) {
if (cb) {
if (!window.Player.OnlineSettings.MBCHC) window.Player.OnlineSettings.MBCHC = {}
cb.call(this, window.Player.OnlineSettings.MBCHC)
}
window.ServerAccountUpdate.QueueData({OnlineSettings: window.Player.OnlineSettings})
},
log: function(level, msg) {console[level]("MBCHC: " + String(msg))},
empty: function(text) {
if (!text) return(true)
if (String(text).trim().length < 1) return(true)
return(false)
},
normalise_message: function(text, options = {}) {
let result = text
if (options.trim) result = result.trim()
if (options.low) result = result.toLocaleLowerCase()
if (options.up) {
let first = result.at(0).toLocaleUpperCase()
let rest = result.slice(1)
result = first + rest
}
if (options.dot && result.match(this.RE_LAST_LETTER)) result = `${result}.`
return(result)
},
tokenise: function(text) { return text.replace(this.RE_SPACES, " ").split(" ") },
inform: function(html, timeout = 60000) { window.ChatRoomSendLocal(`<div class="mbchc">${html}</div>`, 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]) => `<div>/mbchc ${cmd} ${sub.args ? Object.keys(sub.args).join(" ") : ""}: ${sub.desc}</div>`).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("<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
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 => `<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: 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())
}
} // 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 */
})()

814
mbchc.mjs Normal file
View File

@ -0,0 +1,814 @@
export {}
// Zero-width non-joiner, used to break up ligatures, does nothing here, but an empty string is a falsy value
const MISSING_PLAYER_DIALOG = {Tag: 'MISSING PLAYER DIALOG: ', Text: '\u200C'}
if (window.MBCHC) throw new Error('MBCHC found, aborting loading')
window.MBCHC = {
VERSION: 'dev.10',
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 target
whisper: {Ears: {others: 'Whisper'}},
choke: {Neck: {all: 'Choke'}},
brush: {Head: {all: 'TakeCare'}},
french: {Mouth: {others: 'FrenchKiss'}},
sit: {Legs: {others: 'Sit'}},
rim: {Butt: {others: 'MasturbateTongue'}},
press: {Butt: {others: 'Step'}},
rest: {Torso: {others: 'Step'}},
pet: {Head: {all: 'Pet'}},
boop: {Nose: {all: 'Pet'}},
cuddle: {Arms: {others: 'Cuddle'}},
nuzzle: {Nose: {others: 'Cuddle'}},
grab: {Arms: {others: 'Grope'}},
clean: {Mouth: {all: 'Caress'}},
lap: {Legs: {others: 'RestHead'}},
lean: {Breast: {others: 'RestHead'}},
peck: {Mouth: {others: 'PoliteKiss'}},
// Action zone target
item: {
'Breast,Butt,Feet,Legs': {all: 'SpankItem|TickleItem|RubItem|RollItem|MasturbateItem|PourItem|ShockItem|Inject'},
'Nipples,Pelvis': {all: 'SpankItem|TickleItem|RubItem|RollItem|MasturbateItem|PourItem|ShockItem'},
Arms: {all: 'SpankItem|TickleItem|RubItem|RollItem|PourItem|ShockItem|Inject'},
Boots: {all: 'SpankItem|TickleItem|RubItem|RollItem|PourItem|ShockItem'},
'Ears,Mouth': {all: 'TickleItem|RubItem|RollItem'},
'Hood,Nose': {all: 'TickleItem|RubItem'},
Neck: {all: 'TickleItem|RubItem|RollItem|Inject'},
Torso: {all: 'SpankItem|TickleItem|RubItem|RollItem|PourItem|ShockItem'},
Vulva: {all: 'SpankItem|TickleItem|RubItem|MasturbateItem|ShockItem'},
VulvaPiercings: {all: 'SpankItem|TickleItem|RubItem|MasturbateItem|ShockItem|Inject'},
},
kiss: {
Mouth: {others: 'GagKiss|Kiss|GaggedKiss'},
'Boots,Hands': {self: 'PoliteKiss', others: 'PoliteKiss|GaggedKiss'},
'Arms,Breast,Nipples': {self: 'Kiss', others: 'Kiss|GaggedKiss'},
'Butt,Ears,Feet,Head,Legs,Neck,Nose,Pelvis,Torso,Vulva,VulvaPiercings': {others: 'Kiss|GaggedKiss'},
},
smooch: {'Hands,Boots': {all: 'Kiss'}},
'nibble|chew': {'Arms,Hands,Boots,Mouth,Nipples': {all: 'Nibble'}, 'Ears,Feet,Legs,Neck,Nose,Pelvis,Torse,Vulva,VulvaPiercings': {others: 'Nibble'}},
'slap|spank': {'Head,Breast,Vulva,VulvaPiercings': {all: 'Slap'}, 'Arms,Boots,Butt,Feet,Hands,Legs,Pelvis,Torso': {all: 'Spank'}},
tickle: {'Arms,Boots,Breast,Feet,Legs,Neck,Pelvis,Torso': {all: 'Tickle'}},
massage: {'Arms,Boots,Feet,Legs,Neck,Pelvis,Torso': {all: 'MassageHands'}},
lick: {'Arms,Boots,Breast,Hands,Mouth,Nipples': {all: 'Lick'}, 'Ears,Feet,Legs,Neck,Nose,Pelvis,Torso,Vulva,VulvaPiercings': {others: 'Lick'}},
suck: {'Nipples,Hands,Boots': {all: 'Suck'}},
bite: {'Arms,Boots,Feet,Hands,Legs,Mouth': {all: 'Bite'}, 'Breast,Butt,Ears,Head,Neck,Nipples,Nose,Torso': {others: 'Bite'}},
pinch: {'Arms,Ears,Nipples,Nose,Pelvis': {all: 'Pinch'}},
clamp: {Mouth: {all: 'HandGag'}, Nose: {all: 'Choke'}},
step: {'Breast,Neck,Pelvis': {others: 'Step'}},
pull: {'Head,Nose,Nipples': {all: 'Pull'}},
grope: {'Butt,Breast': {all: 'Grope'}, 'Feet,Legs,Pelvis': {others: 'Grope'}},
rub: {'Head,Torso': {others: 'Rub'}, Nose: {all: 'Rub'}, Legs: {self: 'Wiggle'}, Hands: {self: 'Caress'}},
caress: {Hands: {others: 'Caress'}, 'Arms,Breast,Butt,Ears,Feet,Head,Legs,Neck,Nipples,Nose,Pelvis,Torso,Vulva,VulvaPiercings': {all: 'Caress'}},
polish: {'Hands,Boots': {all: 'TakeCare'}},
foot: {'Head,Nose': {others: 'Step'}, 'Torso,Boots': {others: 'MassageFeet'}, 'Vulva,VulvaPiercings': {others: 'MasturbateFoot'}},
fist: {'Vulva,Butt': {all: 'MasturbateFist'}},
fuck: {'Mouth,Vulva,Butt': {others: 'PenetrateSlow'}}, // Peg?
pound: {'Mouth,Vulva,Butt': {others: 'PenetrateFast'}},
tongue: {'Vulva,VulvaPiercings': {others: 'MasturbateTongue'}},
finger: {'Breast,Butt,Vulva,VulvaPiercings': {all: 'MasturbateHand'}},
},
MAP_ZONES: {
ItemBoots: ['foot', 'feet', 'boot', 'boots', 'shoe', 'shoes', 'toes', 'toenails', 'sole', 'soles', 'heel', 'heels'],
ItemFeet: ['leg', 'legs', 'ankle', 'ankles'],
ItemLegs: ['hips', 'hip', 'thighs', 'thigh'],
ItemVulva: ['vulva', 'pussy'],
ItemVulvaPiercings: ['clit', 'clitoris'],
ItemButt: ['butt', 'ass'],
ItemPelvis: ['tummy', 'pelvis'],
ItemTorso: ['body', 'torso', 'back', 'ribs'],
ItemBreast: ['breast', 'breasts', 'boob', 'boobs', 'booby', 'boobie', 'boobies', 'tit', 'tits', 'titty', 'tittie', 'titties'],
ItemNipples: ['nip', 'nips', 'nipple', 'nipples'],
ItemHands: ['hand', 'hands', 'fingers', 'fingernails', 'nails'],
ItemArms: ['arm', 'arms', 'elbow', 'elbows'],
ItemNeck: ['neck'],
ItemMouth: ['mouth', 'lip', 'lips', 'teeth', 'tongue', 'gag', 'cheek', 'cheeks'],
ItemNose: ['nose', 'nostrils'],
ItemEars: ['ear', 'ears', 'earlobe', 'earlobes'],
ItemHead: ['head', 'face', 'hair', 'eyes', 'forehead'],
},
FBC_TESTER_PATCHES: [
[/^\^('s)?( )?/g, '^SourceCharacter$1\\s+'],
[/([^\\])\$/g, '$1\\.?$$'],
],
SUBCOMMANDS_MBCHC: {
// versions: {desc: 'show the mod versions across the room', cb: mbchc => mbchc.inform(mbchc.gather_versions().map(c => `<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'}`)}, // eslint-disable-line no-cond-assign
// 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) {
if (window.Player.OnlineSettings.MBCHC) {
delete window.Player.OnlineSettings.MBCHC
mbchc.save_settings()
}
}},
},
ensure(error, callback) {
const result = callback.call(this)
if (!result) throw error
return (result)
},
calculate_maps() {
this.DO_DATA = {verbs: {}, zones: {}}
for (const [verbs, data] of Object.entries(this.MAP_ACTIONS)) {
const unwound = {}
for (const [zones, actions] of Object.entries(data)) {
const all = (actions.all) ? actions.all.split('|') : []
const processed = {self: (actions.self) ? actions.self.split('|').concat(all) : all, others: (actions.others) ? actions.others.split('|').concat(all) : all}
for (const zone of zones.split(',')) unwound[`Item${zone}`] = processed
}
for (const verb of verbs.split('|')) this.DO_DATA.verbs[verb] = unwound
}
for (const [ag, zones] of Object.entries(this.MAP_ZONES)) for (const zone of zones) this.DO_DATA.zones[zone] = ag
},
settings(setting = null) {
const settings = window.Player.OnlineSettings.MBCHC || {}
return (setting ? settings[setting] : settings)
},
save_settings(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(level, message) {
console[level]('MBCHC: ' + String(message))
},
empty(text) {
if (!text) return (true)
if (String(text).trim().length === 0) return (true)
return (false)
},
normalise_message(text, options = {}) {
let result = text
if (options.trim) result = result.trim()
if (options.low) result = result.toLocaleLowerCase()
if (options.up) {
const first = result.at(0).toLocaleUpperCase()
const rest = result.slice(1)
result = first + rest
}
if (options.dot && this.RE_LAST_LETTER.test(result)) result = `${result}.`
return (result)
},
tokenise(text) {
return text.replace(this.RE_SPACES, ' ').split(' ')
},
inform(html, timeout = 60_000) {
window.ChatRoomSendLocal(`<div class="mbchc">${html}</div>`, timeout)
},
report(x) {
this.inform(`Error: ${x.toString()}`)
if (this.RETHROW) throw x
},
in(x, floor, ceiling) {
return ((x >= floor) && (x <= ceiling))
},
cid2char(cid) {
cid = Number.parseInt(cid, 10)
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(pos) {
if (!this.in(pos, 0, window.ChatRoomCharacter.length - 1)) throw new Error(`invalid position ${pos}`)
return (window.ChatRoomCharacter[pos])
},
rel2char(target) {
const 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 (this.RE_ALL_LEFT.test(target)) pos = me - target.length
if (this.RE_ALL_RIGHT.test(target)) pos = me + target.length
if (pos === null) throw new Error(`failed to parse target "${target}"`)
pos %= window.ChatRoomCharacter.length
if (pos < 0) pos += window.ChatRoomCharacter.length
return (this.pos2char(pos))
},
target2char(target) { // Target should be lowcase
const input = target
if (this.empty(target)) return (window.Player)
const int = Number.parseInt(target, 10)
target = String(target)
let found = []
if (target.startsWith('=')) return (this.cid2char(target.slice(1)))
if (target.startsWith('<') || target.startsWith('>')) return (this.rel2char(target))
if (!Number.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().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 && (c.Nickname.toLocaleLowerCase().includes(target)))) // eslint-disable-line unicorn/no-array-push-push
const map = {}
for (const c of found) {
if (!map[c.cid]) map[c.cid] = c
}
found = Object.values(map)
if (found.length === 0) throw new Error(`target "${input}": no match`)
if (found.length > 1) throw new Error(`target "${input}": multiple matches (${found.map(c => `${c.cid}|${c.Name}|${c.Nickname || c.Name}`).join(',')})`)
return (found[0])
},
char2targets(char) {
const [result, cid] = [new Set(), char.cid.toString()]
result.add(cid).add(`=${cid}`)
for (const t of this.tokenise(char.Name)) {
result.add(t)
result.add(`@${t}`)
}
if (char.Nickname) for (const t of this.tokenise(char.Nickname)) {
result.add(t)
result.add(`@${t}`)
}
return result
},
donate_data(target) {
const char = this.target2char(target)
if (char.IsPlayer()) throw new Error('target must not be you')
if (!char.IsRestrained()) throw new Error('target must be bound')
const cost = Math.round(((Math.random() * 10) + 15))
if (window.Player.Money < cost) throw new Error('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: [MISSING_PLAYER_DIALOG]})
},
run_activity(char, ag, action) {
try {
if (!window.ActivityAllowed()) throw new Error('activities disabled in this room')
if (!window.ServerChatRoomGetAllowItem(window.Player, char)) throw new Error('no permissions')
char.FocusGroup = this.ensure('invalid AssetGroup', () => window.AssetGroupGet(char.AssetFamily, ag))
const 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(window.Player, char, char.FocusGroup, activity)
} finally {
char.FocusGroup = null
}
},
replace_me(match, offset, string) {
const text = string.slice(1)
let suffix = ' '
if (text.startsWith('\'') || text.startsWith(' ')) suffix = ''
return `${window.MBCHC.PREF_ACTIVITY}<${window.Player.cid}:>SourceCharacter${suffix}`
},
cid2dict(type, cid) {
return ({Tag: `${type}Character`, MemberNumber: cid, Text: this.cid2char(cid).dn})
},
send_activity(message) {
const dict = [MISSING_PLAYER_DIALOG]
const cids = message.match(this.RE_ACT_CIDS)
if (cids) {
message = message.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: message, Dictionary: dict})
},
receive(data) {
const char = this.cid2char(data.Sender)
if (char.IsPlayer()) return true // This is our own message, sent back to us
const payload = this.ensure('Empty message', () => data.Dictionary[0])
switch (payload.type) {
case 'greetings': case 'hello': {
char.MBCHC = payload.value
if (payload.type === 'greetings') this.hello(char)
break
}
default: // If we don't know the type it may be from a newer version
}
return true
},
hello(char = null) {
const payload = {type: 'greetings', value: window.Player.MBCHC}
if (char) payload.type = 'hello'
const 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')
// if (!item || !item.Asset || !item.Asset.Name) throw new Error('butt seems empty')
// if (item.Asset.Name !== 'AnalHook') throw new Error('butt seems occupied by something other than the anal hook')
// if (!item.Property.Type || item.Property.Type !== 'Hair') throw new Error('anal hook seems not tied to hair')
// item.Property = {Type: 'Hair', Hide: this.HIDE_ALL}
// window.CharacterRefresh(window.Player, true, true)
// },
// title(title) { // WIP
// if (this.empty(title)) throw new Error('empty title')
// title = this.normalise_message(title, {trim: true, up: true, low: true})
// if (title.length > 16) throw new Error('title too long')
// if (!this.RE_TITLE.test(title)) throw new Error('invalid title')
// window.TitleSet(title)
// // Window.TitleList.push({Name: title, Requirement: () => true}) // check for existing first
// },
copy_fbc_trigger(trigger) {
const 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() {
this.remove_fbc_hook()
delete this.remove_fbc_hook
window.bce_ActivityTriggers.push(...window.bce_ActivityTriggers.filter(t => t.Type === 'Emote').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, because it needs a semicolon in front if it
let cmd = window.Commands.find(c => c.Tag === 'anim')
if (cmd) cmd.AutoComplete = this.complete_fbc_anim
cmd = window.Commands.find(c => c.Tag === 'pose')
if (cmd) cmd.AutoComplete = this.complete_fbc_pose
},
// gather_versions() {
// return (window.ChatRoomCharacter.filter(c => c.MBCHC).map(c => ({name: c.dn, cid: c.cid, version: c.MBCHC.VERSION})))
// },
find_timezone(char) {
const timezones = this.settings('timezones')
if (timezones && typeof timezones[char.cid] === 'number') return (timezones[char.cid])
const match = (char.Description) ? char.Description.match(this.RE_TZ) : null
const int = match ? Number.parseInt(match[1] + match[2], 10) : 42
if (this.in(int, -12, 12)) return (int)
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], 10)
if (Number.isNaN(tz)) throw new Error(`invalid offset "${args[0]}"`)
if (!this.in(tz, -12, 12)) throw new Error('offset should be [-12,12]')
const 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(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(argline, cmdline, args) {
const mbchc = window.MBCHC
try { // `this` is command object
if (args.length === 0) 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())
const sub = mbchc.ensure(`unknown subcommand "${cmd}"`, () => mbchc.SUBCOMMANDS_MBCHC[cmd])
sub.cb.call(mbchc, mbchc, args, argline, cmdline)
} catch (error) {
mbchc.report(error)
}
},
command_activity(argline, cmdline, _) {
const mbchc = window.MBCHC
if (!mbchc.empty(argline)) {
try { // `this` is command object
const message = mbchc.normalise_message(cmdline.replace(mbchc.RE_ACTIVITY, ''), {trim: true, dot: true, up: true})
mbchc.send_activity(message)
} catch (error) {
mbchc.report(error)
}
}
},
command_do(argline, cmdline, args) {
const mbchc = window.MBCHC
try { // `this` is command object
if (args.length === 0) 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.ensure(`unknown verb "${verb}"`, () => mbchc.DO_DATA.verbs[verb])
if (Object.keys(zones).length === 1) {
if (!target) target = zone
zone = mbchc.MAP_ZONES[Object.keys(zones)[0]][0]
}
if (!zone) throw new Error('zone missing')
const ag = mbchc.ensure(`unknown zone "${zone}"`, () => mbchc.DO_DATA.zones[zone])
const types = mbchc.ensure(`zone "${zone}" invalid for "${verb}"`, () => zones[ag])
let char = window.Player
if (target && ((types.self.length === 0) || (types.others.length > 0))) char = mbchc.target2char(target)
const type = char.IsPlayer() ? 'self' : 'others'
const available = window.ActivityAllowedForGroup(char, ag)
const 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 = mbchc.ensure(`zone "${zone}" invalid for ("${verb}" "${type}")`, () => types[type])
const 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 (error) {
mbchc.report(error)
}
},
bell() {
setTimeout(() => {
document.querySelector('#InputChat').style.outline = ''
}, 100)
document.querySelector('#InputChat').style.outline = 'solid red'
},
complete(options, space = true) {
if (options.length === 0) return (this.bell())
if (options.length > 1) {
const width = Math.max(...options.map(o => o.length))
let pref = null
for (let i = width; i > 0; i -= 1) {
const 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.querySelector('#InputChat').value.replace(this.RE_LAST_WORD, `$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'))
const rescroll = window.ElementIsScrolledToEnd('TextAreaChatLog')
window.ChatRoomResize(false)
if (rescroll) window.ElementScrollToEnd('TextAreaChatLog')
},
comp_hint_visible() {
return (this.COMP_HINT.parentElement && this.COMP_HINT.style.display === 'flex')
},
complete_hint_hide() {
if (!this.comp_hint_visible()) return
this.COMP_HINT.style.display = 'none'
window.ChatRoomResize(false)
},
complete_target(token, me2 = true, check_perms = false) {
const [locase, found] = [token.toLocaleLowerCase(), new Set()]
for (const c of window.ChatRoomCharacter) {
if ((c.IsPlayer() && !me2) || (check_perms && !window.ServerChatRoomGetAllowItem(window.Player, c))) continue
for (const s of this.char2targets(c)) {
if (s.toLocaleLowerCase().startsWith(locase)) found.add(s)
}
}
this.complete(Array.from(found))
},
complete_common() {
const input = document.querySelector('#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 === 0) return
if (tokens.length < 2) return (mbchc.complete([`${mbchc.CommandsKey}${this.Tag}`]))
const subname = tokens[1].toLocaleLowerCase()
if (tokens.length < 3) return (mbchc.complete(Object.keys(mbchc.SUBCOMMANDS_MBCHC).filter(c => c.startsWith(subname)))) // Complete subcommand name
const sub = mbchc.SUBCOMMANDS_MBCHC[subname]
if (sub && sub.args) {
const argname = Object.keys(sub.args)[tokens.length - 3]
if (argname === 'TARGET') return (mbchc.complete_target(tokens.at(-1), false))
if (argname === '[TARGET]') return (mbchc.complete_target(tokens.at(-1)), true)
}
},
complete_do_target(actions, token) {
if (!actions) return
const me2 = (actions.self.length > 0)
if (me2 && actions.others.length === 0) 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 === 0) 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
const 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
const 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 === 0) return
if (tokens.length < 2) return (mbchc.complete([`${mbchc.CommandsKey}${this.Tag}`]))
if (tokens.length > 2) return (mbchc.bell())
const 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 === 0) return
if (tokens.length < 2) return (mbchc.complete([`${mbchc.CommandsKey}${this.Tag}`]))
const pose = tokens.at(-1).toLocaleLowerCase()
return (mbchc.complete(window.PoseFemale3DCG.map(p => p.Name).filter(p => p.toLocaleLowerCase().startsWith(pose))))
},
history(down) {
const [text, history] = [window.ElementValue('InputChat'), window.ChatRoomLastMessage]
if (!this.HISTORY_MODE) {
history.push(text)
this.HISTORY_MODE = true
}
const ids = history.map((t, i) => [t, i]).filter(([t, _]) => t.startsWith(history.at(-1))).map(([_, i]) => i)
if (!down) ids.reverse()
const 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_checks() { // we only want to catch chatlog and canvas (no map though) keypresses
if (document.activeElement === document.body) return true
if (document.activeElement.id !== 'MainCanvas') return false
return !window.ChatRoomMapVisible
},
focus_chat_whitelist(event) {
if (event.ctrlKey && event.key === 'v') return true // Ctrl+V should paste
return false
},
focus_chat(event) {
if (event.repeat) return // Only unique presses please
if (!this.focus_chat_checks()) return
if ([event.altKey, event.ctrlKey, event.metaKey].some(Boolean) && !this.focus_chat_whitelist(event)) return // Alt, ctrl and meta should all be false
if (document.querySelector('#InputChat')?.style.display !== 'inline') return // Input chat missing
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 = new RegExp(`^${this.CommandsKey}activity `)
this.PREF_ACTIVITY = `${this.CommandsKey}activity `
this.COMP_HINT = document.createElement('div')
this.COMP_HINT.id = 'mbchcCompHint'
const css = document.createElement('style')
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.append(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.at(-1) === history.at(-2))) {
history.pop()
window.ChatRoomLastMessageIndex -= 1
}
})
this.before('ChatRoomCharacterViewDrawOverlay', (C, CharX, CharY, Zoom, _Pos) => {
// if (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 && typeof C.MBCHC_LOCAL.TZ === 'number') {
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) => ID === 'bce_LayerPriority' && 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.append(this.COMP_HINT))
this.before('ChatRoomClearAllElements', () => {
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.querySelector('#InputChat') && document.querySelector('#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
const [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, message, metadata) => {
data.MBCHC_ID = this.NEXT_MESSAGE
this.NEXT_MESSAGE += 1
if (this.LOG_MESSAGES) console.debug({data, sender, msg: message, metadata})
}})
window.ChatRoomRegisterMessageHandler({Priority: -219, Description: 'MBCHC room enter hook',
Callback: (data, _sender, _message, _metadata) => {
if ((data.Type === 'Action') && (data.Content === 'ServerEnter') && (data.Sender === window.Player.cid)) this.player_enters_room()
},
})
window.ChatRoomRegisterMessageHandler({Priority: -219, Description: 'MBCHC specific consumer',
Callback: (data, _sender, _message, _metadata) => {
if ((data.Type === 'Hidden') && (data.Content === 'MBCHC')) return this.receive(data)
},
})
window.ChatRoomRegisterMessageHandler({Priority: -219, Description: 'MBCHC autohack lookup',
Callback: (data, _sender, _message, _metadata) => {
if ((data.Type === 'Hidden') && (data.Content === 'ReceiveSuitcaseMoney')) this.LAST_HACKED = data.Sender
},
})
// Footer
this.LOADED = true
this.log('info', `loaded version ${this.VERSION}`)
if ((window.CurrentModule === 'Online') && (window.CurrentScreen === 'ChatRoom')) {
for (const c of window.ChatRoomCharacter) this.update_char(c)
this.player_enters_room()
}
},
preloader() {
if (!window.AsylumGGTSSAddItems) throw new Error('AsylumGGTSSAddItems() not found, aborting MBCHC loading')
if (!window.bcModSdk) throw new Error('SDK not found, please load with (or after) FUSAM')
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 (error) {
console.error(error)
}
return next(nextargs)
})
this.after = (name, cb) => this.SDK.hookFunction(name, 0, (nextargs, next) => {
const result = next(nextargs);
try {
cb?.(...nextargs)
} catch (error) {
console.error(error)
}
return result
})
if (window.CurrentModule && window.CurrentScreen && !(window.CurrentModule === 'Character' && window.CurrentScreen === 'Login')) return this.loader()
this.remove_load_hook = this.before('AsylumGGTSSAddItems', () => this.loader())
},
}
window.MBCHC.preloader()
// TODO: with the removal of /mbchc versions it makes no sense to keep track of the mod version and the whole hello() infrastructure can go away

5404
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

32
package.json Normal file
View File

@ -0,0 +1,32 @@
{
"name": "mbchc",
"version": "0.0.9",
"description": "Mute's Bondage Club Hacks Collection",
"type": "module",
"devDependencies": {
"xo": "^0.56.0"
},
"license": "SEE LICENSE IN LICENSE.",
"xo": {
"env": [
"browser",
"node"
],
"rules": {
"camelcase": "off",
"capitalized-comments": "off",
"curly": "off",
"max-params": "off",
"new-cap": "off",
"no-return-assign": "off",
"no-unused-vars": ["error", {
"argsIgnorePattern": "^_",
"destructuredArrayIgnorePattern": "^_"
}],
"padding-line-between-statements": "off",
"semi": "off",
"unicorn/no-array-reduce": "off",
"fake/fuck-commas": "off"
}
}
}

35
server.js Normal file
View File

@ -0,0 +1,35 @@
import {readFileSync} from 'node:fs'
import {createServer} from 'node:http'
function stamp_cors(rx) {
rx.setHeader('Access-Control-Max-Age', '86400')
rx.setHeader('Access-Control-Allow-Private-Network', 'true')
rx.setHeader('Access-Control-Allow-Origin', '*')
rx.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS')
rx.setHeader('Access-Control-Allow-Headers', '*')
// rx.setHeader('Access-Control-Allow-Credentials', 'false') // omit this header to disallow
}
const server = createServer((rq, rx) => {
switch (rq.method) {
case 'GET': {
rx.statusCode = 200
stamp_cors(rx)
rx.setHeader('Content-Type', 'text/javascript')
rx.setHeader('Cache-Control', 'no-cache')
const data = readFileSync('./mbchc.mjs')
rx.write(data)
break
}
case 'OPTIONS': {
rx.statusCode = 204
stamp_cors(rx)
break
}
default: {
rx.statusCode = 400
}
}
rx.end()
})
server.listen(9696, '127.0.0.1', () => console.log('Server started.'))