2023-12-17 03:52:43 +00:00
export {}
2024-06-29 17:09:25 +00:00
/** @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
2024-06-05 17:33:55 +00:00
const MISSING_PLAYER_DIALOG = { Tag : 'MISSING TEXT IN "Interface.csv": ' , Text : '\u200C' }
2023-12-19 01:15:39 +00:00
2024-06-29 17:09:25 +00:00
if ( w . MBCHC ) throw new Error ( 'MBCHC found, aborting loading' )
w . MBCHC = {
VERSION : 'dev.12' ,
2023-12-17 03:52:43 +00:00
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_TZ : /(?:gmt|utc)\s*([+-])\s*(\d\d?)/i ,
RE_ALL_LEFT : /^<+$/ ,
RE_ALL_RIGHT : /^>+$/ ,
RE_SPACES : /\s{2,}/g ,
RE_LAST_WORD : /(^|\s)(\S*)$/ ,
RE_LAST_LETTER : /\w$/ ,
RGB_MUTE : '#6c2132' ,
RGB_POLLY : '#81b1e7' ,
UTC_OFFSET : new Date (). getTimezoneOffset () * 60 * 1000 ,
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 ( mbchc ) {
2024-06-29 17:09:25 +00:00
if ( w . Player . OnlineSettings . MBCHC ) {
delete w . Player . OnlineSettings . MBCHC
2023-12-17 03:52:43 +00:00
mbchc . save_settings ()
}
}},
},
2024-06-29 17:09:25 +00:00
ensure ( text , callback ) {
2023-12-17 03:52:43 +00:00
const result = callback . call ( this )
2024-06-29 17:09:25 +00:00
if ( ! result ) throw new Error ( text )
2023-12-17 03:52:43 +00:00
return ( result )
},
calculate_maps () {
this . DO_DATA = { verbs : {}, zones : {}}
for ( const [ verbs , data ] of Object . entries ( this . MAP_ACTIONS )) {
const unwound = {}
for ( const [ zones , actions ] of Object . entries ( data )) {
const all = ( actions . all ) ? actions . all . split ( '|' ) : []
const processed = { self : ( actions . self ) ? actions . self . split ( '|' ). concat ( all ) : all , others : ( actions . others ) ? actions . others . split ( '|' ). concat ( all ) : all }
for ( const zone of zones . split ( ',' )) unwound [ `Item ${ zone } ` ] = processed
}
for ( const verb of verbs . split ( '|' )) this . DO_DATA . verbs [ verb ] = unwound
}
for ( const [ ag , zones ] of Object . entries ( this . MAP_ZONES )) for ( const zone of zones ) this . DO_DATA . zones [ zone ] = ag
},
settings ( setting = null ) {
2024-06-29 17:09:25 +00:00
const settings = w . Player . OnlineSettings . MBCHC || {}
2023-12-17 03:52:43 +00:00
return ( setting ? settings [ setting ] : settings )
},
save_settings ( cb = null ) {
if ( cb ) {
2024-06-29 17:09:25 +00:00
if ( ! w . Player . OnlineSettings . MBCHC ) w . Player . OnlineSettings . MBCHC = {}
cb . call ( this , w . Player . OnlineSettings . MBCHC )
2023-12-17 03:52:43 +00:00
}
2024-06-29 17:09:25 +00:00
w . ServerAccountUpdate . QueueData ({ OnlineSettings : w . Player . OnlineSettings })
2023-12-17 03:52:43 +00:00
},
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 ) {
2024-06-29 17:09:25 +00:00
w . ChatRoomSendLocal ( `<div class="mbchc"> ${ html } </div>` , timeout )
2023-12-17 03:52:43 +00:00
},
report ( x ) {
2024-06-29 17:09:25 +00:00
this . inform ( ` ${ x . toString () } ` )
2023-12-17 03:52:43 +00:00
if ( this . RETHROW ) throw x
},
in ( x , floor , ceiling ) {
return (( x >= floor ) && ( x <= ceiling ))
},
2024-06-29 17:09:25 +00:00
cid2char ( id ) {
id = Number . parseInt ( id , 10 )
if ( id === cid ( w . Player )) return ( w . Player )
return ( this . ensure ( `character ${ id } not found in the room` , () => w . ChatRoomCharacter . find ( c => cid ( c ) === id )))
2023-12-17 03:52:43 +00:00
},
pos2char ( pos ) {
2024-06-29 17:09:25 +00:00
if ( ! this . in ( pos , 0 , w . ChatRoomCharacter . length - 1 )) throw new Error ( `invalid position ${ pos } ` )
return ( w . ChatRoomCharacter [ pos ])
2023-12-17 03:52:43 +00:00
},
rel2char ( target ) {
2024-06-29 17:09:25 +00:00
const me = this . ensure ( 'can\'t find my position' , () => w . ChatRoomCharacter . findIndex ( char => char . IsPlayer ()) + 1 ) - 1 // 0 is falsey, but is valid index
2023-12-17 03:52:43 +00:00
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 } "` )
2024-06-29 17:09:25 +00:00
pos %= w . ChatRoomCharacter . length
if ( pos < 0 ) pos += w . ChatRoomCharacter . length
2023-12-17 03:52:43 +00:00
return ( this . pos2char ( pos ))
},
target2char ( target ) { // Target should be lowcase
const input = target
2024-06-29 17:09:25 +00:00
if ( this . empty ( target )) return ( w . Player )
2023-12-17 03:52:43 +00:00
const int = Number . parseInt ( target , 10 )
target = String ( target )
let found = []
if ( target . startsWith ( '=' )) return ( this . cid2char ( target . slice ( 1 )))
if ( target . startsWith ( '<' ) || target . startsWith ( '>' )) return ( this . rel2char ( target ))
if ( ! Number . isNaN ( int ) && int . toString () === target ) { // We got a number
if ( this . in ( int , 0 , 9 )) return ( this . pos2char ( int ))
if ( this . in ( int , 11 , 15 )) return ( this . pos2char ( int - 11 ))
if ( this . in ( int , 21 , 25 )) return ( this . pos2char ( int - 16 ))
2024-06-29 17:09:25 +00:00
found . push (... w . ChatRoomCharacter . filter ( c => cid ( c ). toString (). includes ( target )))
2023-12-17 03:52:43 +00:00
}
if ( target . startsWith ( '@' )) target = target . slice ( 1 )
2024-06-29 17:09:25 +00:00
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
2023-12-17 03:52:43 +00:00
const map = {}
for ( const c of found ) {
2024-06-29 17:09:25 +00:00
if ( ! map [ cid ( c )]) map [ cid ( c )] = c
2023-12-17 03:52:43 +00:00
}
found = Object . values ( map )
if ( found . length === 0 ) throw new Error ( `target " ${ input } ": no match` )
2024-06-29 17:09:25 +00:00
if ( found . length > 1 ) throw new Error ( `target " ${ input } ": multiple matches ( ${ found . map ( c => ` ${ cid ( c ) } | ${ c . Name } | ${ c . Nickname || c . Name } ` ). join ( ',' ) } )` )
2023-12-17 03:52:43 +00:00
return ( found [ 0 ])
},
char2targets ( char ) {
2024-06-29 17:09:25 +00:00
const [ result , id ] = [ new Set (), cid ( char ). toString ()]
result . add ( id ). add ( `= ${ id } ` )
2023-12-17 03:52:43 +00:00
for ( const t of this . tokenise ( char . Name )) {
result . add ( t )
result . add ( `@ ${ t } ` )
}
if ( char . Nickname ) for ( const t of this . tokenise ( char . Nickname )) {
result . add ( t )
result . add ( `@ ${ t } ` )
}
return result
},
donate_data ( target ) {
const char = this . target2char ( target )
if ( char . IsPlayer ()) throw new Error ( 'target must not be you' )
if ( ! char . IsRestrained ()) throw new Error ( 'target must be bound' )
const cost = Math . round ((( Math . random () * 10 ) + 15 ))
2024-06-29 17:09:25 +00:00
if ( w . Player . Money < cost ) throw new Error ( 'not enough money' )
w . CharacterChangeMoney ( w . Player , - cost )
w . ServerSend ( 'ChatRoomChat' , { Content : 'ReceiveSuitcaseMoney' , Type : 'Hidden' , Target : 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 ]})
2023-12-17 03:52:43 +00:00
},
run_activity ( char , ag , action ) {
try {
2024-06-29 17:09:25 +00:00
if ( ! w . ActivityAllowed ()) throw new Error ( 'activities disabled in this room' )
if ( ! w . ServerChatRoomGetAllowItem ( w . Player , char )) throw new Error ( 'no permissions' )
char . FocusGroup = this . ensure ( '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 ))
//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 )
2023-12-17 03:52:43 +00:00
} finally {
char . FocusGroup = null
}
},
replace_me ( match , offset , string ) {
const text = string . slice ( 1 )
let suffix = ' '
if ( text . startsWith ( '\'' ) || text . startsWith ( ' ' )) suffix = ''
2024-06-29 17:09:25 +00:00
return ` ${ w . MBCHC . PREF_ACTIVITY } < ${ cid ( w . Player ) } :>SourceCharacter ${ suffix } `
2023-12-17 03:52:43 +00:00
},
cid2dict ( type , cid ) {
return ({ Tag : ` ${ type } Character` , MemberNumber : cid , Text : this . cid2char ( cid ). dn })
},
send_activity ( message ) {
2023-12-19 01:15:39 +00:00
const dict = [ MISSING_PLAYER_DIALOG ]
2023-12-17 03:52:43 +00:00
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 ]))
}
2024-06-29 17:09:25 +00:00
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)
//},
2023-12-17 03:52:43 +00:00
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
2024-06-29 17:09:25 +00:00
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' )
2023-12-17 03:52:43 +00:00
if ( cmd ) cmd . AutoComplete = this . complete_fbc_anim
2024-06-29 17:09:25 +00:00
cmd = w . Commands . find ( c => c . Tag === 'pose' )
2023-12-17 03:52:43 +00:00
if ( cmd ) cmd . AutoComplete = this . complete_fbc_pose
},
find_timezone ( char ) {
const timezones = this . settings ( 'timezones' )
2024-06-29 17:09:25 +00:00
if ( timezones && typeof timezones [ cid ( char )] === 'number' ) return ( timezones [ cid ( char )])
2023-12-17 03:52:43 +00:00
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 )
},
2024-06-29 17:09:25 +00:00
//player_enters_room() { // Or if the mod is loaded while player is in the room
// this.hello()
//},
2023-12-17 03:52:43 +00:00
set_timezone ( args ) {
const tz = Number . parseInt ( args [ 0 ], 10 )
if ( Number . isNaN ( tz )) throw new Error ( `invalid offset " ${ args [ 0 ] } "` )
if ( ! this . in ( tz , - 12 , 12 )) throw new Error ( 'offset should be [-12,12]' )
const char = this . target2char ( args [ 1 ])
char . MBCHC_LOCAL . TZ = tz
this . save_settings ( s => {
if ( ! s . timezones ) s . timezones = {}
2024-06-29 17:09:25 +00:00
s . timezones [ cid ( char )] = tz
2023-12-17 03:52:43 +00:00
})
},
update_char ( char ) {
2024-06-29 17:09:25 +00:00
//char.cid = char.MemberNumber // Club ID (shorter)
char . dn = w . CharacterNickname ( char ) // DisplayName (shortcut)
2023-12-17 03:52:43 +00:00
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 ) {
2024-06-29 17:09:25 +00:00
const mbchc = w . MBCHC
2023-12-17 03:52:43 +00:00
try { // `this` is command object
if ( args . length === 0 ) return ( mbchc . inform ( Object . entries ( mbchc . SUBCOMMANDS_MBCHC ). map (([ cmd , sub ]) => `<div>/mbchc ${ cmd } ${ sub . args ? Object . keys ( sub . args ). join ( ' ' ) : '' } : ${ sub . desc } </div>` ). join ( '' )))
const cmd = String ( args . shift ())
const sub = mbchc . ensure ( `unknown subcommand " ${ cmd } "` , () => mbchc . SUBCOMMANDS_MBCHC [ cmd ])
sub . cb . call ( mbchc , mbchc , args , argline , cmdline )
} catch ( error ) {
mbchc . report ( error )
}
},
command_activity ( argline , cmdline , _ ) {
2024-06-29 17:09:25 +00:00
const mbchc = w . MBCHC
2023-12-17 03:52:43 +00:00
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 ) {
2024-06-29 17:09:25 +00:00
const mbchc = w . MBCHC
2023-12-17 03:52:43 +00:00
try { // `this` is command object
if ( args . length === 0 ) return ( mbchc . inform ( '<div>Usage: /do VERB [ZONE] [TARGET]</div><div>Available verbs:</div>' + Object . keys ( mbchc . MAP_ACTIONS ). join ( ', ' ) + '<div>Available zones:</div>' + Object . keys ( mbchc . DO_DATA . zones ). join ( ', ' )))
let [ verb , zone , target ] = args
const zones = mbchc . ensure ( `unknown verb " ${ verb } "` , () => mbchc . DO_DATA . verbs [ verb ])
if ( Object . keys ( zones ). length === 1 ) {
if ( ! target ) target = zone
zone = mbchc . MAP_ZONES [ Object . keys ( zones )[ 0 ]][ 0 ]
}
if ( ! zone ) throw new Error ( 'zone missing' )
const ag = mbchc . ensure ( `unknown zone " ${ zone } "` , () => mbchc . DO_DATA . zones [ zone ])
const types = mbchc . ensure ( `zone " ${ zone } " invalid for " ${ verb } "` , () => zones [ ag ])
2024-06-29 17:09:25 +00:00
let char = w . Player
2023-12-17 03:52:43 +00:00
if ( target && (( types . self . length === 0 ) || ( types . others . length > 0 ))) char = mbchc . target2char ( target )
const type = char . IsPlayer () ? 'self' : 'others'
2024-06-29 17:09:25 +00:00
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)))
2023-12-17 03:52:43 +00:00
const actions = mbchc . ensure ( `zone " ${ zone } " invalid for (" ${ verb } " " ${ type } ")` , () => types [ type ])
2024-06-29 17:09:25 +00:00
const action = mbchc . ensure ( `invalid action ( ${ verb } ${ zone } ${ target } )` , () => actions . find ( name => available . find ( a => a . Activity ? . Name === name )))
2023-12-17 03:52:43 +00:00
mbchc . run_activity ( char , ag , action )
} catch ( error ) {
mbchc . report ( error )
}
},
bell () {
setTimeout (() => {
2024-06-29 17:09:25 +00:00
style ( '#InputChat' , s => s . outline = '' )
2023-12-17 03:52:43 +00:00
}, 100 )
2024-06-29 17:09:25 +00:00
style ( '#InputChat' , s => s . outline = 'solid red' )
2023-12-17 03:52:43 +00:00
},
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 )
2024-06-29 17:09:25 +00:00
this . comp_hint ( options )
} else w . ElementValue ( 'InputChat' , w . ElementValue ( 'InputChat' ). replace ( this . RE_LAST_WORD , `$1 ${ options [ 0 ] }${ space ? ' ' : '' } ` ))
2023-12-17 03:52:43 +00:00
},
2024-06-29 17:09:25 +00:00
/**
* Displays strings as completion hint
* @param {string[]} options List of words to display. The order will be modified without copy.
* @returns {void}
*/
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' })
2023-12-17 03:52:43 +00:00
},
2024-06-29 17:09:25 +00:00
/**
* Returns true if the completion box is attached to body and its display isn't none
* @returns {boolean}
*/
2023-12-17 03:52:43 +00:00
comp_hint_visible () {
2024-06-29 17:09:25 +00:00
return ( this . COMP_HINT . parentElement && this . COMP_HINT . style . display !== 'none' )
2023-12-17 03:52:43 +00:00
},
2024-06-29 17:09:25 +00:00
comp_hint_hide () {
2023-12-17 03:52:43 +00:00
if ( ! this . comp_hint_visible ()) return
this . COMP_HINT . style . display = 'none'
2024-06-29 17:09:25 +00:00
w . ChatRoomResize ( false )
2023-12-17 03:52:43 +00:00
},
complete_target ( token , me2 = true , check_perms = false ) {
const [ locase , found ] = [ token . toLocaleLowerCase (), new Set ()]
2024-06-29 17:09:25 +00:00
for ( const c of w . ChatRoomCharacter ) {
if (( c . IsPlayer () && ! me2 ) || ( check_perms && ! w . ServerChatRoomGetAllowItem ( w . Player , c ))) continue
2023-12-17 03:52:43 +00:00
for ( const s of this . char2targets ( c )) {
if ( s . toLocaleLowerCase (). startsWith ( locase )) found . add ( s )
}
}
this . complete ( Array . from ( found ))
},
complete_common () {
2024-06-29 17:09:25 +00:00
// w.ElementValue('InputChat') will strip the trailing whitespace
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 )])
2023-12-17 03:52:43 +00:00
},
complete_mbchc ( _args , _locase , _cmdline ) {
2024-06-29 17:09:25 +00:00
const [ mbchc , _input , tokens ] = w . MBCHC . complete_common (); // `this` is command object
2023-12-17 03:52:43 +00:00
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 )
2024-06-29 17:09:25 +00:00
if ( me2 && actions . others . length === 0 ) return ( this . complete ([ cid ( w . Player ). toString ()])) // Target is always the player
2023-12-17 03:52:43 +00:00
this . complete_target ( token , me2 , true )
},
complete_do ( _args , _locase , _cmdline ) {
2024-06-29 17:09:25 +00:00
const [ mbchc , _input , tokens ] = w . MBCHC . complete_common (); // `this` is command object
2023-12-17 03:52:43 +00:00
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 ) {
2024-06-29 17:09:25 +00:00
const [ mbchc , _input , tokens ] = w . MBCHC . complete_common (); // `this` is command object
2023-12-17 03:52:43 +00:00
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 ()
2024-06-29 17:09:25 +00:00
return ( mbchc . complete ( Object . keys ( w . bce_EventExpressions ). filter ( a => a . toLocaleLowerCase (). startsWith ( anim ))))
2023-12-17 03:52:43 +00:00
},
complete_fbc_pose ( _args , _locase , _cmdline ) {
2024-06-29 17:09:25 +00:00
const [ mbchc , _input , tokens ] = w . MBCHC . complete_common (); // `this` is command object
2023-12-17 03:52:43 +00:00
if ( tokens . length === 0 ) return
if ( tokens . length < 2 ) return ( mbchc . complete ([ ` ${ mbchc . CommandsKey }${ this . Tag } ` ]))
const pose = tokens . at ( - 1 ). toLocaleLowerCase ()
2024-06-29 17:09:25 +00:00
return ( mbchc . complete ( w . PoseFemale3DCG . map ( p => p . Name ). filter ( p => p . toLocaleLowerCase (). startsWith ( pose ))))
2023-12-17 03:52:43 +00:00
},
history ( down ) {
2024-06-29 17:09:25 +00:00
const [ text , history ] = [ w . ElementValue ( 'InputChat' ), w . ChatRoomLastMessage ]
2023-12-17 03:52:43 +00:00
if ( ! this . HISTORY_MODE ) {
history . push ( text )
this . HISTORY_MODE = true
}
2024-06-29 17:09:25 +00:00
const ids = history . map (( t , i ) => ({ t , i })). filter ( r => r . t . startsWith ( history . at ( - 1 ))). map ( r => r . i )
2023-12-17 03:52:43 +00:00
if ( ! down ) ids . reverse ()
2024-06-29 17:09:25 +00:00
const found = ids . find ( id => ( down ) ? id > w . ChatRoomLastMessageIndex : id < w . ChatRoomLastMessageIndex )
2023-12-17 03:52:43 +00:00
if ( ! found ) return ( this . bell ())
2024-06-29 17:09:25 +00:00
w . ElementValue ( 'InputChat' , history [ found ])
w . ChatRoomLastMessageIndex = found
2023-12-17 03:52:43 +00:00
},
2023-12-19 01:15:39 +00:00
focus_chat_checks () { // we only want to catch chatlog and canvas (no map though) keypresses
if ( document . activeElement === document . body ) return true
if ( document . activeElement . id !== 'MainCanvas' ) return false
2024-06-29 17:09:25 +00:00
return ! w . ChatRoomMapViewIsActive ()
2023-12-19 01:15:39 +00:00
},
2023-12-17 03:52:43 +00:00
focus_chat_whitelist ( event ) {
if ( event . ctrlKey && event . key === 'v' ) return true // Ctrl+V should paste
return false
},
2023-12-19 01:15:39 +00:00
focus_chat ( event ) {
2023-12-17 03:52:43 +00:00
if ( event . repeat ) return // Only unique presses please
2023-12-19 01:15:39 +00:00
if ( ! this . focus_chat_checks ()) return
2023-12-17 03:52:43 +00:00
if ([ event . altKey , event . ctrlKey , event . metaKey ]. some ( Boolean ) && ! this . focus_chat_whitelist ( event )) return // Alt, ctrl and meta should all be false
2024-06-29 17:09:25 +00:00
if ( style ( '#InputChat' , s => s . display ) !== 'inline' ) return // Input chat missing
w . ElementFocus ( 'InputChat' )
2023-12-17 03:52:43 +00:00
},
loader () {
if ( this . remove_load_hook ) {
this . remove_load_hook ()
delete this . remove_load_hook
}
if ( this . LOADED ) return
// Calculated values
const COMMANDS = [
{ Tag : 'mbchc' , Description : ': Utility functions ("/mbchc" for help)' , Action : this . command_mbchc , AutoComplete : this . complete_mbchc },
{ Tag : 'activity' , Description : '[Message]: Send a custom activity (or "@@Message", or "@Message" as yourself)' , Action : this . command_activity },
{ Tag : 'do' , Description : ': Do an activity, as if clicked on its button ("/do" for help)' , Action : this . command_do , AutoComplete : this . complete_do },
]
this . CommandsKey = CommandsKey /* eslint-disable-line no-undef */ // window.CommandsKey is undefined
this . RE_ACTIVITY = new RegExp ( `^ ${ this . CommandsKey } activity ` )
this . PREF_ACTIVITY = ` ${ this . CommandsKey } activity `
this . COMP_HINT = document . createElement ( 'div' )
this . COMP_HINT . id = 'mbchcCompHint'
const css = document . createElement ( 'style' )
css . textContent = `
#TextAreaChatLog .mbchc, #TextAreaChatLog .mbchc {
background-color: ${ this . RGB_POLLY } ;
}
#TextAreaChatLog[data-colortheme="dark"] .mbchc, #TextAreaChatLog[data-colortheme="dark2"] .mbchc {
background-color: ${ this . RGB_MUTE } ;
}
# ${ this . COMP_HINT . id } {
display: none;
2024-06-29 17:09:25 +00:00
text-align: right;
}
# ${ this . COMP_HINT . id } > div {
overflow: auto;
position: absolute;
bottom: 0;
right: 0;
max-height: 100%;
padding: 0 0.5ex;
2023-12-17 03:52:43 +00:00
background-color: ${ this . RGB_POLLY } ;
color: black;
}
2024-06-29 17:09:25 +00:00
# ${ this . COMP_HINT . id } [data-colortheme="dark"] > div, # ${ this . COMP_HINT . id } [data-colortheme="dark2"] > div {
2023-12-17 03:52:43 +00:00
background-color: ${ this . RGB_MUTE } ;
color: white;
}
2024-06-29 17:09:25 +00:00
# ${ this . COMP_HINT . id } > div div {
margin: 0.25ex 0;
2023-12-17 03:52:43 +00:00
}
`
2024-06-29 17:09:25 +00:00
2023-12-17 03:52:43 +00:00
document . head . append ( css )
// Actions
this . calculate_maps ()
2024-06-29 17:09:25 +00:00
//w.Player.MBCHC = {VERSION: this.VERSION}
w . CommandCombine ( COMMANDS )
2023-12-17 03:52:43 +00:00
// Hooks
2024-06-29 17:09:25 +00:00
this . remove_fbc_hook = this . before ( 'MainRun' , () => w . bce_ActivityTriggers && this . patch_fbc ())
2023-12-17 03:52:43 +00:00
this . after ( 'CharacterOnlineRefresh' , char => this . update_char ( char ))
this . after ( 'ChatRoomReceiveSuitcaseMoney' , () => {
if ( this . AUTOHACK_ENABLED && this . LAST_HACKED ) {
2024-06-29 17:09:25 +00:00
w . CurrentCharacter = this . cid2char ( this . LAST_HACKED )
2023-12-17 03:52:43 +00:00
this . LAST_HACKED = null
2024-06-29 17:09:25 +00:00
w . ChatRoomTryToTakeSuitcase ()
2023-12-17 03:52:43 +00:00
}
})
this . before ( 'ChatRoomSendChat' , () => {
2024-06-29 17:09:25 +00:00
let input = w . ElementValue ( 'InputChat' )
2023-12-17 03:52:43 +00:00
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 )
2024-06-29 17:09:25 +00:00
w . ElementValue ( 'InputChat' , input )
2023-12-17 03:52:43 +00:00
}
})
this . after ( 'ChatRoomSendChat' , () => {
2024-06-29 17:09:25 +00:00
const history = w . ChatRoomLastMessage
2023-12-17 03:52:43 +00:00
if (( history . length > 1 ) && ( history . at ( - 1 ) === history . at ( - 2 ))) {
history . pop ()
2024-06-29 17:09:25 +00:00
w . ChatRoomLastMessageIndex -= 1
2023-12-17 03:52:43 +00:00
}
})
2024-02-29 18:17:35 +00:00
this . before ( 'ChatRoomCharacterViewDrawOverlay' , ( C , CharX , CharY , Zoom , _Pos ) => {
2024-06-29 17:09:25 +00:00
// 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)
2023-12-19 01:15:39 +00:00
// }
2024-06-29 17:09:25 +00:00
if ( w . ChatRoomHideIconState < 1 && C . MBCHC_LOCAL && typeof C . MBCHC_LOCAL . TZ === 'number' ) {
const hours = new Date ( w . CommonTime () + this . UTC_OFFSET + ( C . MBCHC_LOCAL . TZ * 60 * 60 * 1000 )). getHours ()
w . DrawTextFit ( hours < 10 ? '0' + hours . toString () : hours . toString (), CharX + ( 200 * Zoom ), CharY + ( 25 * Zoom ), 46 * Zoom , 'white' , 'black' )
2023-12-17 03:52:43 +00:00
}
})
this . after ( 'ChatRoomCreateElement' , () => this . COMP_HINT . parentElement || document . body . append ( this . COMP_HINT ))
this . before ( 'ChatRoomClearAllElements' , () => {
2024-06-29 17:09:25 +00:00
this . comp_hint_hide ()
2023-12-17 03:52:43 +00:00
this . COMP_HINT . remove ()
})
2024-06-29 17:09:25 +00:00
this . before ( 'ChatRoomClick' , () => {
this . comp_hint_hide ()
})
2023-12-17 03:52:43 +00:00
this . after ( 'ChatRoomResize' , () => {
2024-06-29 17:09:25 +00:00
if ( w . CharacterGetCurrent () === null && w . CurrentScreen === 'ChatRoom' && document . querySelector ( '#InputChat' ) && document . querySelector ( '#TextAreaChatLog' ) && this . comp_hint_visible ()) { // Upstream
2023-12-17 03:52:43 +00:00
const fontsize = ChatRoomFontSize /* eslint-disable-line no-undef */ // window.ChatRoomFontSize is undefined
2024-06-29 17:09:25 +00:00
//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'
2023-12-17 03:52:43 +00:00
}
})
document . addEventListener ( 'keydown' , event => this . focus_chat ( event ))
this . SDK . hookFunction ( 'ChatRoomKeyDown' , 0 , ( nextargs , next ) => { // This fires on chat input events
const [ event ] = nextargs
2024-06-29 17:09:25 +00:00
w . MBCHC . comp_hint_hide ()
if (( w . KeyPress === 33 ) || ( w . KeyPress === 34 )) { // Better history
2023-12-17 03:52:43 +00:00
event . preventDefault ()
2024-06-29 17:09:25 +00:00
return ( w . MBCHC . history ( w . KeyPress - 33 ))
2023-12-17 03:52:43 +00:00
}
2024-06-29 17:09:25 +00:00
if ( w . MBCHC . HISTORY_MODE ) {
w . ChatRoomLastMessage . pop ()
w . MBCHC . HISTORY_MODE = false
2023-12-17 03:52:43 +00:00
}
return ( next ( nextargs ))
})
// Chat room handlers
2024-06-29 17:09:25 +00:00
w . ChatRoomRegisterMessageHandler ({ Priority : - 220 , Description : 'MBCHC preprocessor' , Callback : ( data , sender , message , metadata ) => {
2023-12-17 03:52:43 +00:00
data . MBCHC_ID = this . NEXT_MESSAGE
this . NEXT_MESSAGE += 1
if ( this . LOG_MESSAGES ) console . debug ({ data , sender , msg : message , metadata })
2024-06-29 17:09:25 +00:00
return false
2023-12-17 03:52:43 +00:00
}})
2024-06-29 17:09:25 +00:00
//w.ChatRoomRegisterMessageHandler({Priority: -219, Description: 'MBCHC room enter hook',
// Callback: (data, _sender, _message, _metadata) => {
// if ((data.Type === 'Action') && (data.Content === 'ServerEnter') && (data.Sender === 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' ,
2023-12-17 03:52:43 +00:00
Callback : ( data , _sender , _message , _metadata ) => {
if (( data . Type === 'Hidden' ) && ( data . Content === 'ReceiveSuitcaseMoney' )) this . LAST_HACKED = data . Sender
2024-06-29 17:09:25 +00:00
return false
2023-12-17 03:52:43 +00:00
},
})
// Footer
this . LOADED = true
this . log ( 'info' , `loaded version ${ this . VERSION } ` )
2024-06-29 17:09:25 +00:00
if (( w . CurrentModule === 'Online' ) && ( w . CurrentScreen === 'ChatRoom' )) {
for ( const c of w . ChatRoomCharacter ) this . update_char ( c )
//this.player_enters_room()
2023-12-17 03:52:43 +00:00
}
},
preloader () {
2024-06-29 17:09:25 +00:00
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/' })
2023-12-17 03:52:43 +00:00
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
})
2024-06-29 17:09:25 +00:00
// 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 ())
} else this . loader ()
2023-12-17 03:52:43 +00:00
},
}
2024-06-29 17:09:25 +00:00
w . MBCHC . preloader ()