Mute
16308eccf1
[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.
783 lines
35 KiB
JavaScript
783 lines
35 KiB
JavaScript
/** @type {MBCHC.Root} */ const W = window, D = W.document
|
|
if (W.MBCHC !== undefined) throw new Error('MBCHC found, aborting loading')
|
|
|
|
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
|
|
|
|
/** @implements {MBCHC.Interval} */ class Interval { proxy = new Proxy({}, this); min = 0; max = 0; mini = false; maxi = false
|
|
/** @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(),
|
|
cid: char => char.MemberNumber,
|
|
dn: char => W.CharacterNickname(char),
|
|
current: () => `${W.CurrentModule}/${W.CurrentScreen}`,
|
|
with: (v, cb) => cb(v),
|
|
true(cb) {cb(); return true},
|
|
mutate(v, cb) {cb(v); return 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 = {
|
|
migrate_0_1(v0) { // I hate change
|
|
U.with(v0.MBCHC.timezones, tz => tz !== undefined && this.save(v1 => void U.each(tz, (k, v) => v1.TZ[k] ||= v)))
|
|
W.ServerAccountUpdate.QueueData({OnlineSettings: U.rm(v0, 'MBCHC')})
|
|
return U.true(() => void console.warn('MBCHC: settings migration done (v0 -> v1). This should never appear again.'))
|
|
},
|
|
save(cb = undefined) {W.Player.ExtensionSettings.MBCHC ||= {}; cb?.(this.v1); W.ServerPlayerExtensionSettingsSync('MBCHC'); return true},
|
|
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(),
|
|
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),
|
|
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)
|
|
// =================================================================================
|
|
// v legacy mess
|
|
|
|
W.MBCHC = {
|
|
VERSION,
|
|
Settings,
|
|
NEXT_MESSAGE: 1,
|
|
LOG_MESSAGES: false,
|
|
RETHROW: false,
|
|
LOADED: false,
|
|
AUTOHACK_ENABLED: false,
|
|
LAST_HACKED: null,
|
|
HISTORY_MODE: false,
|
|
RE_PREF_ACTIVITY_ME: /^@/,
|
|
RE_PREF_ACTIVITY: /^@@/,
|
|
RE_ACT_CIDS: /^<(\d+)?:(\d+)?>/,
|
|
RE_ALL_LEFT: /^<+$/,
|
|
RE_ALL_RIGHT: /^>+$/,
|
|
RE_SPACES: /\s{2,}/g,
|
|
RE_LAST_WORD: /(^|\s)(\S*)$/,
|
|
RE_LAST_LETTER: /\w$/,
|
|
RGB_MUTE: '#6c2132',
|
|
RGB_POLLY: '#81b1e7',
|
|
UTC_OFFSET: new Date().getTimezoneOffset() * 60 * 1000,
|
|
MAP_ACTIONS: { // ActivityFemale3DCG
|
|
// action
|
|
'nod|yes': {Head: {self: 'Nod'}},
|
|
no: {Head: {self: 'Wiggle'}},
|
|
moan: {Mouth: {self: 'MoanGag'}},
|
|
mumble: {Mouth: {self: 'MoanGagTalk'}},
|
|
whimper: {Mouth: {self: 'MoanGagWhimper'}},
|
|
groan: {Mouth: {self: 'MoanGagGroan'}},
|
|
scream: {Mouth: {self: 'MoanGagAngry'}},
|
|
giggle: {Mouth: {self: 'MoanGagGiggle'}},
|
|
struggle: {Arms: {self: 'StruggleArms'}},
|
|
thrash: {Legs: {self: 'StruggleLegs'}},
|
|
|
|
// Action zone
|
|
'wiggle|shake': {'Arms,Breast,Boots,Butt,Ears,Feet,Hands,Nose,Pelvis,Torso': {self: 'Wiggle'}},
|
|
|
|
// Action target
|
|
whisper: {Ears: {others: 'Whisper'}},
|
|
choke: {Neck: {all: 'Choke'}},
|
|
brush: {Head: {all: 'TakeCare'}},
|
|
french: {Mouth: {others: 'FrenchKiss'}},
|
|
sit: {Legs: {others: 'Sit'}},
|
|
rim: {Butt: {others: 'MasturbateTongue'}},
|
|
press: {Butt: {others: 'Step'}},
|
|
rest: {Torso: {others: 'Step'}},
|
|
pet: {Head: {all: 'Pet'}},
|
|
boop: {Nose: {all: 'Pet'}},
|
|
cuddle: {Arms: {others: 'Cuddle'}},
|
|
nuzzle: {Nose: {others: 'Cuddle'}},
|
|
grab: {Arms: {others: 'Grope'}},
|
|
clean: {Mouth: {all: 'Caress'}},
|
|
lap: {Legs: {others: 'RestHead'}},
|
|
lean: {Breast: {others: 'RestHead'}},
|
|
peck: {Mouth: {others: 'PoliteKiss'}},
|
|
|
|
// Action zone target
|
|
item: {
|
|
'Breast,Butt,Feet,Legs': {all: 'SpankItem|TickleItem|RubItem|RollItem|MasturbateItem|PourItem|ShockItem|Inject'},
|
|
'Nipples,Pelvis': {all: 'SpankItem|TickleItem|RubItem|RollItem|MasturbateItem|PourItem|ShockItem'},
|
|
Arms: {all: 'SpankItem|TickleItem|RubItem|RollItem|PourItem|ShockItem|Inject'},
|
|
Boots: {all: 'SpankItem|TickleItem|RubItem|RollItem|PourItem|ShockItem'},
|
|
'Ears,Mouth': {all: 'TickleItem|RubItem|RollItem'},
|
|
'Hood,Nose': {all: 'TickleItem|RubItem'},
|
|
Neck: {all: 'TickleItem|RubItem|RollItem|Inject'},
|
|
Torso: {all: 'SpankItem|TickleItem|RubItem|RollItem|PourItem|ShockItem'},
|
|
Vulva: {all: 'SpankItem|TickleItem|RubItem|MasturbateItem|ShockItem'},
|
|
VulvaPiercings: {all: 'SpankItem|TickleItem|RubItem|MasturbateItem|ShockItem|Inject'},
|
|
},
|
|
kiss: {
|
|
Mouth: {others: 'GagKiss|Kiss|GaggedKiss'},
|
|
'Boots,Hands': {self: 'PoliteKiss', others: 'PoliteKiss|GaggedKiss'},
|
|
'Arms,Breast,Nipples': {self: 'Kiss', others: 'Kiss|GaggedKiss'},
|
|
'Butt,Ears,Feet,Head,Legs,Neck,Nose,Pelvis,Torso,Vulva,VulvaPiercings': {others: 'Kiss|GaggedKiss'},
|
|
},
|
|
smooch: {'Hands,Boots': {all: 'Kiss'}},
|
|
'nibble|chew': {'Arms,Hands,Boots,Mouth,Nipples': {all: 'Nibble'}, 'Ears,Feet,Legs,Neck,Nose,Pelvis,Torse,Vulva,VulvaPiercings': {others: 'Nibble'}},
|
|
'slap|spank': {'Head,Breast,Vulva,VulvaPiercings': {all: 'Slap'}, 'Arms,Boots,Butt,Feet,Hands,Legs,Pelvis,Torso': {all: 'Spank'}},
|
|
tickle: {'Arms,Boots,Breast,Feet,Legs,Neck,Pelvis,Torso': {all: 'Tickle'}},
|
|
massage: {'Arms,Boots,Feet,Legs,Neck,Pelvis,Torso': {all: 'MassageHands'}},
|
|
lick: {'Arms,Boots,Breast,Hands,Mouth,Nipples': {all: 'Lick'}, 'Ears,Feet,Legs,Neck,Nose,Pelvis,Torso,Vulva,VulvaPiercings': {others: 'Lick'}},
|
|
suck: {'Nipples,Hands,Boots': {all: 'Suck'}},
|
|
bite: {'Arms,Boots,Feet,Hands,Legs,Mouth': {all: 'Bite'}, 'Breast,Butt,Ears,Head,Neck,Nipples,Nose,Torso': {others: 'Bite'}},
|
|
pinch: {'Arms,Ears,Nipples,Nose,Pelvis': {all: 'Pinch'}},
|
|
clamp: {Mouth: {all: 'HandGag'}, Nose: {all: 'Choke'}},
|
|
step: {'Breast,Neck,Pelvis': {others: 'Step'}},
|
|
pull: {'Head,Nose,Nipples': {all: 'Pull'}},
|
|
grope: {'Butt,Breast': {all: 'Grope'}, 'Feet,Legs,Pelvis': {others: 'Grope'}},
|
|
rub: {'Head,Torso': {others: 'Rub'}, Nose: {all: 'Rub'}, Legs: {self: 'Wiggle'}, Hands: {self: 'Caress'}},
|
|
caress: {Hands: {others: 'Caress'}, 'Arms,Breast,Butt,Ears,Feet,Head,Legs,Neck,Nipples,Nose,Pelvis,Torso,Vulva,VulvaPiercings': {all: 'Caress'}},
|
|
polish: {'Hands,Boots': {all: 'TakeCare'}},
|
|
foot: {'Head,Nose': {others: 'Step'}, 'Torso,Boots': {others: 'MassageFeet'}, 'Vulva,VulvaPiercings': {others: 'MasturbateFoot'}},
|
|
fist: {'Vulva,Butt': {all: 'MasturbateFist'}},
|
|
fuck: {'Mouth,Vulva,Butt': {others: 'PenetrateSlow'}}, // Peg?
|
|
pound: {'Mouth,Vulva,Butt': {others: 'PenetrateFast'}},
|
|
tongue: {'Vulva,VulvaPiercings': {others: 'MasturbateTongue'}},
|
|
finger: {'Breast,Butt,Vulva,VulvaPiercings': {all: 'MasturbateHand'}},
|
|
},
|
|
MAP_ZONES: {
|
|
ItemBoots: ['foot', 'feet', 'boot', 'boots', 'shoe', 'shoes', 'toes', 'toenails', 'sole', 'soles', 'heel', 'heels'],
|
|
ItemFeet: ['leg', 'legs', 'ankle', 'ankles'],
|
|
ItemLegs: ['hips', 'hip', 'thighs', 'thigh'],
|
|
ItemVulva: ['vulva', 'pussy'],
|
|
ItemVulvaPiercings: ['clit', 'clitoris'],
|
|
ItemButt: ['butt', 'ass'],
|
|
ItemPelvis: ['tummy', 'pelvis'],
|
|
ItemTorso: ['body', 'torso', 'back', 'ribs'],
|
|
ItemBreast: ['breast', 'breasts', 'boob', 'boobs', 'booby', 'boobie', 'boobies', 'tit', 'tits', 'titty', 'tittie', 'titties'],
|
|
ItemNipples: ['nip', 'nips', 'nipple', 'nipples'],
|
|
ItemHands: ['hand', 'hands', 'fingers', 'fingernails', 'nails'],
|
|
ItemArms: ['arm', 'arms', 'elbow', 'elbows'],
|
|
ItemNeck: ['neck'],
|
|
ItemMouth: ['mouth', 'lip', 'lips', 'teeth', 'tongue', 'gag', 'cheek', 'cheeks'],
|
|
ItemNose: ['nose', 'nostrils'],
|
|
ItemEars: ['ear', 'ears', 'earlobe', 'earlobes'],
|
|
ItemHead: ['head', 'face', 'hair', 'eyes', 'forehead'],
|
|
},
|
|
FBC_TESTER_PATCHES: [
|
|
[/^\^('s)?( )?/g, '^SourceCharacter$1\\s+'],
|
|
[/([^\\])\$/g, '$1\\.?$$'],
|
|
],
|
|
SUBCOMMANDS_MBCHC: {
|
|
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])},
|
|
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: () => Settings['purge!']()},
|
|
// if (W.Player.ExtensionSettings.MBCHC) {
|
|
// delete W.Player.ExtensionSettings.MBCHC
|
|
// mbchc.save_settings() // FIXME
|
|
// }
|
|
//}},
|
|
},
|
|
calculate_maps() {
|
|
this.DO_DATA = {verbs: {}, zones: {}}
|
|
for (const [verbs, data] of Object.entries(this.MAP_ACTIONS)) {
|
|
const unwound = {}
|
|
for (const [zones, actions] of Object.entries(data)) {
|
|
const all = (actions.all) ? actions.all.split('|') : []
|
|
const processed = {self: (actions.self) ? actions.self.split('|').concat(all) : all, others: (actions.others) ? actions.others.split('|').concat(all) : all}
|
|
for (const zone of zones.split(',')) unwound[`Item${zone}`] = processed
|
|
}
|
|
for (const verb of verbs.split('|')) this.DO_DATA.verbs[verb] = unwound
|
|
}
|
|
for (const [ag, zones] of Object.entries(this.MAP_ZONES)) for (const zone of zones) this.DO_DATA.zones[zone] = ag
|
|
},
|
|
log(level, message) {
|
|
console[level]('MBCHC: ' + String(message))
|
|
},
|
|
empty(text) {
|
|
if (!text) return (true)
|
|
if (String(text).trim().length === 0) return (true)
|
|
return (false)
|
|
},
|
|
normalise_message(text, options = {}) {
|
|
let result = text
|
|
if (options.trim) result = result.trim()
|
|
if (options.low) result = result.toLocaleLowerCase()
|
|
if (options.up) {
|
|
const first = result.at(0).toLocaleUpperCase()
|
|
const rest = result.slice(1)
|
|
result = first + rest
|
|
}
|
|
if (options.dot && this.RE_LAST_LETTER.test(result)) result = `${result}.`
|
|
return (result)
|
|
},
|
|
tokenise(text) {
|
|
return text.replace(this.RE_SPACES, ' ').split(' ')
|
|
},
|
|
inform(html, timeout = 60_000) {
|
|
W.ChatRoomSendLocal(`<div class="mbchc">${html}</div>`, timeout)
|
|
},
|
|
report(x) {
|
|
this.inform(`${x.toString()}`)
|
|
if (this.RETHROW) throw x
|
|
},
|
|
cid2char(id) {
|
|
id = Number.parseInt(id, 10)
|
|
if (id === U.cid(W.Player)) return (W.Player)
|
|
return U.assert(`character ${id} not found in the room`, W.ChatRoomCharacter.find(c => U.cid(c) === id))
|
|
},
|
|
pos2char(pos) {
|
|
if (!(pos in U.range(0, W.ChatRoomCharacter.length - 1))) throw new Error(`invalid position ${pos}`)
|
|
return (W.ChatRoomCharacter[pos])
|
|
},
|
|
rel2char(target) {
|
|
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
|
|
if (this.RE_ALL_LEFT.test(target)) pos = me - target.length
|
|
if (this.RE_ALL_RIGHT.test(target)) pos = me + target.length
|
|
if (pos === null) throw new Error(`failed to parse target "${target}"`)
|
|
pos %= W.ChatRoomCharacter.length
|
|
if (pos < 0) pos += W.ChatRoomCharacter.length
|
|
return (this.pos2char(pos))
|
|
},
|
|
target2char(target) { // Target should be lowcase
|
|
const input = target
|
|
if (this.empty(target)) return (W.Player)
|
|
const int = Number.parseInt(target, 10)
|
|
target = String(target)
|
|
let found = []
|
|
if (target.startsWith('=')) return (this.cid2char(target.slice(1)))
|
|
if (target.startsWith('<') || target.startsWith('>')) return (this.rel2char(target))
|
|
if (!Number.isNaN(int) && int.toString() === target) { // We got a number
|
|
if (int in U.range(0, 9)) return (this.pos2char(int))
|
|
if (int in U.range(11, 15)) return (this.pos2char(int - 11))
|
|
if (int in U.range(21, 25)) return (this.pos2char(int - 16))
|
|
found.push(...W.ChatRoomCharacter.filter(c => U.cid(c).toString().includes(target)))
|
|
}
|
|
|
|
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.Nickname && (c.Nickname.toLocaleLowerCase().includes(target)))) // eslint-disable-line unicorn/no-array-push-push
|
|
const map = {}
|
|
for (const c of found) {
|
|
if (!map[U.cid(c)]) map[U.cid(c)] = c
|
|
}
|
|
|
|
found = Object.values(map)
|
|
if (found.length === 0) throw new Error(`target "${input}": no match`)
|
|
if (found.length > 1) throw new Error(`target "${input}": multiple matches (${found.map(c => `${U.cid(c)}|${c.Name}|${c.Nickname || c.Name}`).join(',')})`)
|
|
return (found[0])
|
|
},
|
|
char2targets(char) {
|
|
const [result, id] = [new Set(), U.cid(char).toString()]
|
|
result.add(id).add(`=${id}`)
|
|
for (const t of this.tokenise(char.Name)) {
|
|
result.add(t)
|
|
result.add(`@${t}`)
|
|
}
|
|
|
|
if (char.Nickname) for (const t of this.tokenise(char.Nickname)) {
|
|
result.add(t)
|
|
result.add(`@${t}`)
|
|
}
|
|
|
|
return result
|
|
},
|
|
//get settings() {return Settings.v1},
|
|
donate_data(target) {
|
|
const char = this.target2char(target)
|
|
if (char.IsPlayer()) throw new Error('target must not be you')
|
|
if (!char.IsRestrained()) throw new Error('target must be bound')
|
|
const cost = Math.round(((Math.random() * 10) + 15))
|
|
if (W.Player.Money < cost) throw new Error('not enough money')
|
|
W.CharacterChangeMoney(W.Player, -cost)
|
|
W.ServerSend('ChatRoomChat', {Content: 'ReceiveSuitcaseMoney', Type: 'Hidden', Target: U.cid(char)})
|
|
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) {
|
|
try {
|
|
if (!W.ActivityAllowed()) throw new Error('activities disabled in this room')
|
|
if (!W.ServerChatRoomGetAllowItem(W.Player, char)) throw new Error('no permissions')
|
|
char.FocusGroup = U.assert('invalid AssetGroup', W.AssetGroupGet(char.AssetFamily, ag))
|
|
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')) {
|
|
// 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)
|
|
//} else w.ActivityRun(w.Player, char, char.FocusGroup, activity)
|
|
W.ActivityRun(W.Player, char, char.FocusGroup, activity)
|
|
} finally {
|
|
char.FocusGroup = null
|
|
}
|
|
},
|
|
replace_me(match, offset, string) {
|
|
const text = string.slice(1)
|
|
let suffix = ' '
|
|
if (text.startsWith('\'') || text.startsWith(' ')) suffix = ''
|
|
return `${W.MBCHC.PREF_ACTIVITY}<${U.cid(W.Player)}:>SourceCharacter${suffix}`
|
|
},
|
|
cid2dict(type, cid) {
|
|
return ({Tag: `${type}Character`, MemberNumber: cid, Text: U.dn(this.cid2char(cid))})
|
|
},
|
|
send_activity(message) {
|
|
const dict = [MISSING_PLAYER_DIALOG]
|
|
const cids = message.match(this.RE_ACT_CIDS)
|
|
if (cids) {
|
|
message = message.replace(this.RE_ACT_CIDS, '')
|
|
if (cids[1]) dict.push(this.cid2dict('Source', cids[1]))
|
|
if (cids[2]) dict.push(this.cid2dict('Target', cids[2]), this.cid2dict('Destination', cids[2]))
|
|
}
|
|
W.ServerSend('ChatRoomChat', {Type: 'Action', Content: message, Dictionary: dict})
|
|
},
|
|
//receive(data) {
|
|
// const char = this.cid2char(data.Sender)
|
|
// if (char.IsPlayer()) return true // This is our own message, sent back to us
|
|
// const payload = this.ensure('Empty message', () => data.Dictionary[0])
|
|
// switch (payload.type) {
|
|
// case 'greetings': case 'hello': {
|
|
// char.MBCHC = payload.value
|
|
// if (payload.type === 'greetings') this.hello(char)
|
|
// break
|
|
// }
|
|
// default: // If we don't know the type it may be from a newer version
|
|
// }
|
|
// return true
|
|
//},
|
|
//hello(char = null) {
|
|
// const payload = {type: 'greetings', value: w.Player.MBCHC}
|
|
// if (char) payload.type = 'hello'
|
|
// const message = {Content: 'MBCHC', Type: /** @type {const} */ ('Hidden'), Dictionary: [payload]}
|
|
// if (char) message.Target = char.cid
|
|
// w.ServerSend('ChatRoomChat', message)
|
|
//},
|
|
copy_fbc_trigger(trigger) {
|
|
const result = {
|
|
Type: 'Action',
|
|
Event: trigger.Event,
|
|
Matchers: trigger.Matchers.map(m => ({Tester: new RegExp(this.FBC_TESTER_PATCHES.reduce((ax, [f, r]) => ax.replaceAll(f, r), m.Tester.source), 'u')})),
|
|
}
|
|
return (result)
|
|
},
|
|
patch_fbc() {
|
|
this.remove_fbc_hook()
|
|
delete this.remove_fbc_hook
|
|
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
|
|
let cmd = W.Commands.find(c => c.Tag === 'anim')
|
|
if (cmd) cmd.AutoComplete = this.complete_fbc_anim
|
|
cmd = W.Commands.find(c => c.Tag === 'pose')
|
|
if (cmd) cmd.AutoComplete = this.complete_fbc_pose
|
|
},
|
|
//player_enters_room() { // Or if the mod is loaded while player is in the room
|
|
// this.hello()
|
|
//},
|
|
set_timezone(args) {
|
|
const tz = Number.parseInt(args[0], 10)
|
|
if (Number.isNaN(tz)) throw new Error(`invalid offset "${args[0]}"`)
|
|
if (!(tz in U.range(-12, 12))) throw new Error('offset should be [-12,12]')
|
|
const char = this.target2char(args[1])
|
|
if (char.MemberNumber === undefined) return
|
|
Settings.save(v1 => v1.TZ[char.MemberNumber] = tz)
|
|
TZ.memo(char.MemberNumber)
|
|
//char.MBCHC_LOCAL.TZ = tz
|
|
//this.save_settings(s => { // FIXME
|
|
// if (!s.timezones) s.timezones = {}
|
|
// s.timezones[U.cid(char)] = tz
|
|
//})
|
|
},
|
|
command_mbchc(argline, cmdline, args) {
|
|
const mbchc = W.MBCHC
|
|
try { // `this` is command object
|
|
if (args.length === 0) return (mbchc.inform(Object.entries(mbchc.SUBCOMMANDS_MBCHC).map(([cmd, sub]) => `<div>/mbchc ${cmd} ${sub.args ? Object.keys(sub.args).join(' ') : ''}: ${sub.desc}</div>`).join('')))
|
|
const cmd = String(args.shift())
|
|
const sub = U.assert(`unknown subcommand "${cmd}"`, mbchc.SUBCOMMANDS_MBCHC[cmd])
|
|
sub.cb.call(mbchc, mbchc, args, argline, cmdline)
|
|
} catch (error) {
|
|
mbchc.report(error)
|
|
}
|
|
},
|
|
command_activity(argline, cmdline, _) {
|
|
const mbchc = W.MBCHC
|
|
if (!mbchc.empty(argline)) {
|
|
try { // `this` is command object
|
|
const message = mbchc.normalise_message(cmdline.replace(mbchc.RE_ACTIVITY, ''), {trim: true, dot: true, up: true})
|
|
mbchc.send_activity(message)
|
|
} catch (error) {
|
|
mbchc.report(error)
|
|
}
|
|
}
|
|
},
|
|
command_do(argline, cmdline, args) {
|
|
const mbchc = W.MBCHC
|
|
try { // `this` is command object
|
|
if (args.length === 0) return (mbchc.inform('<div>Usage: /do VERB [ZONE] [TARGET]</div><div>Available verbs:</div>' + Object.keys(mbchc.MAP_ACTIONS).join(', ') + '<div>Available zones:</div>' + Object.keys(mbchc.DO_DATA.zones).join(', ')))
|
|
let [verb, zone, target] = args
|
|
const zones = U.assert(`unknown verb "${verb}"`, mbchc.DO_DATA.verbs[verb])
|
|
if (Object.keys(zones).length === 1) {
|
|
if (!target) target = zone
|
|
zone = mbchc.MAP_ZONES[Object.keys(zones)[0]][0]
|
|
}
|
|
|
|
if (!zone) throw new Error('zone missing')
|
|
const ag = U.assert(`unknown zone "${zone}"`, mbchc.DO_DATA.zones[zone])
|
|
const types = U.assert(`zone "${zone}" invalid for "${verb}"`, zones[ag])
|
|
let char = W.Player
|
|
if (target && ((types.self.length === 0) || (types.others.length > 0))) char = mbchc.target2char(target)
|
|
const type = char.IsPlayer() ? 'self' : 'others'
|
|
const available = W.ActivityAllowedForGroup(char, ag)
|
|
//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)))
|
|
const actions = U.assert(`zone "${zone}" invalid for ("${verb}" "${type}")`, types[type])
|
|
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)
|
|
} catch (error) {
|
|
mbchc.report(error)
|
|
}
|
|
},
|
|
bell() {
|
|
setTimeout(() => {
|
|
U.style('#InputChat', s => s.outline = '')
|
|
}, 100)
|
|
U.style('#InputChat', s => s.outline = 'solid red')
|
|
},
|
|
complete(options, space = true) {
|
|
if (options.length === 0) return (this.bell())
|
|
if (options.length > 1) {
|
|
const width = Math.max(...options.map(o => o.length))
|
|
let pref = null
|
|
for (let i = width; i > 0; i -= 1) {
|
|
const test = options[0].slice(0, i)
|
|
if (options.every(o => o.startsWith(test))) {
|
|
pref = test
|
|
break
|
|
}
|
|
}
|
|
|
|
if (pref) this.complete([pref], false)
|
|
this.comp_hint(options)
|
|
} else W.ElementValue('InputChat', W.ElementValue('InputChat').replace(this.RE_LAST_WORD, `$1${options[0]}${space ? ' ' : ''}`))
|
|
},
|
|
|
|
/**
|
|
* Displays strings as completion hint
|
|
* @param {string[]} options List of words to display. The order will be modified without copy.
|
|
* @returns {undefined}
|
|
*/
|
|
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() {
|
|
return (this.COMP_HINT.parentElement && this.COMP_HINT.style.display !== 'none')
|
|
},
|
|
comp_hint_hide() {
|
|
if (!this.comp_hint_visible()) return
|
|
this.COMP_HINT.style.display = 'none'
|
|
W.ChatRoomResize(false)
|
|
},
|
|
complete_target(token, me2 = true, check_perms = false) {
|
|
const [locase, found] = [token.toLocaleLowerCase(), new Set()]
|
|
for (const c of W.ChatRoomCharacter) {
|
|
if ((c.IsPlayer() && !me2) || (check_perms && !W.ServerChatRoomGetAllowItem(W.Player, c))) continue
|
|
for (const s of this.char2targets(c)) {
|
|
if (s.toLocaleLowerCase().startsWith(locase)) found.add(s)
|
|
}
|
|
}
|
|
|
|
this.complete(Array.from(found))
|
|
},
|
|
complete_common() {
|
|
// w.ElementValue('InputChat') will strip the trailing whitespace
|
|
const E = D.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) {
|
|
const [mbchc, _input, tokens] = W.MBCHC.complete_common(); // `this` is command object
|
|
if (tokens.length === 0) return
|
|
if (tokens.length < 2) return (mbchc.complete([`${mbchc.CommandsKey}${this.Tag}`]))
|
|
const subname = tokens[1].toLocaleLowerCase()
|
|
if (tokens.length < 3) return (mbchc.complete(Object.keys(mbchc.SUBCOMMANDS_MBCHC).filter(c => c.startsWith(subname)))) // Complete subcommand name
|
|
const sub = mbchc.SUBCOMMANDS_MBCHC[subname]
|
|
if (sub && sub.args) {
|
|
const argname = Object.keys(sub.args)[tokens.length - 3]
|
|
if (argname === 'TARGET') return (mbchc.complete_target(tokens.at(-1), false))
|
|
if (argname === '[TARGET]') return (mbchc.complete_target(tokens.at(-1)), true)
|
|
}
|
|
},
|
|
complete_do_target(actions, token) {
|
|
if (!actions) return
|
|
const me2 = (actions.self.length > 0)
|
|
if (me2 && actions.others.length === 0) return (this.complete([U.cid(W.Player).toString()])) // Target is always the player
|
|
this.complete_target(token, me2, true)
|
|
},
|
|
complete_do(_args, _locase, _cmdline) {
|
|
const [mbchc, _input, tokens] = W.MBCHC.complete_common(); // `this` is command object
|
|
if (tokens.length === 0) return
|
|
if (tokens.length < 2) return (mbchc.complete([`${mbchc.CommandsKey}${this.Tag}`]))
|
|
// Now, we *could* run a filter to exclude impossible activities, but it isn't very useful, and also seems like a lot of CPU to iterate over every action on every zone of every char in the room
|
|
let low = tokens[1].toLocaleLowerCase()
|
|
if (tokens.length < 3) return (mbchc.complete(Object.keys(mbchc.DO_DATA.verbs).filter(c => c.startsWith(low)))) // Complete verb
|
|
const ags = mbchc.DO_DATA.verbs[low]
|
|
if (!ags) return (mbchc.bell())
|
|
low = tokens[2].toLocaleLowerCase()
|
|
if (tokens.length < 4) { // Complete zone or target
|
|
if (Object.keys(ags).length < 2) return (mbchc.complete_do_target(ags[Object.keys(ags)[0]], tokens[2])) // Zone implied, complete target
|
|
const zones = Object.entries(mbchc.DO_DATA.zones).filter(([zone, ag]) => zone.startsWith(low) && ags[ag]).map(([zone, _ag]) => zone)
|
|
return (mbchc.complete(zones))
|
|
}
|
|
|
|
if (tokens.length < 5) { // Complete target where it belongs
|
|
if (Object.keys(ags).length < 2) return // Zone implied, target already given
|
|
return (mbchc.complete_do_target(ags[mbchc.DO_DATA.zones[low]], tokens[3]))
|
|
}
|
|
|
|
mbchc.bell()
|
|
},
|
|
complete_fbc_anim(_args, _locase, _cmdline) {
|
|
const [mbchc, _input, tokens] = W.MBCHC.complete_common(); // `this` is command object
|
|
if (tokens.length === 0) return
|
|
if (tokens.length < 2) return (mbchc.complete([`${mbchc.CommandsKey}${this.Tag}`]))
|
|
if (tokens.length > 2) return (mbchc.bell())
|
|
const anim = tokens[1].toLocaleLowerCase()
|
|
return (mbchc.complete(Object.keys(W.bce_EventExpressions).filter(a => a.toLocaleLowerCase().startsWith(anim))))
|
|
},
|
|
complete_fbc_pose(_args, _locase, _cmdline) {
|
|
const [mbchc, _input, tokens] = W.MBCHC.complete_common(); // `this` is command object
|
|
if (tokens.length === 0) return
|
|
if (tokens.length < 2) return (mbchc.complete([`${mbchc.CommandsKey}${this.Tag}`]))
|
|
const pose = tokens.at(-1).toLocaleLowerCase()
|
|
return (mbchc.complete(W.PoseFemale3DCG.map(p => p.Name).filter(p => p.toLocaleLowerCase().startsWith(pose))))
|
|
},
|
|
history(down) {
|
|
const [text, history] = [W.ElementValue('InputChat'), W.ChatRoomLastMessage]
|
|
if (!this.HISTORY_MODE) {
|
|
history.push(text)
|
|
this.HISTORY_MODE = true
|
|
}
|
|
|
|
const ids = history.map((t, i) => ({t, i})).filter(r => r.t.startsWith(history.at(-1))).map(r => r.i)
|
|
if (!down) ids.reverse()
|
|
const found = ids.find(id => (down) ? id > W.ChatRoomLastMessageIndex : id < W.ChatRoomLastMessageIndex)
|
|
if (!found) return (this.bell())
|
|
W.ElementValue('InputChat', history[found])
|
|
W.ChatRoomLastMessageIndex = found
|
|
},
|
|
focus_chat_checks() { // we only want to catch chatlog and canvas (no map though) keypresses
|
|
if (D.activeElement === D.body) return true
|
|
if (D.activeElement?.id !== 'MainCanvas') return false
|
|
return !W.ChatRoomMapViewIsActive()
|
|
},
|
|
focus_chat_whitelist(event) {
|
|
if (event.ctrlKey && event.key === 'v') return true // Ctrl+V should paste
|
|
return false
|
|
},
|
|
focus_chat(event) {
|
|
if (event.repeat) return // Only unique presses please
|
|
if (!this.focus_chat_checks()) return
|
|
if ([event.altKey, event.ctrlKey, event.metaKey].some(Boolean) && !this.focus_chat_whitelist(event)) return // Alt, ctrl and meta should all be false
|
|
if (U.style('#InputChat', s => s.display) !== 'inline') return // Input chat missing
|
|
W.ElementFocus('InputChat')
|
|
},
|
|
loader() {
|
|
if (this.remove_load_hook) {
|
|
this.remove_load_hook()
|
|
delete this.remove_load_hook
|
|
}
|
|
|
|
if (this.LOADED) return
|
|
|
|
U.with(/** @type {MBCHC.Settings.V0} */ (W.Player.OnlineSettings), os => os?.MBCHC && Settings.migrate_0_1(os))
|
|
|
|
// Calculated values
|
|
const COMMANDS = [
|
|
{Tag: 'mbchc', Description: ': Utility functions ("/mbchc" for help)', Action: this.command_mbchc, AutoComplete: this.complete_mbchc},
|
|
{Tag: 'activity', Description: '[Message]: Send a custom activity (or "@@Message", or "@Message" as yourself)', Action: this.command_activity},
|
|
{Tag: 'do', Description: ': Do an activity, as if clicked on its button ("/do" for help)', Action: this.command_do, AutoComplete: this.complete_do},
|
|
]
|
|
this.CommandsKey = CommandsKey
|
|
this.RE_ACTIVITY = new RegExp(`^${this.CommandsKey}activity `)
|
|
this.PREF_ACTIVITY = `${this.CommandsKey}activity `
|
|
this.COMP_HINT = D.createElement('div')
|
|
this.COMP_HINT.id = 'mbchcCompHint'
|
|
const css = D.createElement('style')
|
|
css.textContent = `
|
|
#TextAreaChatLog .mbchc, #TextAreaChatLog .mbchc {
|
|
background-color: ${this.RGB_POLLY};
|
|
}
|
|
#TextAreaChatLog[data-colortheme="dark"] .mbchc, #TextAreaChatLog[data-colortheme="dark2"] .mbchc {
|
|
background-color: ${this.RGB_MUTE};
|
|
}
|
|
#${this.COMP_HINT.id} {
|
|
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};
|
|
color: black;
|
|
}
|
|
#${this.COMP_HINT.id}[data-colortheme="dark"] > div, #${this.COMP_HINT.id}[data-colortheme="dark2"] > div {
|
|
background-color: ${this.RGB_MUTE};
|
|
color: white;
|
|
}
|
|
#${this.COMP_HINT.id} > div div {
|
|
margin: 0.25ex 0;
|
|
}
|
|
`
|
|
|
|
D.head.append(css)
|
|
// Actions
|
|
this.calculate_maps()
|
|
//w.Player.MBCHC = {VERSION: this.VERSION}
|
|
W.CommandCombine(COMMANDS)
|
|
|
|
// Hooks
|
|
this.remove_fbc_hook = this.before('MainRun', () => W.bce_ActivityTriggers && this.patch_fbc())
|
|
// this.after('CharacterOnlineRefresh', char => this.update_char(char))
|
|
this.after('ChatRoomReceiveSuitcaseMoney', () => {
|
|
if (this.AUTOHACK_ENABLED && this.LAST_HACKED) {
|
|
W.CurrentCharacter = this.cid2char(this.LAST_HACKED)
|
|
this.LAST_HACKED = null
|
|
W.ChatRoomTryToTakeSuitcase()
|
|
}
|
|
})
|
|
this.before('ChatRoomSendChat', () => {
|
|
let input = W.ElementValue('InputChat')
|
|
if (!input.startsWith('@@@') && input.startsWith('@')) {
|
|
input = input.replace(this.RE_PREF_ACTIVITY, this.PREF_ACTIVITY)
|
|
input = input.replace(this.RE_PREF_ACTIVITY_ME, this.replace_me)
|
|
W.ElementValue('InputChat', input)
|
|
}
|
|
})
|
|
this.after('ChatRoomSendChat', () => {
|
|
const history = W.ChatRoomLastMessage
|
|
if ((history.length > 1) && (history.at(-1) === history.at(-2))) {
|
|
history.pop()
|
|
W.ChatRoomLastMessageIndex -= 1
|
|
}
|
|
})
|
|
this.before('ChatRoomCharacterViewDrawOverlay', (C, CharX, CharY, Zoom, _Pos) => {
|
|
// 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)
|
|
// }
|
|
const ctz = TZ.for(C)
|
|
if (W.ChatRoomHideIconState < 1 && ctz !== undefined) {
|
|
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 || D.body.append(this.COMP_HINT))
|
|
this.before('ChatRoomClearAllElements', () => {
|
|
this.comp_hint_hide()
|
|
this.COMP_HINT.remove()
|
|
})
|
|
this.before('ChatRoomClick', () => {
|
|
this.comp_hint_hide()
|
|
})
|
|
this.after('ChatRoomResize', () => {
|
|
if (W.CharacterGetCurrent() === null && W.CurrentScreen === 'ChatRoom' && D.querySelector('#InputChat') && D.querySelector('#TextAreaChatLog') && this.comp_hint_visible()) { // Upstream
|
|
const fontsize = ChatRoomFontSize
|
|
//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, 800, 65, 200, 835)
|
|
//this.COMP_HINT.style.display = 'flex'
|
|
}
|
|
})
|
|
D.addEventListener('keydown', event => this.focus_chat(event))
|
|
this.SDK.hookFunction('ChatRoomKeyDown', 0, (nextargs, next) => { // This fires on chat input events
|
|
const [event] = nextargs
|
|
W.MBCHC.comp_hint_hide()
|
|
if ((W.KeyPress === 33) || (W.KeyPress === 34)) { // Better history
|
|
event.preventDefault()
|
|
return (W.MBCHC.history(W.KeyPress - 33))
|
|
}
|
|
|
|
if (W.MBCHC.HISTORY_MODE) {
|
|
W.ChatRoomLastMessage.pop()
|
|
W.MBCHC.HISTORY_MODE = false
|
|
}
|
|
|
|
return (next(nextargs))
|
|
})
|
|
|
|
// Chat room handlers
|
|
W.ChatRoomRegisterMessageHandler({Priority: -220, Description: 'MBCHC preprocessor', Callback: (data, sender, message, metadata) => {
|
|
data.MBCHC_ID = this.NEXT_MESSAGE
|
|
this.NEXT_MESSAGE += 1
|
|
if (this.LOG_MESSAGES) console.debug({data, sender, msg: message, metadata})
|
|
return false
|
|
}})
|
|
//w.ChatRoomRegisterMessageHandler({Priority: -219, Description: 'MBCHC room enter hook',
|
|
// Callback: (data, _sender, _message, _metadata) => {
|
|
// if ((data.Type === 'Action') && (data.Content === 'ServerEnter') && (data.Sender === U.cid(w.Player))) this.player_enters_room()
|
|
// return false
|
|
// },
|
|
//})
|
|
//w.ChatRoomRegisterMessageHandler({Priority: -219, Description: 'MBCHC specific consumer',
|
|
// Callback: (data, _sender, _message, _metadata) => {
|
|
// if ((data.Type === 'Hidden') && (data.Content === 'MBCHC')) return this.receive(data)
|
|
// return false
|
|
// },
|
|
//})
|
|
W.ChatRoomRegisterMessageHandler({Priority: -219, Description: 'MBCHC autohack lookup',
|
|
Callback: (data, _sender, _message, _metadata) => {
|
|
if ((data.Type === 'Hidden') && (data.Content === 'ReceiveSuitcaseMoney')) this.LAST_HACKED = data.Sender
|
|
return false
|
|
},
|
|
})
|
|
|
|
// Footer
|
|
this.LOADED = true
|
|
this.log('info', `loaded version ${this.VERSION}`)
|
|
//if ((W.CurrentModule === 'Online') && (W.CurrentScreen === 'ChatRoom')) {
|
|
// for (const c of W.ChatRoomCharacter) this.update_char(c)
|
|
// //this.player_enters_room()
|
|
//}
|
|
},
|
|
preloader() {
|
|
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')
|
|
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) => {
|
|
try {
|
|
cb?.(...nextargs)
|
|
} catch (error) {
|
|
console.error(error)
|
|
}
|
|
return next(nextargs)
|
|
})
|
|
this.after = (name, cb) => this.SDK.hookFunction(name, 0, (nextargs, next) => {
|
|
const result = next(nextargs);
|
|
try {
|
|
cb?.(...nextargs)
|
|
} catch (error) {
|
|
console.error(error)
|
|
}
|
|
return result
|
|
})
|
|
|
|
U.current() === 'Character/Login' ? this.remove_load_hook = this.before('AsylumGGTSSAddItems', () => this.loader()) :this.loader()
|
|
},
|
|
}
|
|
|
|
W.MBCHC.preloader()
|