668 lines
38 KiB
JavaScript
668 lines
38 KiB
JavaScript
// ==UserScript==
|
|
// @name MBCHC-local
|
|
// @version trunk
|
|
// @description Mute's Bondage Club Hacks Collection (development version)
|
|
// @author codename.mute@proton.me
|
|
// @namespace https://code.fleshless.org/mute/
|
|
// @homepage https://code.fleshless.org/mute/MBCHC
|
|
// @updateURL https://code.fleshless.org/mute/MBCHC/raw/branch/master/mbchc-local.user.js
|
|
// @downloadURL https://code.fleshless.org/mute/MBCHC/raw/branch/master/mbchc-local.user.js
|
|
// @match https://bondageprojects.elementfx.com/R*
|
|
// @match https://www.bondageprojects.elementfx.com/R*
|
|
// @match https://bondage-europe.com/R*
|
|
// @match https://www.bondage-europe.com/R*
|
|
// @match http://localhost:*/*
|
|
// @match http://127.0.0.1:*/*
|
|
// @grant none
|
|
// ==/UserScript==
|
|
|
|
(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 */
|
|
})()
|