dev.12: R105 fixes

a massive diff due to infrastructure work
the completion pane moved to the left of chat
no other intended changes for users
This commit is contained in:
Mute 2024-06-29 17:09:25 +00:00
parent b0961f4fb8
commit 90231cb2ae
6 changed files with 1633 additions and 1202 deletions

18
jsconfig.json Normal file
View File

@ -0,0 +1,18 @@
{
"include": [
"node_modules/bc-stubs/bc/**/*.d.ts",
"node_modules/bondage-club-mod-sdk/dist/**/*.d.ts",
"typedef.d.ts",
"mbchc.mjs",
"server.js"
],
"compilerOptions": {
"lib": [
"es2022",
"DOM"
],
"checkJs": true,
"strict": false,
"noImplicitOverride": true
}
}

490
mbchc.mjs
View File

@ -1,11 +1,45 @@
export {} export {}
// Zero-width non-joiner, used to break up ligatures, does nothing here, but an empty string is a falsy value /** @typedef {import('bondage-club-mod-sdk').ModSDKGlobalAPI} ModSDKGlobalAPI */
/** @type {Window & typeof globalThis & {MBCHC?: any, bcModSdk?: ModSDKGlobalAPI, bce_ActivityTriggers?: any, bce_EventExpressions?: any}} */
const w = window
/**
* A silly helper to memorise values in callbacks
* @template V, R
* @param {V} v Value to memorise
* @param {function(V): R} cb Callback
* @returns {R} Return value of the callback
*/
const take = (v, cb) => cb(v)
/**
* Takes a DOM query and passes the element's style into a callback, returning its result
* @template T
* @param {string} q Query
* @param {function(CSSStyleDeclaration): T} cb Callback
* @returns {T | void} Return value of the callback, if it was called
*/
const style = (q, cb) => take(document.querySelector(q), E => E && E instanceof HTMLElement && E.style ? cb(E.style) : undefined)
/**
* @returns {string} Current view
*/
const current = () => `${w.CurrentModule}/${w.CurrentScreen}`
/**
* @param {Character} char
* @returns {number}
*/
const cid = char => char.MemberNumber
// Zero-width non-joiner, used to break up ligatures, does nothing here, but an empty string is a falsey value
const MISSING_PLAYER_DIALOG = {Tag: 'MISSING TEXT IN "Interface.csv": ', Text: '\u200C'} const MISSING_PLAYER_DIALOG = {Tag: 'MISSING TEXT IN "Interface.csv": ', Text: '\u200C'}
if (window.MBCHC) throw new Error('MBCHC found, aborting loading') if (w.MBCHC) throw new Error('MBCHC found, aborting loading')
window.MBCHC = { w.MBCHC = {
VERSION: 'dev.11', VERSION: 'dev.12',
NEXT_MESSAGE: 1, NEXT_MESSAGE: 1,
LOG_MESSAGES: false, LOG_MESSAGES: false,
RETHROW: false, RETHROW: false,
@ -13,7 +47,6 @@ window.MBCHC = {
AUTOHACK_ENABLED: false, AUTOHACK_ENABLED: false,
LAST_HACKED: null, LAST_HACKED: null,
HISTORY_MODE: false, HISTORY_MODE: false,
// RE_TITLE: /^[a-zA-Z]+$/,
RE_PREF_ACTIVITY_ME: /^@/, RE_PREF_ACTIVITY_ME: /^@/,
RE_PREF_ACTIVITY: /^@@/, RE_PREF_ACTIVITY: /^@@/,
RE_ACT_CIDS: /^<(\d+)?:(\d+)?>/, RE_ACT_CIDS: /^<(\d+)?:(\d+)?>/,
@ -26,62 +59,6 @@ window.MBCHC = {
RGB_MUTE: '#6c2132', RGB_MUTE: '#6c2132',
RGB_POLLY: '#81b1e7', RGB_POLLY: '#81b1e7',
UTC_OFFSET: new Date().getTimezoneOffset() * 60 * 1000, 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 MAP_ACTIONS: { // ActivityFemale3DCG
// action // action
'nod|yes': {Head: {self: 'Nod'}}, 'nod|yes': {Head: {self: 'Nod'}},
@ -183,22 +160,19 @@ window.MBCHC = {
[/([^\\])\$/g, '$1\\.?$$'], [/([^\\])\$/g, '$1\\.?$$'],
], ],
SUBCOMMANDS_MBCHC: { 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 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])}, 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)}, 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) { 'purge!': {desc: 'delete MBCHC online saved data', cb(mbchc) {
if (window.Player.OnlineSettings.MBCHC) { if (w.Player.OnlineSettings.MBCHC) {
delete window.Player.OnlineSettings.MBCHC delete w.Player.OnlineSettings.MBCHC
mbchc.save_settings() mbchc.save_settings()
} }
}}, }},
}, },
ensure(error, callback) { ensure(text, callback) {
const result = callback.call(this) const result = callback.call(this)
if (!result) throw error if (!result) throw new Error(text)
return (result) return (result)
}, },
calculate_maps() { calculate_maps() {
@ -215,15 +189,15 @@ window.MBCHC = {
for (const [ag, zones] of Object.entries(this.MAP_ZONES)) for (const zone of zones) this.DO_DATA.zones[zone] = ag for (const [ag, zones] of Object.entries(this.MAP_ZONES)) for (const zone of zones) this.DO_DATA.zones[zone] = ag
}, },
settings(setting = null) { settings(setting = null) {
const settings = window.Player.OnlineSettings.MBCHC || {} const settings = w.Player.OnlineSettings.MBCHC || {}
return (setting ? settings[setting] : settings) return (setting ? settings[setting] : settings)
}, },
save_settings(cb = null) { save_settings(cb = null) {
if (cb) { if (cb) {
if (!window.Player.OnlineSettings.MBCHC) window.Player.OnlineSettings.MBCHC = {} if (!w.Player.OnlineSettings.MBCHC) w.Player.OnlineSettings.MBCHC = {}
cb.call(this, window.Player.OnlineSettings.MBCHC) cb.call(this, w.Player.OnlineSettings.MBCHC)
} }
window.ServerAccountUpdate.QueueData({OnlineSettings: window.Player.OnlineSettings}) w.ServerAccountUpdate.QueueData({OnlineSettings: w.Player.OnlineSettings})
}, },
log(level, message) { log(level, message) {
console[level]('MBCHC: ' + String(message)) console[level]('MBCHC: ' + String(message))
@ -249,37 +223,37 @@ window.MBCHC = {
return text.replace(this.RE_SPACES, ' ').split(' ') return text.replace(this.RE_SPACES, ' ').split(' ')
}, },
inform(html, timeout = 60_000) { inform(html, timeout = 60_000) {
window.ChatRoomSendLocal(`<div class="mbchc">${html}</div>`, timeout) w.ChatRoomSendLocal(`<div class="mbchc">${html}</div>`, timeout)
}, },
report(x) { report(x) {
this.inform(`Error: ${x.toString()}`) this.inform(`${x.toString()}`)
if (this.RETHROW) throw x if (this.RETHROW) throw x
}, },
in(x, floor, ceiling) { in(x, floor, ceiling) {
return ((x >= floor) && (x <= ceiling)) return ((x >= floor) && (x <= ceiling))
}, },
cid2char(cid) { cid2char(id) {
cid = Number.parseInt(cid, 10) id = Number.parseInt(id, 10)
if (cid === window.Player.cid) return (window.Player) if (id === cid(w.Player)) return (w.Player)
return (this.ensure(`character ${cid} not found in the room`, () => window.ChatRoomCharacter.find(c => c.cid === cid))) return (this.ensure(`character ${id} not found in the room`, () => w.ChatRoomCharacter.find(c => cid(c) === id)))
}, },
pos2char(pos) { pos2char(pos) {
if (!this.in(pos, 0, window.ChatRoomCharacter.length - 1)) throw new Error(`invalid position ${pos}`) if (!this.in(pos, 0, w.ChatRoomCharacter.length - 1)) throw new Error(`invalid position ${pos}`)
return (window.ChatRoomCharacter[pos]) return (w.ChatRoomCharacter[pos])
}, },
rel2char(target) { 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 const me = this.ensure('can\'t find my position', () => w.ChatRoomCharacter.findIndex(char => char.IsPlayer()) + 1) - 1 // 0 is falsey, but is valid index
let pos = null let pos = null
if (this.RE_ALL_LEFT.test(target)) pos = me - target.length if (this.RE_ALL_LEFT.test(target)) pos = me - target.length
if (this.RE_ALL_RIGHT.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}"`) if (pos === null) throw new Error(`failed to parse target "${target}"`)
pos %= window.ChatRoomCharacter.length pos %= w.ChatRoomCharacter.length
if (pos < 0) pos += window.ChatRoomCharacter.length if (pos < 0) pos += w.ChatRoomCharacter.length
return (this.pos2char(pos)) return (this.pos2char(pos))
}, },
target2char(target) { // Target should be lowcase target2char(target) { // Target should be lowcase
const input = target const input = target
if (this.empty(target)) return (window.Player) if (this.empty(target)) return (w.Player)
const int = Number.parseInt(target, 10) const int = Number.parseInt(target, 10)
target = String(target) target = String(target)
let found = [] let found = []
@ -289,25 +263,25 @@ window.MBCHC = {
if (this.in(int, 0, 9)) return (this.pos2char(int)) 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, 11, 15)) return (this.pos2char(int - 11))
if (this.in(int, 21, 25)) return (this.pos2char(int - 16)) if (this.in(int, 21, 25)) return (this.pos2char(int - 16))
found.push(...window.ChatRoomCharacter.filter(c => c.cid.toString().includes(target))) found.push(...w.ChatRoomCharacter.filter(c => cid(c).toString().includes(target)))
} }
if (target.startsWith('@')) target = target.slice(1) if (target.startsWith('@')) target = target.slice(1)
found.push(...window.ChatRoomCharacter.filter(c => c.Name.toLocaleLowerCase().includes(target))) found.push(...w.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 found.push(...w.ChatRoomCharacter.filter(c => c.Nickname && (c.Nickname.toLocaleLowerCase().includes(target)))) // eslint-disable-line unicorn/no-array-push-push
const map = {} const map = {}
for (const c of found) { for (const c of found) {
if (!map[c.cid]) map[c.cid] = c if (!map[cid(c)]) map[cid(c)] = c
} }
found = Object.values(map) found = Object.values(map)
if (found.length === 0) throw new Error(`target "${input}": no match`) 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(',')})`) if (found.length > 1) throw new Error(`target "${input}": multiple matches (${found.map(c => `${cid(c)}|${c.Name}|${c.Nickname || c.Name}`).join(',')})`)
return (found[0]) return (found[0])
}, },
char2targets(char) { char2targets(char) {
const [result, cid] = [new Set(), char.cid.toString()] const [result, id] = [new Set(), cid(char).toString()]
result.add(cid).add(`=${cid}`) result.add(id).add(`=${id}`)
for (const t of this.tokenise(char.Name)) { for (const t of this.tokenise(char.Name)) {
result.add(t) result.add(t)
result.add(`@${t}`) result.add(`@${t}`)
@ -325,21 +299,22 @@ window.MBCHC = {
if (char.IsPlayer()) throw new Error('target must not be you') if (char.IsPlayer()) throw new Error('target must not be you')
if (!char.IsRestrained()) throw new Error('target must be bound') if (!char.IsRestrained()) throw new Error('target must be bound')
const cost = Math.round(((Math.random() * 10) + 15)) const cost = Math.round(((Math.random() * 10) + 15))
if (window.Player.Money < cost) throw new Error('not enough money') if (w.Player.Money < cost) throw new Error('not enough money')
window.CharacterChangeMoney(window.Player, -cost) w.CharacterChangeMoney(w.Player, -cost)
window.ServerSend('ChatRoomChat', {Content: 'ReceiveSuitcaseMoney', Type: 'Hidden', Target: char.cid}) w.ServerSend('ChatRoomChat', {Content: 'ReceiveSuitcaseMoney', Type: 'Hidden', Target: cid(char)})
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]}) w.ChatRoomMessage({Sender: cid(w.Player), Type: 'Action', Content: `You've bought data for $${cost} and sent it to ${char.dn}.`, Dictionary: [MISSING_PLAYER_DIALOG]})
}, },
run_activity(char, ag, action) { run_activity(char, ag, action) {
try { try {
if (!window.ActivityAllowed()) throw new Error('activities disabled in this room') if (!w.ActivityAllowed()) throw new Error('activities disabled in this room')
if (!window.ServerChatRoomGetAllowItem(window.Player, char)) throw new Error('no permissions') if (!w.ServerChatRoomGetAllowItem(w.Player, char)) throw new Error('no permissions')
char.FocusGroup = this.ensure('invalid AssetGroup', () => window.AssetGroupGet(char.AssetFamily, ag)) char.FocusGroup = this.ensure('invalid AssetGroup', () => w.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)) const activity = this.ensure('invalid activity', () => w.ActivityAllowedForGroup(char, char.FocusGroup.Name).find(a => a.Activity?.Name === action))
if ((activity.Name || activity.Activity.Name).endsWith('Item')) { //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))) // const item = this.ensure('no toy found', () => w.Player.Inventory.find(i => i.Asset?.Name === 'SpankingToys' && i.Asset.Group?.Name === char.FocusGroup.Name && w.AssetSpankingToys.DynamicActivity(char) === (activity.Name || activity.Activity.Name)))
window.DialogPublishAction(char, item) // w.DialogPublishAction(char, item)
} else window.ActivityRun(window.Player, char, char.FocusGroup, activity) //} else w.ActivityRun(w.Player, char, char.FocusGroup, activity)
w.ActivityRun(w.Player, char, char.FocusGroup, activity)
} finally { } finally {
char.FocusGroup = null char.FocusGroup = null
} }
@ -348,7 +323,7 @@ window.MBCHC = {
const text = string.slice(1) const text = string.slice(1)
let suffix = ' ' let suffix = ' '
if (text.startsWith('\'') || text.startsWith(' ')) suffix = '' if (text.startsWith('\'') || text.startsWith(' ')) suffix = ''
return `${window.MBCHC.PREF_ACTIVITY}<${window.Player.cid}:>SourceCharacter${suffix}` return `${w.MBCHC.PREF_ACTIVITY}<${cid(w.Player)}:>SourceCharacter${suffix}`
}, },
cid2dict(type, cid) { cid2dict(type, cid) {
return ({Tag: `${type}Character`, MemberNumber: cid, Text: this.cid2char(cid).dn}) return ({Tag: `${type}Character`, MemberNumber: cid, Text: this.cid2char(cid).dn})
@ -361,45 +336,29 @@ window.MBCHC = {
if (cids[1]) dict.push(this.cid2dict('Source', cids[1])) 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])) if (cids[2]) dict.push(this.cid2dict('Target', cids[2]), this.cid2dict('Destination', cids[2]))
} }
window.ServerSend('ChatRoomChat', {Type: 'Action', Content: message, Dictionary: dict}) w.ServerSend('ChatRoomChat', {Type: 'Action', Content: message, Dictionary: dict})
}, },
receive(data) { //receive(data) {
const char = this.cid2char(data.Sender) // const char = this.cid2char(data.Sender)
if (char.IsPlayer()) return true // This is our own message, sent back to us // if (char.IsPlayer()) return true // This is our own message, sent back to us
const payload = this.ensure('Empty message', () => data.Dictionary[0]) // const payload = this.ensure('Empty message', () => data.Dictionary[0])
switch (payload.type) { // switch (payload.type) {
case 'greetings': case 'hello': { // case 'greetings': case 'hello': {
char.MBCHC = payload.value // char.MBCHC = payload.value
if (payload.type === 'greetings') this.hello(char) // if (payload.type === 'greetings') this.hello(char)
break // break
} // }
default: // If we don't know the type it may be from a newer version // default: // If we don't know the type it may be from a newer version
} // }
return true // return true
}, //},
hello(char = null) { //hello(char = null) {
const payload = {type: 'greetings', value: window.Player.MBCHC} // const payload = {type: 'greetings', value: w.Player.MBCHC}
if (char) payload.type = 'hello' // if (char) payload.type = 'hello'
const message = {Content: 'MBCHC', Type: 'Hidden', Dictionary: [payload]} // const message = {Content: 'MBCHC', Type: /** @type {const} */ ('Hidden'), Dictionary: [payload]}
if (char) message.Target = char.cid // if (char) message.Target = char.cid
window.ServerSend('ChatRoomChat', message) // w.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) { copy_fbc_trigger(trigger) {
const result = { const result = {
Type: 'Action', Type: 'Action',
@ -411,27 +370,24 @@ window.MBCHC = {
patch_fbc() { patch_fbc() {
this.remove_fbc_hook() this.remove_fbc_hook()
delete 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))) w.bce_ActivityTriggers.push(...w.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 /* (["anim", "pose"]).forEach(tag => {let cmd = w.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') let cmd = w.Commands.find(c => c.Tag === 'anim')
if (cmd) cmd.AutoComplete = this.complete_fbc_anim if (cmd) cmd.AutoComplete = this.complete_fbc_anim
cmd = window.Commands.find(c => c.Tag === 'pose') cmd = w.Commands.find(c => c.Tag === 'pose')
if (cmd) cmd.AutoComplete = this.complete_fbc_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) { find_timezone(char) {
const timezones = this.settings('timezones') const timezones = this.settings('timezones')
if (timezones && typeof timezones[char.cid] === 'number') return (timezones[char.cid]) if (timezones && typeof timezones[cid(char)] === 'number') return (timezones[cid(char)])
const match = (char.Description) ? char.Description.match(this.RE_TZ) : null const match = (char.Description) ? char.Description.match(this.RE_TZ) : null
const int = match ? Number.parseInt(match[1] + match[2], 10) : 42 const int = match ? Number.parseInt(match[1] + match[2], 10) : 42
if (this.in(int, -12, 12)) return (int) if (this.in(int, -12, 12)) return (int)
return (null) return (null)
}, },
player_enters_room() { // Or if the mod is loaded while player is in the room //player_enters_room() { // Or if the mod is loaded while player is in the room
this.hello() // this.hello()
}, //},
set_timezone(args) { set_timezone(args) {
const tz = Number.parseInt(args[0], 10) const tz = Number.parseInt(args[0], 10)
if (Number.isNaN(tz)) throw new Error(`invalid offset "${args[0]}"`) if (Number.isNaN(tz)) throw new Error(`invalid offset "${args[0]}"`)
@ -440,17 +396,17 @@ window.MBCHC = {
char.MBCHC_LOCAL.TZ = tz char.MBCHC_LOCAL.TZ = tz
this.save_settings(s => { this.save_settings(s => {
if (!s.timezones) s.timezones = {} if (!s.timezones) s.timezones = {}
s.timezones[char.cid] = tz s.timezones[cid(char)] = tz
}) })
}, },
update_char(char) { update_char(char) {
char.cid = char.MemberNumber // Club ID (shorter) //char.cid = char.MemberNumber // Club ID (shorter)
char.dn = window.CharacterNickname(char) // DisplayName (shortcut) char.dn = w.CharacterNickname(char) // DisplayName (shortcut)
if (!char.MBCHC_LOCAL) char.MBCHC_LOCAL = {} if (!char.MBCHC_LOCAL) char.MBCHC_LOCAL = {}
if (!char.MBCHC_LOCAL.TZ) char.MBCHC_LOCAL.TZ = this.find_timezone(char) if (!char.MBCHC_LOCAL.TZ) char.MBCHC_LOCAL.TZ = this.find_timezone(char)
}, },
command_mbchc(argline, cmdline, args) { command_mbchc(argline, cmdline, args) {
const mbchc = window.MBCHC const mbchc = w.MBCHC
try { // `this` is command object 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(''))) 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 cmd = String(args.shift())
@ -461,7 +417,7 @@ window.MBCHC = {
} }
}, },
command_activity(argline, cmdline, _) { command_activity(argline, cmdline, _) {
const mbchc = window.MBCHC const mbchc = w.MBCHC
if (!mbchc.empty(argline)) { if (!mbchc.empty(argline)) {
try { // `this` is command object try { // `this` is command object
const message = mbchc.normalise_message(cmdline.replace(mbchc.RE_ACTIVITY, ''), {trim: true, dot: true, up: true}) const message = mbchc.normalise_message(cmdline.replace(mbchc.RE_ACTIVITY, ''), {trim: true, dot: true, up: true})
@ -472,7 +428,7 @@ window.MBCHC = {
} }
}, },
command_do(argline, cmdline, args) { command_do(argline, cmdline, args) {
const mbchc = window.MBCHC const mbchc = w.MBCHC
try { // `this` is command object 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(', '))) 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 let [verb, zone, target] = args
@ -485,14 +441,14 @@ window.MBCHC = {
if (!zone) throw new Error('zone missing') if (!zone) throw new Error('zone missing')
const ag = mbchc.ensure(`unknown zone "${zone}"`, () => mbchc.DO_DATA.zones[zone]) const ag = mbchc.ensure(`unknown zone "${zone}"`, () => mbchc.DO_DATA.zones[zone])
const types = mbchc.ensure(`zone "${zone}" invalid for "${verb}"`, () => zones[ag]) const types = mbchc.ensure(`zone "${zone}" invalid for "${verb}"`, () => zones[ag])
let char = window.Player let char = w.Player
if (target && ((types.self.length === 0) || (types.others.length > 0))) char = mbchc.target2char(target) if (target && ((types.self.length === 0) || (types.others.length > 0))) char = mbchc.target2char(target)
const type = char.IsPlayer() ? 'self' : 'others' const type = char.IsPlayer() ? 'self' : 'others'
const available = window.ActivityAllowedForGroup(char, ag) const available = w.ActivityAllowedForGroup(char, ag)
const toy = window.InventoryGet(window.Player, 'ItemHands') //const toy = w.InventoryGet(w.Player, 'ItemHands')
if (toy && toy.Asset.Name === 'SpankingToys') available.push(window.AssetAllActivities(char.AssetFamily).find(a => a.Name === window.InventorySpankingToysGetActivity?.(window.Player))) //if (toy && toy.Asset.Name === 'SpankingToys') available.push(w.AssetAllActivities(char.AssetFamily).find(a => a.Name === w.InventorySpankingToysGetActivity?.(w.Player)))
const actions = mbchc.ensure(`zone "${zone}" invalid for ("${verb}" "${type}")`, () => types[type]) 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))) const action = mbchc.ensure(`invalid action (${verb} ${zone} ${target})`, () => actions.find(name => available.find(a => a.Activity?.Name === name)))
mbchc.run_activity(char, ag, action) mbchc.run_activity(char, ag, action)
} catch (error) { } catch (error) {
mbchc.report(error) mbchc.report(error)
@ -500,9 +456,9 @@ window.MBCHC = {
}, },
bell() { bell() {
setTimeout(() => { setTimeout(() => {
document.querySelector('#InputChat').style.outline = '' style('#InputChat', s => s.outline = '')
}, 100) }, 100)
document.querySelector('#InputChat').style.outline = 'solid red' style('#InputChat', s => s.outline = 'solid red')
}, },
complete(options, space = true) { complete(options, space = true) {
if (options.length === 0) return (this.bell()) if (options.length === 0) return (this.bell())
@ -518,29 +474,42 @@ window.MBCHC = {
} }
if (pref) this.complete([pref], false) if (pref) this.complete([pref], false)
this.complete_hint(options) this.comp_hint(options)
} else window.ElementValue('InputChat', document.querySelector('#InputChat').value.replace(this.RE_LAST_WORD, `$1${options[0]}${space ? ' ' : ''}`)) } else w.ElementValue('InputChat', w.ElementValue('InputChat').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' * Displays strings as completion hint
window.ElementSetDataAttribute(this.COMP_HINT.id, 'colortheme', (window.Player.ChatSettings.ColorTheme || 'Light')) * @param {string[]} options List of words to display. The order will be modified without copy.
const rescroll = window.ElementIsScrolledToEnd('TextAreaChatLog') * @returns {void}
window.ChatRoomResize(false) */
if (rescroll) window.ElementScrollToEnd('TextAreaChatLog') comp_hint(options) {
if (options.length === 0) return
this.COMP_HINT.innerHTML = '<div>' + options.sort().reverse().map(s => `<div>${s}</div>`).join('') + '</div>'
this.COMP_HINT.style.display = 'block'
w.ElementSetDataAttribute(this.COMP_HINT.id, 'colortheme', (w.Player.ChatSettings.ColorTheme || 'Light'))
const rescroll = w.ElementIsScrolledToEnd('TextAreaChatLog')
w.ChatRoomResize(false)
if (rescroll) w.ElementScrollToEnd('TextAreaChatLog')
this.COMP_HINT.firstChild?.lastChild?.scrollIntoView({behaviour: 'instant'})
}, },
/**
* Returns true if the completion box is attached to body and its display isn't none
* @returns {boolean}
*/
comp_hint_visible() { comp_hint_visible() {
return (this.COMP_HINT.parentElement && this.COMP_HINT.style.display === 'flex') return (this.COMP_HINT.parentElement && this.COMP_HINT.style.display !== 'none')
}, },
complete_hint_hide() { comp_hint_hide() {
if (!this.comp_hint_visible()) return if (!this.comp_hint_visible()) return
this.COMP_HINT.style.display = 'none' this.COMP_HINT.style.display = 'none'
window.ChatRoomResize(false) w.ChatRoomResize(false)
}, },
complete_target(token, me2 = true, check_perms = false) { complete_target(token, me2 = true, check_perms = false) {
const [locase, found] = [token.toLocaleLowerCase(), new Set()] const [locase, found] = [token.toLocaleLowerCase(), new Set()]
for (const c of window.ChatRoomCharacter) { for (const c of w.ChatRoomCharacter) {
if ((c.IsPlayer() && !me2) || (check_perms && !window.ServerChatRoomGetAllowItem(window.Player, c))) continue if ((c.IsPlayer() && !me2) || (check_perms && !w.ServerChatRoomGetAllowItem(w.Player, c))) continue
for (const s of this.char2targets(c)) { for (const s of this.char2targets(c)) {
if (s.toLocaleLowerCase().startsWith(locase)) found.add(s) if (s.toLocaleLowerCase().startsWith(locase)) found.add(s)
} }
@ -549,11 +518,13 @@ window.MBCHC = {
this.complete(Array.from(found)) this.complete(Array.from(found))
}, },
complete_common() { complete_common() {
const input = document.querySelector('#InputChat').value // w.ElementValue('InputChat') will strip the trailing whitespace
return ([this, input, this.tokenise(input)]) const E = document.querySelector('#InputChat')
if (!(E && E instanceof HTMLTextAreaElement)) throw new Error('somehow InputChat is broken')
return ([this, E.value, this.tokenise(E.value)])
}, },
complete_mbchc(_args, _locase, _cmdline) { complete_mbchc(_args, _locase, _cmdline) {
const [mbchc, _input, tokens] = window.MBCHC.complete_common(); // `this` is command object const [mbchc, _input, tokens] = w.MBCHC.complete_common(); // `this` is command object
if (tokens.length === 0) return if (tokens.length === 0) return
if (tokens.length < 2) return (mbchc.complete([`${mbchc.CommandsKey}${this.Tag}`])) if (tokens.length < 2) return (mbchc.complete([`${mbchc.CommandsKey}${this.Tag}`]))
const subname = tokens[1].toLocaleLowerCase() const subname = tokens[1].toLocaleLowerCase()
@ -568,11 +539,11 @@ window.MBCHC = {
complete_do_target(actions, token) { complete_do_target(actions, token) {
if (!actions) return if (!actions) return
const me2 = (actions.self.length > 0) const me2 = (actions.self.length > 0)
if (me2 && actions.others.length === 0) return (this.complete([window.Player.cid.toString()])) // Target is always the player if (me2 && actions.others.length === 0) return (this.complete([cid(w.Player).toString()])) // Target is always the player
this.complete_target(token, me2, true) this.complete_target(token, me2, true)
}, },
complete_do(_args, _locase, _cmdline) { complete_do(_args, _locase, _cmdline) {
const [mbchc, _input, tokens] = window.MBCHC.complete_common(); // `this` is command object const [mbchc, _input, tokens] = w.MBCHC.complete_common(); // `this` is command object
if (tokens.length === 0) return if (tokens.length === 0) return
if (tokens.length < 2) return (mbchc.complete([`${mbchc.CommandsKey}${this.Tag}`])) 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 // 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
@ -595,38 +566,38 @@ window.MBCHC = {
mbchc.bell() mbchc.bell()
}, },
complete_fbc_anim(_args, _locase, _cmdline) { complete_fbc_anim(_args, _locase, _cmdline) {
const [mbchc, _input, tokens] = window.MBCHC.complete_common(); // `this` is command object const [mbchc, _input, tokens] = w.MBCHC.complete_common(); // `this` is command object
if (tokens.length === 0) return if (tokens.length === 0) return
if (tokens.length < 2) return (mbchc.complete([`${mbchc.CommandsKey}${this.Tag}`])) if (tokens.length < 2) return (mbchc.complete([`${mbchc.CommandsKey}${this.Tag}`]))
if (tokens.length > 2) return (mbchc.bell()) if (tokens.length > 2) return (mbchc.bell())
const anim = tokens[1].toLocaleLowerCase() const anim = tokens[1].toLocaleLowerCase()
return (mbchc.complete(Object.keys(window.bce_EventExpressions).filter(a => a.toLocaleLowerCase().startsWith(anim)))) return (mbchc.complete(Object.keys(w.bce_EventExpressions).filter(a => a.toLocaleLowerCase().startsWith(anim))))
}, },
complete_fbc_pose(_args, _locase, _cmdline) { complete_fbc_pose(_args, _locase, _cmdline) {
const [mbchc, _input, tokens] = window.MBCHC.complete_common(); // `this` is command object const [mbchc, _input, tokens] = w.MBCHC.complete_common(); // `this` is command object
if (tokens.length === 0) return if (tokens.length === 0) return
if (tokens.length < 2) return (mbchc.complete([`${mbchc.CommandsKey}${this.Tag}`])) if (tokens.length < 2) return (mbchc.complete([`${mbchc.CommandsKey}${this.Tag}`]))
const pose = tokens.at(-1).toLocaleLowerCase() const pose = tokens.at(-1).toLocaleLowerCase()
return (mbchc.complete(window.PoseFemale3DCG.map(p => p.Name).filter(p => p.toLocaleLowerCase().startsWith(pose)))) return (mbchc.complete(w.PoseFemale3DCG.map(p => p.Name).filter(p => p.toLocaleLowerCase().startsWith(pose))))
}, },
history(down) { history(down) {
const [text, history] = [window.ElementValue('InputChat'), window.ChatRoomLastMessage] const [text, history] = [w.ElementValue('InputChat'), w.ChatRoomLastMessage]
if (!this.HISTORY_MODE) { if (!this.HISTORY_MODE) {
history.push(text) history.push(text)
this.HISTORY_MODE = true this.HISTORY_MODE = true
} }
const ids = history.map((t, i) => [t, i]).filter(([t, _]) => t.startsWith(history.at(-1))).map(([_, i]) => i) const ids = history.map((t, i) => ({t, i})).filter(r => r.t.startsWith(history.at(-1))).map(r => r.i)
if (!down) ids.reverse() if (!down) ids.reverse()
const found = ids.find(id => (down) ? id > window.ChatRoomLastMessageIndex : id < window.ChatRoomLastMessageIndex) const found = ids.find(id => (down) ? id > w.ChatRoomLastMessageIndex : id < w.ChatRoomLastMessageIndex)
if (!found) return (this.bell()) if (!found) return (this.bell())
window.ElementValue('InputChat', history[found]) w.ElementValue('InputChat', history[found])
window.ChatRoomLastMessageIndex = found w.ChatRoomLastMessageIndex = found
}, },
focus_chat_checks() { // we only want to catch chatlog and canvas (no map though) keypresses 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 === document.body) return true
if (document.activeElement.id !== 'MainCanvas') return false if (document.activeElement.id !== 'MainCanvas') return false
return !window.ChatRoomMapVisible return !w.ChatRoomMapViewIsActive()
}, },
focus_chat_whitelist(event) { focus_chat_whitelist(event) {
if (event.ctrlKey && event.key === 'v') return true // Ctrl+V should paste if (event.ctrlKey && event.key === 'v') return true // Ctrl+V should paste
@ -636,8 +607,8 @@ window.MBCHC = {
if (event.repeat) return // Only unique presses please if (event.repeat) return // Only unique presses please
if (!this.focus_chat_checks()) return 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 ([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 if (style('#InputChat', s => s.display) !== 'inline') return // Input chat missing
window.ElementFocus('InputChat') w.ElementFocus('InputChat')
}, },
loader() { loader() {
if (this.remove_load_hook) { if (this.remove_load_hook) {
@ -652,7 +623,6 @@ window.MBCHC = {
{Tag: 'activity', Description: '[Message]: Send a custom activity (or "@@Message", or "@Message" as yourself)', Action: this.command_activity}, {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}, {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.CommandsKey = CommandsKey /* eslint-disable-line no-undef */ // window.CommandsKey is undefined
this.RE_ACTIVITY = new RegExp(`^${this.CommandsKey}activity `) this.RE_ACTIVITY = new RegExp(`^${this.CommandsKey}activity `)
this.PREF_ACTIVITY = `${this.CommandsKey}activity ` this.PREF_ACTIVITY = `${this.CommandsKey}activity `
@ -667,126 +637,140 @@ window.MBCHC = {
background-color: ${this.RGB_MUTE}; background-color: ${this.RGB_MUTE};
} }
#${this.COMP_HINT.id} { #${this.COMP_HINT.id} {
flex-flow: column wrap;
overflow: auto;
display: none; display: none;
text-align: right;
}
#${this.COMP_HINT.id} > div {
overflow: auto;
position: absolute;
bottom: 0;
right: 0;
max-height: 100%;
padding: 0 0.5ex;
background-color: ${this.RGB_POLLY}; background-color: ${this.RGB_POLLY};
color: black; color: black;
} }
#${this.COMP_HINT.id}[data-colortheme="dark"], #${this.COMP_HINT.id}[data-colortheme="dark2"] { #${this.COMP_HINT.id}[data-colortheme="dark"] > div, #${this.COMP_HINT.id}[data-colortheme="dark2"] > div {
background-color: ${this.RGB_MUTE}; background-color: ${this.RGB_MUTE};
color: white; color: white;
} }
#${this.COMP_HINT.id} div { #${this.COMP_HINT.id} > div div {
margin: 0 0.5ex; margin: 0.25ex 0;
} }
` `
document.head.append(css) document.head.append(css)
// Actions // Actions
this.calculate_maps() this.calculate_maps()
window.Player.MBCHC = {VERSION: this.VERSION} //w.Player.MBCHC = {VERSION: this.VERSION}
window.CommandCombine(COMMANDS) w.CommandCombine(COMMANDS)
// Hooks // Hooks
this.remove_fbc_hook = this.before('MainRun', () => window.bce_ActivityTriggers && this.patch_fbc()) this.remove_fbc_hook = this.before('MainRun', () => w.bce_ActivityTriggers && this.patch_fbc())
this.after('CharacterOnlineRefresh', char => this.update_char(char)) this.after('CharacterOnlineRefresh', char => this.update_char(char))
this.after('ChatRoomReceiveSuitcaseMoney', () => { this.after('ChatRoomReceiveSuitcaseMoney', () => {
if (this.AUTOHACK_ENABLED && this.LAST_HACKED) { if (this.AUTOHACK_ENABLED && this.LAST_HACKED) {
window.CurrentCharacter = this.cid2char(this.LAST_HACKED) w.CurrentCharacter = this.cid2char(this.LAST_HACKED)
this.LAST_HACKED = null this.LAST_HACKED = null
window.ChatRoomTryToTakeSuitcase() w.ChatRoomTryToTakeSuitcase()
} }
}) })
this.before('ChatRoomSendChat', () => { this.before('ChatRoomSendChat', () => {
let input = window.ElementValue('InputChat') let input = w.ElementValue('InputChat')
if (!input.startsWith('@@@') && input.startsWith('@')) { if (!input.startsWith('@@@') && input.startsWith('@')) {
input = input.replace(this.RE_PREF_ACTIVITY, this.PREF_ACTIVITY) input = input.replace(this.RE_PREF_ACTIVITY, this.PREF_ACTIVITY)
input = input.replace(this.RE_PREF_ACTIVITY_ME, this.replace_me) input = input.replace(this.RE_PREF_ACTIVITY_ME, this.replace_me)
window.ElementValue('InputChat', input) w.ElementValue('InputChat', input)
} }
}) })
this.after('ChatRoomSendChat', () => { this.after('ChatRoomSendChat', () => {
const history = window.ChatRoomLastMessage const history = w.ChatRoomLastMessage
if ((history.length > 1) && (history.at(-1) === history.at(-2))) { if ((history.length > 1) && (history.at(-1) === history.at(-2))) {
history.pop() history.pop()
window.ChatRoomLastMessageIndex -= 1 w.ChatRoomLastMessageIndex -= 1
} }
}) })
this.before('ChatRoomCharacterViewDrawOverlay', (C, CharX, CharY, Zoom, _Pos) => { this.before('ChatRoomCharacterViewDrawOverlay', (C, CharX, CharY, Zoom, _Pos) => {
// if (window.ChatRoomHideIconState < 1 && C.MBCHC) { // if (w.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) // w.DrawRect(CharX + (175 * Zoom), CharY, 50 * Zoom, 50 * Zoom, C.MBCHC.VERSION === w.Player.MBCHC.VERSION ? this.RGB_POLLY : this.RGB_MUTE)
// } // }
if (window.ChatRoomHideIconState < 1 && C.MBCHC_LOCAL && typeof C.MBCHC_LOCAL.TZ === 'number') { if (w.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() const hours = new Date(w.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') w.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.after('ChatRoomCreateElement', () => this.COMP_HINT.parentElement || document.body.append(this.COMP_HINT))
this.before('ChatRoomClearAllElements', () => { this.before('ChatRoomClearAllElements', () => {
this.complete_hint_hide() this.comp_hint_hide()
this.COMP_HINT.remove() this.COMP_HINT.remove()
}) })
this.before('ChatRoomClick', () => this.complete_hint_hide()) this.before('ChatRoomClick', () => {
this.comp_hint_hide()
})
this.after('ChatRoomResize', () => { this.after('ChatRoomResize', () => {
if (window.CharacterGetCurrent() === null && window.CurrentScreen === 'ChatRoom' && document.querySelector('#InputChat') && document.querySelector('#TextAreaChatLog') && this.comp_hint_visible()) { // Upstream if (w.CharacterGetCurrent() === null && w.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 const fontsize = ChatRoomFontSize /* eslint-disable-line no-undef */ // window.ChatRoomFontSize is undefined
window.ElementPositionFix('TextAreaChatLog', fontsize, 1005, 66, 988, 630) //w.ElementPositionFix('TextAreaChatLog', fontsize, 1005, 66, 988, 630)
window.ElementPositionFix(this.COMP_HINT.id, fontsize, 1005, 701, 988, 200) //w.ElementPositionFix(this.COMP_HINT.id, fontsize, 1005, 701, 988, 200)
this.COMP_HINT.style.display = 'flex' w.ElementPositionFix(this.COMP_HINT.id, fontsize, 800, 65, 200, 835)
//this.COMP_HINT.style.display = 'flex'
} }
}) })
document.addEventListener('keydown', event => this.focus_chat(event)) document.addEventListener('keydown', event => this.focus_chat(event))
this.SDK.hookFunction('ChatRoomKeyDown', 0, (nextargs, next) => { // This fires on chat input events this.SDK.hookFunction('ChatRoomKeyDown', 0, (nextargs, next) => { // This fires on chat input events
const [event] = nextargs const [event] = nextargs
window.MBCHC.complete_hint_hide() w.MBCHC.comp_hint_hide()
if ((window.KeyPress === 33) || (window.KeyPress === 34)) { // Better history if ((w.KeyPress === 33) || (w.KeyPress === 34)) { // Better history
event.preventDefault() event.preventDefault()
return (window.MBCHC.history(window.KeyPress - 33)) return (w.MBCHC.history(w.KeyPress - 33))
} }
if (window.MBCHC.HISTORY_MODE) { if (w.MBCHC.HISTORY_MODE) {
window.ChatRoomLastMessage.pop() w.ChatRoomLastMessage.pop()
window.MBCHC.HISTORY_MODE = false w.MBCHC.HISTORY_MODE = false
} }
return (next(nextargs)) return (next(nextargs))
}) })
// Chat room handlers // Chat room handlers
window.ChatRoomRegisterMessageHandler({Priority: -220, Description: 'MBCHC preprocessor', Callback: (data, sender, message, metadata) => { w.ChatRoomRegisterMessageHandler({Priority: -220, Description: 'MBCHC preprocessor', Callback: (data, sender, message, metadata) => {
data.MBCHC_ID = this.NEXT_MESSAGE data.MBCHC_ID = this.NEXT_MESSAGE
this.NEXT_MESSAGE += 1 this.NEXT_MESSAGE += 1
if (this.LOG_MESSAGES) console.debug({data, sender, msg: message, metadata}) if (this.LOG_MESSAGES) console.debug({data, sender, msg: message, metadata})
return false
}}) }})
window.ChatRoomRegisterMessageHandler({Priority: -219, Description: 'MBCHC room enter hook', //w.ChatRoomRegisterMessageHandler({Priority: -219, Description: 'MBCHC room enter hook',
Callback: (data, _sender, _message, _metadata) => { // Callback: (data, _sender, _message, _metadata) => {
if ((data.Type === 'Action') && (data.Content === 'ServerEnter') && (data.Sender === window.Player.cid)) this.player_enters_room() // if ((data.Type === 'Action') && (data.Content === 'ServerEnter') && (data.Sender === cid(w.Player))) this.player_enters_room()
}, // return false
}) // },
window.ChatRoomRegisterMessageHandler({Priority: -219, Description: 'MBCHC specific consumer', //})
Callback: (data, _sender, _message, _metadata) => { //w.ChatRoomRegisterMessageHandler({Priority: -219, Description: 'MBCHC specific consumer',
if ((data.Type === 'Hidden') && (data.Content === 'MBCHC')) return this.receive(data) // Callback: (data, _sender, _message, _metadata) => {
}, // if ((data.Type === 'Hidden') && (data.Content === 'MBCHC')) return this.receive(data)
}) // return false
window.ChatRoomRegisterMessageHandler({Priority: -219, Description: 'MBCHC autohack lookup', // },
//})
w.ChatRoomRegisterMessageHandler({Priority: -219, Description: 'MBCHC autohack lookup',
Callback: (data, _sender, _message, _metadata) => { Callback: (data, _sender, _message, _metadata) => {
if ((data.Type === 'Hidden') && (data.Content === 'ReceiveSuitcaseMoney')) this.LAST_HACKED = data.Sender if ((data.Type === 'Hidden') && (data.Content === 'ReceiveSuitcaseMoney')) this.LAST_HACKED = data.Sender
return false
}, },
}) })
// Footer // Footer
this.LOADED = true this.LOADED = true
this.log('info', `loaded version ${this.VERSION}`) this.log('info', `loaded version ${this.VERSION}`)
if ((window.CurrentModule === 'Online') && (window.CurrentScreen === 'ChatRoom')) { if ((w.CurrentModule === 'Online') && (w.CurrentScreen === 'ChatRoom')) {
for (const c of window.ChatRoomCharacter) this.update_char(c) for (const c of w.ChatRoomCharacter) this.update_char(c)
this.player_enters_room() //this.player_enters_room()
} }
}, },
preloader() { preloader() {
if (!window.AsylumGGTSSAddItems) throw new Error('AsylumGGTSSAddItems() not found, aborting MBCHC loading') if (!w.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') if (!w.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.SDK = w.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) => { this.before = (name, cb) => this.SDK.hookFunction(name, 0, (nextargs, next) => {
try { try {
cb?.(...nextargs) cb?.(...nextargs)
@ -804,11 +788,13 @@ window.MBCHC = {
} }
return result return result
}) })
if (window.CurrentModule && window.CurrentScreen && !(window.CurrentModule === 'Character' && window.CurrentScreen === 'Login')) return this.loader() // for some reason many (not all) hooks don't work if the mod is loaded in the room
// to be honest I have no idea what's going on. the hooks get registered, they just don't get called by SDK.
if (current() === 'Online/ChatRoom') throw new Error('please do not load in a chat room')
if (current() === 'Character/Login') {
this.remove_load_hook = this.before('AsylumGGTSSAddItems', () => this.loader()) this.remove_load_hook = this.before('AsylumGGTSSAddItems', () => this.loader())
} else this.loader()
}, },
} }
window.MBCHC.preloader() w.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

2245
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,10 +1,12 @@
{ {
"name": "mbchc", "name": "mbchc",
"version": "0.0.9", "version": "0.0.12",
"description": "Mute's Bondage Club Hacks Collection", "description": "Mute's Bondage Club Hacks Collection",
"type": "module", "type": "module",
"devDependencies": { "devDependencies": {
"xo": "^0.56.0" "xo": "^0.56.0",
"bc-stubs": "^105.0.0",
"bondage-club-mod-sdk": "^1.2.0"
}, },
"license": "SEE LICENSE IN LICENSE.", "license": "SEE LICENSE IN LICENSE.",
"xo": { "xo": {
@ -13,18 +15,22 @@
"node" "node"
], ],
"rules": { "rules": {
"brace-style": "off",
"camelcase": "off", "camelcase": "off",
"capitalized-comments": "off", "capitalized-comments": "off",
"curly": "off", "curly": "off",
"max-params": "off", "max-params": "off",
"max-statements-per-line": "off",
"new-cap": "off", "new-cap": "off",
"no-return-assign": "off", "no-return-assign": "off",
"no-unused-expressions": "off",
"no-unused-vars": ["error", { "no-unused-vars": ["error", {
"argsIgnorePattern": "^_", "argsIgnorePattern": "^_",
"destructuredArrayIgnorePattern": "^_" "destructuredArrayIgnorePattern": "^_"
}], }],
"padding-line-between-statements": "off", "padding-line-between-statements": "off",
"semi": "off", "semi": "off",
"spaced-comment": "off",
"unicorn/no-array-reduce": "off", "unicorn/no-array-reduce": "off",
"fake/fuck-commas": "off" "fake/fuck-commas": "off"
} }

View File

@ -1,35 +1,33 @@
import {readFileSync} from 'node:fs' import {readFileSync} from 'node:fs'
import {createServer} from 'node:http' import {createServer} from 'node:http'
function stamp_cors(rx) { const config = {host: '127.0.0.1', port: 9696}
rx.setHeader('Access-Control-Max-Age', '86400')
rx.setHeader('Access-Control-Allow-Private-Network', 'true') const h_cors = {
rx.setHeader('Access-Control-Allow-Origin', '*') 'Access-Control-Max-Age': '86400',
rx.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS') 'Access-Control-Allow-Private-Network': 'true',
rx.setHeader('Access-Control-Allow-Headers', '*') 'Access-Control-Allow-Origin': '*',
// rx.setHeader('Access-Control-Allow-Credentials', 'false') // omit this header to disallow 'Access-Control-Allow-Methods': 'GET, OPTIONS',
'Access-Control-Allow-Headers': '*',
// 'Access-Control-Allow-Credentials': 'false', // omit this header to disallow
}
const h_all = Object.assign({
'Content-Type': 'text/javascript',
'Cache-Control': 'no-cache',
}, h_cors)
/**
* @typedef {import('node:http').ServerResponse} ServerResponse
* @type {Record<string,function(ServerResponse):void>}
*/
const resp = {
GET(rx) { rx.writeHead(200, h_all); rx.write(readFileSync('./mbchc.mjs')) },
OPTIONS(rx) { rx.writeHead(204, h_cors) },
} }
const server = createServer((rq, rx) => { const server = createServer((rq, rx) => {
switch (rq.method) { resp[rq.method] && (new URL(`http://${config.host}:${config.port}${rq.url}`)).pathname === '/' ? resp[rq.method](rx) : rx.writeHead(400)
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() rx.end()
console.log('%s %d %s %s', (new Date()).toISOString(), rx.statusCode, rq.method, rq.url)
}) })
server.listen(9696, '127.0.0.1', () => console.log('Server started.')) server.listen(config.port, config.host, () => console.log(`Server started at http://${config.host}:${config.port}`))

10
typedef.d.ts vendored Normal file
View File

@ -0,0 +1,10 @@
/* eslint-disable no-unused-vars */
/* eslint-disable @typescript-eslint/consistent-type-definitions */
interface PlayerOnlineSettings {
MBCHC?: any;
}
interface ServerChatRoomMessage {
MBCHC_ID?: number;
}