105.13.0 semver; settings migration

[lots of internal change]
I'm determined to clean this mess up after all.
Instead of a total rewrite (which didn't work), I'll migrate the
code to JSDoc piece by piece. This is the first piece, setting up
some basic framework and testing out this and that.
Until I figure out how test this thing, I'll just ask peeps to import
the module from testing branch I guess.
Probably not gonna deal with anything new until I can turn "strict" on.
This commit is contained in:
Mute 2024-07-13 21:55:45 +00:00
parent 90231cb2ae
commit 16308eccf1
6 changed files with 6106 additions and 6030 deletions

62
ambient.d.ts vendored Normal file
View File

@ -0,0 +1,62 @@
interface ServerChatRoomMessage {
MBCHC_ID?: number
}
declare namespace MBCHC {
type WGT = Window & typeof globalThis
interface Root extends WGT {
MBCHC?: any
bcModSdk?: import('bondage-club-mod-sdk').ModSDKGlobalAPI
bce_ActivityTriggers?: any
bce_EventExpressions?: any
}
namespace Settings {
type V0 = PlayerOnlineSettings & {MBCHC: {timezones?: Record<number, number>}} // V0 is the whole onlinesettings
interface V1 { // V1 is specifically MBCHC inside extensionsettings
TZ: Record<number, number>
}
interface Methods {
migrate_0_1(v0: V0): true
save(cb?: (v1: V1) => unknown): true
replace(new_v1: V1): true
'purge!'(): true
get v1(): V1
}
}
interface Interval {
proxy: object // eslint-disable-line @typescript-eslint/ban-types
min: number
max: number
mini: boolean
maxi: boolean
upd(min: number, max: number, min_inclusive?: boolean, max_inclusive?: boolean): undefined
has(_: unknown, x: string): boolean
}
interface Utils {
interval: Interval
cid(character: Character): number | undefined
dn(character: Character): string
current(): string
with<V, R>(value: V, cb: (value: V) => R): R // A silly helper to kinda curry values
true<F extends (...args: unknown[]) => unknown>(cb: F): true // Useful for type-safe chaining
mutate<T>(value: T, cb: (v: T) => unknown): T // A silly helper for chaining
rm<T>(object: T, property: keyof T): T
mrg<T extends Record<string, unknown>>(target: T, ...source: T[]): T // Shorter, also less confusing with types
style<T>(query: string, cb: (s: CSSStyleDeclaration) => T): T | undefined
range(min: number, max: number, min_inclusive?: boolean, max_inclusive?: boolean): Proxy
assert<T>(error: string, value: T, cb?: (value: T) => boolean): T | never
each<T extends Record<string, unknown>>(object: T, cb: (key: string, value: unknown) => unknown): true
}
interface TZ_Cache {
map: Map<number, number>
RE: RegExp
for(character: Character): number | undefined
memo(member_number: number, description?: string | undefined): number | undefined
parse(description: string | undefined): number | undefined
}
}

View File

@ -2,7 +2,7 @@
"include": [ "include": [
"node_modules/bc-stubs/bc/**/*.d.ts", "node_modules/bc-stubs/bc/**/*.d.ts",
"node_modules/bondage-club-mod-sdk/dist/**/*.d.ts", "node_modules/bondage-club-mod-sdk/dist/**/*.d.ts",
"typedef.d.ts", "ambient.d.ts",
"mbchc.mjs", "mbchc.mjs",
"server.js" "server.js"
], ],
@ -13,6 +13,7 @@
], ],
"checkJs": true, "checkJs": true,
"strict": false, "strict": false,
"strictNullChecks": true,
"noImplicitOverride": true "noImplicitOverride": true
} }
} }

378
mbchc.mjs
View File

@ -1,45 +1,57 @@
export {} /** @type {MBCHC.Root} */ const W = window, D = W.document
if (W.MBCHC !== undefined) throw new Error('MBCHC found, aborting loading')
/** @typedef {import('bondage-club-mod-sdk').ModSDKGlobalAPI} ModSDKGlobalAPI */ export const VERSION = '105.13.0'
const MISSING_PLAYER_DIALOG = {Tag: 'MISSING TEXT IN "Interface.csv": ', Text: '\u200C'} // Zero-width non-joiner, used to break up ligatures, does nothing here, but an empty string is a falsey value
/** @type {Window & typeof globalThis & {MBCHC?: any, bcModSdk?: ModSDKGlobalAPI, bce_ActivityTriggers?: any, bce_EventExpressions?: any}} */ /** @implements {MBCHC.Interval} */ class Interval { proxy = new Proxy({}, this); min = 0; max = 0; mini = false; maxi = false
const w = window /** @type {MBCHC.Interval['upd']} */ upd(min, max, mini = true, maxi = true) {this.min = min; this.max = max; this.mini = mini; this.maxi = maxi}
/** @type {MBCHC.Interval['has']} */ has(_, x) {return U.with(Number(x), x => (this.mini ? x >= this.min : x > this.min) && (this.maxi ? x <= this.max : x < this.max))}
}
/** /** @type {MBCHC.Utils} */ const U = { interval: new Interval(),
* A silly helper to memorise values in callbacks cid: char => char.MemberNumber,
* @template V, R dn: char => W.CharacterNickname(char),
* @param {V} v Value to memorise current: () => `${W.CurrentModule}/${W.CurrentScreen}`,
* @param {function(V): R} cb Callback with: (v, cb) => cb(v),
* @returns {R} Return value of the callback true(cb) {cb(); return true},
*/ mutate(v, cb) {cb(v); return v},
const take = (v, cb) => cb(v) rm(o, p) {delete o[p]; return o}, // eslint-disable-line @typescript-eslint/no-dynamic-delete
mrg: (t, ...s) => Object.assign(t, ...s), // eslint-disable-line @typescript-eslint/no-unsafe-return
style: (q, cb) => U.with(D.querySelector(q), E => E instanceof HTMLElement ? cb(E.style) : undefined),
range: (...args) => U.true(() => void U.interval.upd(...args)) && U.interval.proxy,
assert(x, v, cb = Boolean) {if (!cb(v)) throw new Error(x); return v},
each: (o, cb) => U.true(() => void Object.entries(o).forEach(kv => cb(...kv))),
}
/** /** @type {MBCHC.Settings.Methods} */ const Settings = {
* Takes a DOM query and passes the element's style into a callback, returning its result migrate_0_1(v0) { // I hate change
* @template T U.with(v0.MBCHC.timezones, tz => tz !== undefined && this.save(v1 => void U.each(tz, (k, v) => v1.TZ[k] ||= v)))
* @param {string} q Query W.ServerAccountUpdate.QueueData({OnlineSettings: U.rm(v0, 'MBCHC')})
* @param {function(CSSStyleDeclaration): T} cb Callback return U.true(() => void console.warn('MBCHC: settings migration done (v0 -> v1). This should never appear again.'))
* @returns {T | void} Return value of the callback, if it was called },
*/ save(cb = undefined) {W.Player.ExtensionSettings.MBCHC ||= {}; cb?.(this.v1); W.ServerPlayerExtensionSettingsSync('MBCHC'); return true},
const style = (q, cb) => take(document.querySelector(q), E => E && E instanceof HTMLElement && E.style ? cb(E.style) : undefined) replace: v1 => U.true(() => W.Player.ExtensionSettings.MBCHC = v1) && Settings.save(),
'purge!': () => Settings.replace(/** @type {MBCHC.Settings.V1} */ ({})),
get v1() {return U.mutate(/** @type {MBCHC.Settings.V1} */ (W.Player.ExtensionSettings.MBCHC) ?? {}, v1 => { // we need to check and repair the whole object every time we access it
v1.TZ ||= {}
})},
}
/** /** @type {MBCHC.TZ_Cache} */ const TZ = { map: new Map(),
* @returns {string} Current view RE: /(?:gmt|utc)\s*([+-])\s*(\d\d?)/i,
*/ for: c => c.MemberNumber === undefined ? undefined : TZ.map.get(c.MemberNumber) ?? TZ.memo(c.MemberNumber, c.Description),
const current = () => `${w.CurrentModule}/${w.CurrentScreen}` memo: (cid, desc = undefined) => U.with(Settings.v1.TZ[cid] ?? TZ.parse(desc), n => n === undefined ? undefined : U.true(() => TZ.map.set(cid, n)) && n),
parse: desc => desc === undefined ? undefined : U.with(TZ.RE.exec(desc), m => m === null ? undefined : U.with(Number.parseInt(m[1] + m[2], 10), n => n in U.range(-12, 12) ? n : undefined)),
}
/** // ^ type-safe (still need strict later)
* @param {Character} char // =================================================================================
* @returns {number} // v legacy mess
*/
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 W.MBCHC = {
const MISSING_PLAYER_DIALOG = {Tag: 'MISSING TEXT IN "Interface.csv": ', Text: '\u200C'} VERSION,
Settings,
if (w.MBCHC) throw new Error('MBCHC found, aborting loading')
w.MBCHC = {
VERSION: 'dev.12',
NEXT_MESSAGE: 1, NEXT_MESSAGE: 1,
LOG_MESSAGES: false, LOG_MESSAGES: false,
RETHROW: false, RETHROW: false,
@ -50,7 +62,6 @@ w.MBCHC = {
RE_PREF_ACTIVITY_ME: /^@/, RE_PREF_ACTIVITY_ME: /^@/,
RE_PREF_ACTIVITY: /^@@/, RE_PREF_ACTIVITY: /^@@/,
RE_ACT_CIDS: /^<(\d+)?:(\d+)?>/, RE_ACT_CIDS: /^<(\d+)?:(\d+)?>/,
RE_TZ: /(?:gmt|utc)\s*([+-])\s*(\d\d?)/i,
RE_ALL_LEFT: /^<+$/, RE_ALL_LEFT: /^<+$/,
RE_ALL_RIGHT: /^>+$/, RE_ALL_RIGHT: /^>+$/,
RE_SPACES: /\s{2,}/g, RE_SPACES: /\s{2,}/g,
@ -163,18 +174,13 @@ w.MBCHC = {
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
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])},
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: () => Settings['purge!']()},
if (w.Player.OnlineSettings.MBCHC) { // if (W.Player.ExtensionSettings.MBCHC) {
delete w.Player.OnlineSettings.MBCHC // delete W.Player.ExtensionSettings.MBCHC
mbchc.save_settings() // mbchc.save_settings() // FIXME
} // }
}}, //}},
}, },
ensure(text, callback) {
const result = callback.call(this)
if (!result) throw new Error(text)
return (result)
},
calculate_maps() { calculate_maps() {
this.DO_DATA = {verbs: {}, zones: {}} this.DO_DATA = {verbs: {}, zones: {}}
for (const [verbs, data] of Object.entries(this.MAP_ACTIONS)) { for (const [verbs, data] of Object.entries(this.MAP_ACTIONS)) {
@ -188,17 +194,6 @@ w.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) {
const settings = w.Player.OnlineSettings.MBCHC || {}
return (setting ? settings[setting] : settings)
},
save_settings(cb = null) {
if (cb) {
if (!w.Player.OnlineSettings.MBCHC) w.Player.OnlineSettings.MBCHC = {}
cb.call(this, w.Player.OnlineSettings.MBCHC)
}
w.ServerAccountUpdate.QueueData({OnlineSettings: w.Player.OnlineSettings})
},
log(level, message) { log(level, message) {
console[level]('MBCHC: ' + String(message)) console[level]('MBCHC: ' + String(message))
}, },
@ -223,64 +218,61 @@ w.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) {
w.ChatRoomSendLocal(`<div class="mbchc">${html}</div>`, timeout) W.ChatRoomSendLocal(`<div class="mbchc">${html}</div>`, timeout)
}, },
report(x) { report(x) {
this.inform(`${x.toString()}`) this.inform(`${x.toString()}`)
if (this.RETHROW) throw x if (this.RETHROW) throw x
}, },
in(x, floor, ceiling) {
return ((x >= floor) && (x <= ceiling))
},
cid2char(id) { cid2char(id) {
id = Number.parseInt(id, 10) id = Number.parseInt(id, 10)
if (id === cid(w.Player)) return (w.Player) if (id === U.cid(W.Player)) return (W.Player)
return (this.ensure(`character ${id} not found in the room`, () => w.ChatRoomCharacter.find(c => cid(c) === id))) return U.assert(`character ${id} not found in the room`, W.ChatRoomCharacter.find(c => U.cid(c) === id))
}, },
pos2char(pos) { pos2char(pos) {
if (!this.in(pos, 0, w.ChatRoomCharacter.length - 1)) throw new Error(`invalid position ${pos}`) if (!(pos in U.range(0, W.ChatRoomCharacter.length - 1))) throw new Error(`invalid position ${pos}`)
return (w.ChatRoomCharacter[pos]) return (W.ChatRoomCharacter[pos])
}, },
rel2char(target) { rel2char(target) {
const me = this.ensure('can\'t find my position', () => w.ChatRoomCharacter.findIndex(char => char.IsPlayer()) + 1) - 1 // 0 is falsey, but is valid index const me = U.assert('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 %= w.ChatRoomCharacter.length pos %= W.ChatRoomCharacter.length
if (pos < 0) pos += w.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 (w.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 = []
if (target.startsWith('=')) return (this.cid2char(target.slice(1))) if (target.startsWith('=')) return (this.cid2char(target.slice(1)))
if (target.startsWith('<') || target.startsWith('>')) return (this.rel2char(target)) if (target.startsWith('<') || target.startsWith('>')) return (this.rel2char(target))
if (!Number.isNaN(int) && int.toString() === target) { // We got a number if (!Number.isNaN(int) && int.toString() === target) { // We got a number
if (this.in(int, 0, 9)) return (this.pos2char(int)) if (int in U.range(0, 9)) return (this.pos2char(int))
if (this.in(int, 11, 15)) return (this.pos2char(int - 11)) if (int in U.range(11, 15)) return (this.pos2char(int - 11))
if (this.in(int, 21, 25)) return (this.pos2char(int - 16)) if (int in U.range(21, 25)) return (this.pos2char(int - 16))
found.push(...w.ChatRoomCharacter.filter(c => cid(c).toString().includes(target))) found.push(...W.ChatRoomCharacter.filter(c => U.cid(c).toString().includes(target)))
} }
if (target.startsWith('@')) target = target.slice(1) if (target.startsWith('@')) target = target.slice(1)
found.push(...w.ChatRoomCharacter.filter(c => c.Name.toLocaleLowerCase().includes(target))) found.push(...W.ChatRoomCharacter.filter(c => c.Name.toLocaleLowerCase().includes(target)))
found.push(...w.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[cid(c)]) map[cid(c)] = c if (!map[U.cid(c)]) map[U.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 => `${cid(c)}|${c.Name}|${c.Nickname || c.Name}`).join(',')})`) if (found.length > 1) throw new Error(`target "${input}": multiple matches (${found.map(c => `${U.cid(c)}|${c.Name}|${c.Nickname || c.Name}`).join(',')})`)
return (found[0]) return (found[0])
}, },
char2targets(char) { char2targets(char) {
const [result, id] = [new Set(), cid(char).toString()] const [result, id] = [new Set(), U.cid(char).toString()]
result.add(id).add(`=${id}`) 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)
@ -294,27 +286,28 @@ w.MBCHC = {
return result return result
}, },
//get settings() {return Settings.v1},
donate_data(target) { donate_data(target) {
const char = this.target2char(target) const char = this.target2char(target)
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 (w.Player.Money < cost) throw new Error('not enough money') if (W.Player.Money < cost) throw new Error('not enough money')
w.CharacterChangeMoney(w.Player, -cost) W.CharacterChangeMoney(W.Player, -cost)
w.ServerSend('ChatRoomChat', {Content: 'ReceiveSuitcaseMoney', Type: 'Hidden', Target: cid(char)}) W.ServerSend('ChatRoomChat', {Content: 'ReceiveSuitcaseMoney', Type: 'Hidden', Target: U.cid(char)})
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]}) W.ChatRoomMessage({Sender: U.cid(W.Player), Type: 'Action', Content: `You've bought data for $${cost} and sent it to ${U.dn(char)}.`, Dictionary: [MISSING_PLAYER_DIALOG]})
}, },
run_activity(char, ag, action) { run_activity(char, ag, action) {
try { try {
if (!w.ActivityAllowed()) throw new Error('activities disabled in this room') if (!W.ActivityAllowed()) throw new Error('activities disabled in this room')
if (!w.ServerChatRoomGetAllowItem(w.Player, char)) throw new Error('no permissions') if (!W.ServerChatRoomGetAllowItem(W.Player, char)) throw new Error('no permissions')
char.FocusGroup = this.ensure('invalid AssetGroup', () => w.AssetGroupGet(char.AssetFamily, ag)) char.FocusGroup = U.assert('invalid AssetGroup', W.AssetGroupGet(char.AssetFamily, ag))
const activity = this.ensure('invalid activity', () => w.ActivityAllowedForGroup(char, char.FocusGroup.Name).find(a => a.Activity?.Name === action)) const activity = U.assert('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', () => 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))) // 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)))
// w.DialogPublishAction(char, item) // w.DialogPublishAction(char, item)
//} else w.ActivityRun(w.Player, char, char.FocusGroup, activity) //} else w.ActivityRun(w.Player, char, char.FocusGroup, activity)
w.ActivityRun(w.Player, char, char.FocusGroup, activity) W.ActivityRun(W.Player, char, char.FocusGroup, activity)
} finally { } finally {
char.FocusGroup = null char.FocusGroup = null
} }
@ -323,10 +316,10 @@ w.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 `${w.MBCHC.PREF_ACTIVITY}<${cid(w.Player)}:>SourceCharacter${suffix}` return `${W.MBCHC.PREF_ACTIVITY}<${U.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: U.dn(this.cid2char(cid))})
}, },
send_activity(message) { send_activity(message) {
const dict = [MISSING_PLAYER_DIALOG] const dict = [MISSING_PLAYER_DIALOG]
@ -336,7 +329,7 @@ w.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]))
} }
w.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)
@ -370,54 +363,43 @@ w.MBCHC = {
patch_fbc() { patch_fbc() {
this.remove_fbc_hook() this.remove_fbc_hook()
delete this.remove_fbc_hook delete this.remove_fbc_hook
w.bce_ActivityTriggers.push(...w.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 = 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 /* (["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 = w.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 = w.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
}, },
find_timezone(char) {
const timezones = this.settings('timezones')
if (timezones && typeof timezones[cid(char)] === 'number') return (timezones[cid(char)])
const match = (char.Description) ? char.Description.match(this.RE_TZ) : null
const int = match ? Number.parseInt(match[1] + match[2], 10) : 42
if (this.in(int, -12, 12)) return (int)
return (null)
},
//player_enters_room() { // Or if the mod is loaded while player is in the room //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]}"`)
if (!this.in(tz, -12, 12)) throw new Error('offset should be [-12,12]') if (!(tz in U.range(-12, 12))) throw new Error('offset should be [-12,12]')
const char = this.target2char(args[1]) const char = this.target2char(args[1])
char.MBCHC_LOCAL.TZ = tz if (char.MemberNumber === undefined) return
this.save_settings(s => { Settings.save(v1 => v1.TZ[char.MemberNumber] = tz)
if (!s.timezones) s.timezones = {} TZ.memo(char.MemberNumber)
s.timezones[cid(char)] = tz //char.MBCHC_LOCAL.TZ = tz
}) //this.save_settings(s => { // FIXME
}, // if (!s.timezones) s.timezones = {}
update_char(char) { // s.timezones[U.cid(char)] = tz
//char.cid = char.MemberNumber // Club ID (shorter) //})
char.dn = w.CharacterNickname(char) // DisplayName (shortcut)
if (!char.MBCHC_LOCAL) char.MBCHC_LOCAL = {}
if (!char.MBCHC_LOCAL.TZ) char.MBCHC_LOCAL.TZ = this.find_timezone(char)
}, },
command_mbchc(argline, cmdline, args) { command_mbchc(argline, cmdline, args) {
const mbchc = w.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())
const sub = mbchc.ensure(`unknown subcommand "${cmd}"`, () => mbchc.SUBCOMMANDS_MBCHC[cmd]) const sub = U.assert(`unknown subcommand "${cmd}"`, mbchc.SUBCOMMANDS_MBCHC[cmd])
sub.cb.call(mbchc, mbchc, args, argline, cmdline) sub.cb.call(mbchc, mbchc, args, argline, cmdline)
} catch (error) { } catch (error) {
mbchc.report(error) mbchc.report(error)
} }
}, },
command_activity(argline, cmdline, _) { command_activity(argline, cmdline, _) {
const mbchc = w.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})
@ -428,27 +410,27 @@ w.MBCHC = {
} }
}, },
command_do(argline, cmdline, args) { command_do(argline, cmdline, args) {
const mbchc = w.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
const zones = mbchc.ensure(`unknown verb "${verb}"`, () => mbchc.DO_DATA.verbs[verb]) const zones = U.assert(`unknown verb "${verb}"`, mbchc.DO_DATA.verbs[verb])
if (Object.keys(zones).length === 1) { if (Object.keys(zones).length === 1) {
if (!target) target = zone if (!target) target = zone
zone = mbchc.MAP_ZONES[Object.keys(zones)[0]][0] zone = mbchc.MAP_ZONES[Object.keys(zones)[0]][0]
} }
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 = U.assert(`unknown zone "${zone}"`, mbchc.DO_DATA.zones[zone])
const types = mbchc.ensure(`zone "${zone}" invalid for "${verb}"`, () => zones[ag]) const types = U.assert(`zone "${zone}" invalid for "${verb}"`, zones[ag])
let char = w.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 = w.ActivityAllowedForGroup(char, ag) const available = W.ActivityAllowedForGroup(char, ag)
//const toy = w.InventoryGet(w.Player, 'ItemHands') //const toy = w.InventoryGet(w.Player, 'ItemHands')
//if (toy && toy.Asset.Name === 'SpankingToys') available.push(w.AssetAllActivities(char.AssetFamily).find(a => a.Name === w.InventorySpankingToysGetActivity?.(w.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 = U.assert(`zone "${zone}" invalid for ("${verb}" "${type}")`, types[type])
const action = mbchc.ensure(`invalid action (${verb} ${zone} ${target})`, () => actions.find(name => available.find(a => a.Activity?.Name === name))) const action = U.assert(`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)
@ -456,9 +438,9 @@ w.MBCHC = {
}, },
bell() { bell() {
setTimeout(() => { setTimeout(() => {
style('#InputChat', s => s.outline = '') U.style('#InputChat', s => s.outline = '')
}, 100) }, 100)
style('#InputChat', s => s.outline = 'solid red') U.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())
@ -475,22 +457,22 @@ w.MBCHC = {
if (pref) this.complete([pref], false) if (pref) this.complete([pref], false)
this.comp_hint(options) this.comp_hint(options)
} else w.ElementValue('InputChat', w.ElementValue('InputChat').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 ? ' ' : ''}`))
}, },
/** /**
* Displays strings as completion hint * Displays strings as completion hint
* @param {string[]} options List of words to display. The order will be modified without copy. * @param {string[]} options List of words to display. The order will be modified without copy.
* @returns {void} * @returns {undefined}
*/ */
comp_hint(options) { comp_hint(options) {
if (options.length === 0) return if (options.length === 0) return
this.COMP_HINT.innerHTML = '<div>' + options.sort().reverse().map(s => `<div>${s}</div>`).join('') + '</div>' this.COMP_HINT.innerHTML = '<div>' + options.sort().reverse().map(s => `<div>${s}</div>`).join('') + '</div>'
this.COMP_HINT.style.display = 'block' this.COMP_HINT.style.display = 'block'
w.ElementSetDataAttribute(this.COMP_HINT.id, 'colortheme', (w.Player.ChatSettings.ColorTheme || 'Light')) W.ElementSetDataAttribute(this.COMP_HINT.id, 'colortheme', (W.Player.ChatSettings?.ColorTheme || 'Light'))
const rescroll = w.ElementIsScrolledToEnd('TextAreaChatLog') const rescroll = W.ElementIsScrolledToEnd('TextAreaChatLog')
w.ChatRoomResize(false) W.ChatRoomResize(false)
if (rescroll) w.ElementScrollToEnd('TextAreaChatLog') if (rescroll) W.ElementScrollToEnd('TextAreaChatLog')
this.COMP_HINT.firstChild?.lastChild?.scrollIntoView({behaviour: 'instant'}) this.COMP_HINT.firstChild?.lastChild?.scrollIntoView({behaviour: 'instant'})
}, },
@ -504,12 +486,12 @@ w.MBCHC = {
comp_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'
w.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 w.ChatRoomCharacter) { for (const c of W.ChatRoomCharacter) {
if ((c.IsPlayer() && !me2) || (check_perms && !w.ServerChatRoomGetAllowItem(w.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)
} }
@ -519,12 +501,12 @@ w.MBCHC = {
}, },
complete_common() { complete_common() {
// w.ElementValue('InputChat') will strip the trailing whitespace // w.ElementValue('InputChat') will strip the trailing whitespace
const E = document.querySelector('#InputChat') const E = D.querySelector('#InputChat')
if (!(E && E instanceof HTMLTextAreaElement)) throw new Error('somehow InputChat is broken') if (!(E && E instanceof HTMLTextAreaElement)) throw new Error('somehow InputChat is broken')
return ([this, E.value, this.tokenise(E.value)]) return ([this, E.value, this.tokenise(E.value)])
}, },
complete_mbchc(_args, _locase, _cmdline) { complete_mbchc(_args, _locase, _cmdline) {
const [mbchc, _input, tokens] = w.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()
@ -539,11 +521,11 @@ w.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([cid(w.Player).toString()])) // Target is always the player if (me2 && actions.others.length === 0) return (this.complete([U.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] = w.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
@ -566,22 +548,22 @@ w.MBCHC = {
mbchc.bell() mbchc.bell()
}, },
complete_fbc_anim(_args, _locase, _cmdline) { complete_fbc_anim(_args, _locase, _cmdline) {
const [mbchc, _input, tokens] = w.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(w.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] = w.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(w.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] = [w.ElementValue('InputChat'), w.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
@ -589,15 +571,15 @@ w.MBCHC = {
const ids = history.map((t, i) => ({t, i})).filter(r => r.t.startsWith(history.at(-1))).map(r => r.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 > w.ChatRoomLastMessageIndex : id < w.ChatRoomLastMessageIndex) const found = ids.find(id => (down) ? id > W.ChatRoomLastMessageIndex : id < W.ChatRoomLastMessageIndex)
if (!found) return (this.bell()) if (!found) return (this.bell())
w.ElementValue('InputChat', history[found]) W.ElementValue('InputChat', history[found])
w.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 (D.activeElement === D.body) return true
if (document.activeElement.id !== 'MainCanvas') return false if (D.activeElement?.id !== 'MainCanvas') return false
return !w.ChatRoomMapViewIsActive() 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
@ -607,8 +589,8 @@ w.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 (style('#InputChat', s => s.display) !== 'inline') return // Input chat missing if (U.style('#InputChat', s => s.display) !== 'inline') return // Input chat missing
w.ElementFocus('InputChat') W.ElementFocus('InputChat')
}, },
loader() { loader() {
if (this.remove_load_hook) { if (this.remove_load_hook) {
@ -617,18 +599,21 @@ w.MBCHC = {
} }
if (this.LOADED) return if (this.LOADED) return
U.with(/** @type {MBCHC.Settings.V0} */ (W.Player.OnlineSettings), os => os?.MBCHC && Settings.migrate_0_1(os))
// Calculated values // Calculated values
const COMMANDS = [ const COMMANDS = [
{Tag: 'mbchc', Description: ': Utility functions ("/mbchc" for help)', Action: this.command_mbchc, AutoComplete: this.complete_mbchc}, {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: '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.CommandsKey = CommandsKey /* eslint-disable-line no-undef */ // window.CommandsKey is undefined this.CommandsKey = CommandsKey
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 `
this.COMP_HINT = document.createElement('div') this.COMP_HINT = D.createElement('div')
this.COMP_HINT.id = 'mbchcCompHint' this.COMP_HINT.id = 'mbchcCompHint'
const css = document.createElement('style') const css = D.createElement('style')
css.textContent = ` css.textContent = `
#TextAreaChatLog .mbchc, #TextAreaChatLog .mbchc { #TextAreaChatLog .mbchc, #TextAreaChatLog .mbchc {
background-color: ${this.RGB_POLLY}; background-color: ${this.RGB_POLLY};
@ -659,47 +644,48 @@ w.MBCHC = {
} }
` `
document.head.append(css) D.head.append(css)
// Actions // Actions
this.calculate_maps() this.calculate_maps()
//w.Player.MBCHC = {VERSION: this.VERSION} //w.Player.MBCHC = {VERSION: this.VERSION}
w.CommandCombine(COMMANDS) W.CommandCombine(COMMANDS)
// Hooks // Hooks
this.remove_fbc_hook = this.before('MainRun', () => w.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) {
w.CurrentCharacter = this.cid2char(this.LAST_HACKED) W.CurrentCharacter = this.cid2char(this.LAST_HACKED)
this.LAST_HACKED = null this.LAST_HACKED = null
w.ChatRoomTryToTakeSuitcase() W.ChatRoomTryToTakeSuitcase()
} }
}) })
this.before('ChatRoomSendChat', () => { this.before('ChatRoomSendChat', () => {
let input = w.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)
w.ElementValue('InputChat', input) W.ElementValue('InputChat', input)
} }
}) })
this.after('ChatRoomSendChat', () => { this.after('ChatRoomSendChat', () => {
const history = w.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()
w.ChatRoomLastMessageIndex -= 1 W.ChatRoomLastMessageIndex -= 1
} }
}) })
this.before('ChatRoomCharacterViewDrawOverlay', (C, CharX, CharY, Zoom, _Pos) => { this.before('ChatRoomCharacterViewDrawOverlay', (C, CharX, CharY, Zoom, _Pos) => {
// if (w.ChatRoomHideIconState < 1 && C.MBCHC) { // if (w.ChatRoomHideIconState < 1 && C.MBCHC) {
// w.DrawRect(CharX + (175 * Zoom), CharY, 50 * Zoom, 50 * Zoom, C.MBCHC.VERSION === w.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 (w.ChatRoomHideIconState < 1 && C.MBCHC_LOCAL && typeof C.MBCHC_LOCAL.TZ === 'number') { const ctz = TZ.for(C)
const hours = new Date(w.CommonTime() + this.UTC_OFFSET + (C.MBCHC_LOCAL.TZ * 60 * 60 * 1000)).getHours() if (W.ChatRoomHideIconState < 1 && ctz !== undefined) {
w.DrawTextFit(hours < 10 ? '0' + hours.toString() : hours.toString(), CharX + (200 * Zoom), CharY + (25 * Zoom), 46 * Zoom, 'white', 'black') const hours = new Date(W.CommonTime() + this.UTC_OFFSET + (ctz * 60 * 60 * 1000)).getHours()
W.DrawTextFit(hours < 10 ? '0' + hours.toString() : hours.toString(), CharX + (200 * Zoom), CharY + (25 * Zoom), 46 * Zoom, 'white', 'black')
} }
}) })
this.after('ChatRoomCreateElement', () => this.COMP_HINT.parentElement || document.body.append(this.COMP_HINT)) this.after('ChatRoomCreateElement', () => this.COMP_HINT.parentElement || D.body.append(this.COMP_HINT))
this.before('ChatRoomClearAllElements', () => { this.before('ChatRoomClearAllElements', () => {
this.comp_hint_hide() this.comp_hint_hide()
this.COMP_HINT.remove() this.COMP_HINT.remove()
@ -708,33 +694,33 @@ w.MBCHC = {
this.comp_hint_hide() this.comp_hint_hide()
}) })
this.after('ChatRoomResize', () => { this.after('ChatRoomResize', () => {
if (w.CharacterGetCurrent() === null && w.CurrentScreen === 'ChatRoom' && document.querySelector('#InputChat') && document.querySelector('#TextAreaChatLog') && this.comp_hint_visible()) { // Upstream if (W.CharacterGetCurrent() === null && W.CurrentScreen === 'ChatRoom' && D.querySelector('#InputChat') && D.querySelector('#TextAreaChatLog') && this.comp_hint_visible()) { // Upstream
const fontsize = ChatRoomFontSize /* eslint-disable-line no-undef */ // window.ChatRoomFontSize is undefined const fontsize = ChatRoomFontSize
//w.ElementPositionFix('TextAreaChatLog', fontsize, 1005, 66, 988, 630) //w.ElementPositionFix('TextAreaChatLog', fontsize, 1005, 66, 988, 630)
//w.ElementPositionFix(this.COMP_HINT.id, fontsize, 1005, 701, 988, 200) //w.ElementPositionFix(this.COMP_HINT.id, fontsize, 1005, 701, 988, 200)
w.ElementPositionFix(this.COMP_HINT.id, fontsize, 800, 65, 200, 835) W.ElementPositionFix(this.COMP_HINT.id, fontsize, 800, 65, 200, 835)
//this.COMP_HINT.style.display = 'flex' //this.COMP_HINT.style.display = 'flex'
} }
}) })
document.addEventListener('keydown', event => this.focus_chat(event)) D.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
w.MBCHC.comp_hint_hide() W.MBCHC.comp_hint_hide()
if ((w.KeyPress === 33) || (w.KeyPress === 34)) { // Better history if ((W.KeyPress === 33) || (W.KeyPress === 34)) { // Better history
event.preventDefault() event.preventDefault()
return (w.MBCHC.history(w.KeyPress - 33)) return (W.MBCHC.history(W.KeyPress - 33))
} }
if (w.MBCHC.HISTORY_MODE) { if (W.MBCHC.HISTORY_MODE) {
w.ChatRoomLastMessage.pop() W.ChatRoomLastMessage.pop()
w.MBCHC.HISTORY_MODE = false W.MBCHC.HISTORY_MODE = false
} }
return (next(nextargs)) return (next(nextargs))
}) })
// Chat room handlers // Chat room handlers
w.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})
@ -742,7 +728,7 @@ w.MBCHC = {
}}) }})
//w.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 === cid(w.Player))) this.player_enters_room() // if ((data.Type === 'Action') && (data.Content === 'ServerEnter') && (data.Sender === U.cid(w.Player))) this.player_enters_room()
// return false // return false
// }, // },
//}) //})
@ -752,7 +738,7 @@ w.MBCHC = {
// return false // return false
// }, // },
//}) //})
w.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 return false
@ -762,15 +748,15 @@ w.MBCHC = {
// Footer // Footer
this.LOADED = true this.LOADED = true
this.log('info', `loaded version ${this.VERSION}`) this.log('info', `loaded version ${this.VERSION}`)
if ((w.CurrentModule === 'Online') && (w.CurrentScreen === 'ChatRoom')) { //if ((W.CurrentModule === 'Online') && (W.CurrentScreen === 'ChatRoom')) {
for (const c of w.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 (!w.AsylumGGTSSAddItems) throw new Error('AsylumGGTSSAddItems() not found, aborting MBCHC loading') if (!W.AsylumGGTSSAddItems) throw new Error('AsylumGGTSSAddItems() not found, aborting MBCHC loading')
if (!w.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 = w.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)
@ -788,13 +774,9 @@ w.MBCHC = {
} }
return result return result
}) })
// 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. U.current() === 'Character/Login' ? this.remove_load_hook = this.before('AsylumGGTSSAddItems', () => this.loader()) :this.loader()
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())
} else this.loader()
}, },
} }
w.MBCHC.preloader() W.MBCHC.preloader()

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "mbchc", "name": "mbchc",
"version": "0.0.12", "version": "105.13.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "mbchc", "name": "mbchc",
"version": "0.0.12", "version": "105.13.0",
"license": "SEE LICENSE IN LICENSE.", "license": "SEE LICENSE IN LICENSE.",
"devDependencies": { "devDependencies": {
"bc-stubs": "^105.0.0", "bc-stubs": "^105.0.0",

View File

@ -1,6 +1,6 @@
{ {
"name": "mbchc", "name": "mbchc",
"version": "0.0.12", "version": "105.13.0",
"description": "Mute's Bondage Club Hacks Collection", "description": "Mute's Bondage Club Hacks Collection",
"type": "module", "type": "module",
"devDependencies": { "devDependencies": {
@ -9,12 +9,37 @@
"bondage-club-mod-sdk": "^1.2.0" "bondage-club-mod-sdk": "^1.2.0"
}, },
"license": "SEE LICENSE IN LICENSE.", "license": "SEE LICENSE IN LICENSE.",
"xo": { "eslintConfig": {
"env": [ "root": true,
"browser", "extends": [
"node" "xo",
"xo-typescript"
], ],
"parser": "@typescript-eslint/parser",
"parserOptions": { "project": ["./jsconfig.json"] },
"plugins": ["@typescript-eslint"],
"globals": {"GM": true, "GM_info": true},
"rules": { "rules": {
"@typescript-eslint/brace-style": "off",
"@typescript-eslint/comma-dangle": ["error", "only-multiline"],
"@typescript-eslint/consistent-type-definitions": "off",
"@typescript-eslint/consistent-type-imports": "off",
"@typescript-eslint/member-delimiter-style": "off",
"@typescript-eslint/naming-convention": "off",
"@typescript-eslint/no-confusing-void-expression": ["error", {
"ignoreVoidOperator": true
}],
"@typescript-eslint/no-meaningless-void-operator": "off",
"@typescript-eslint/object-curly-spacing": "off",
"@typescript-eslint/padding-line-between-statements": "off",
"@typescript-eslint/semi": "off",
"@typescript-eslint/space-before-function-paren": "off",
"@typescript-eslint/strict-boolean-expressions": ["error", {
"allowString": false,
"allowNumber": false,
"allowNullableObject": false
}],
"array-element-newline": "off",
"brace-style": "off", "brace-style": "off",
"camelcase": "off", "camelcase": "off",
"capitalized-comments": "off", "capitalized-comments": "off",
@ -28,11 +53,27 @@
"argsIgnorePattern": "^_", "argsIgnorePattern": "^_",
"destructuredArrayIgnorePattern": "^_" "destructuredArrayIgnorePattern": "^_"
}], }],
"no-void": "off",
"padding-line-between-statements": "off", "padding-line-between-statements": "off",
"object-curly-newline": "off",
"one-var": "off",
"one-var-declaration-per-line": "off",
"semi": "off", "semi": "off",
"space-before-function-paren": "off",
"spaced-comment": "off", "spaced-comment": "off",
"unicorn/no-array-for-each": "off",
"unicorn/no-array-reduce": "off", "unicorn/no-array-reduce": "off",
"unicorn/prefer-module": "off",
"unicorn/prefer-top-level-await": "off",
"fake/fuck-commas": "off" "fake/fuck-commas": "off"
},
"overrides": [
{
"files": ["*.d.ts"],
"rules": {
"no-unused-vars": "off"
} }
} }
]
}
} }

10
typedef.d.ts vendored
View File

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