@@ -1,5 +1,5 @@
// Take a look at the .d.ts for comments.
export const version = '108 .13.1 '
export const version = '107 .13.0 '
const W = window , D = W . document , /**fuck money*/ $ = undefined , /**@type {''}*/ $S = '' , /**@type {{}}*/ $O = { } , /**@type {Set<string>}*/ $Ss = new Set ( ) // /**@type {readonly []}*/$A = [],
const /**@type {TextDictionaryEntry}*/ 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
@@ -158,9 +158,7 @@ const/**@type {Utils}*/U = { remove_loader_hook: $, RGB: {Polly: '#81b1e7', Mute
return $Ss
} ) } ,
replace _me : ( _ , _ _ , whole ) => cur ( whole . slice ( 1 ) , t => ` ${ U . ACT } < ${ U . cid ( W . Player ) } :>SourceCharacter ${ t . startsWith ( '\'' ) || t . startsWith ( ' ' ) ? $S : ' ' } ` ) ,
scroll : ( ) => yes ( void W . ElementScrollToEnd( 'TextAreaChatLog') ) ,
get scrolled ( ) { return W . ElementIsScrolledToEnd ( 'TextAreaChatLog' ) } ,
rescroll : f => cur ( U . scrolled , s => yes ( f , s && U . scroll ( ) ) ) ,
//pad_chat: c => loo(100, _ => c.scrollHeight <= c.clientHeight, _ => yes(void c.prepend(U.mkdiv('\u061C')))) && void W. ElementScrollToEnd( 'TextAreaChatLog')
}
const /**@type {SUBCOMMANDS}*/ SUBCOMMANDS _MBCHC = {
@@ -180,7 +178,7 @@ const/**@type {Complete}*/C = { S_OPTS: {behavior: 'instant'},
hint : cs => m _t ( cs ) || yes ( _ => {
yes ( C . e . style . display = 'block' ) && C . div ? . replaceChildren ( ... [ ... cs ] . sort ( ) . reverse ( ) . map ( U . mkdiv ) )
W . ElementSetDataAttribute ( C . e . id , 'colortheme' , W . Player . ChatSettings ? . ColorTheme ? ? 'Light' )
U . rescroll ( _ => void W . ChatRoomResize ( false ) )
cur ( W . ElementIsScrolledToEnd ( 'TextAreaChatLog' ) , r => yes ( void W . ChatRoomResize ( false ) ) && r && void W . ElementScrollToEnd ( 'TextAreaChatLog' ) )
C . div ? . lastElementChild ? . scrollIntoView ( C . S _OPTS )
} ) ,
get hidden ( ) { return C . e . parentElement === null || C . e . style . display === 'none' } ,
@@ -205,13 +203,8 @@ const/**@type {Complete}*/C = { S_OPTS: {behavior: 'instant'},
}
const /**@type {InputHistory}*/ H = { input : undefined , ids : undefined , bottom : undefined , // FIXME ids don't need to be a set, but I'm too tired right now
key : {
Escape : ( _ , ic ) => yes ( val ( H . input , i => ic . value = i ) ) ,
ArrowLeft : ( _ , ic ) => yes ( void ic . setSelectionRange ( 0 , 0 ) ) ,
} ,
icro : ( ic , ro ) => yes ( ic . readOnly = ro , val ( ic . parentElement ? . parentElement ? . dataset , d => ro ? d [ 'mbchcMode' ] = 'h' : del ( d , 'mbchcMode' ) ) ) ,
enter : ( ic , i , b , is ) => yes ( H . input = i , H . bottom = b , H . ids = is , H . icro ( ic , true ) , b && U . scroll ( ) ) ,
exit : ( ic , e ) => yes ( H . icro ( ic , false ) , H . key [ e . key ] ? . ( e , ic ) , val ( H . bottom , b => b && U . scroll ( ) ) , W . ChatRoomLastMessageIndex = W . ChatRoomLastMessage . length )
enter : ( ic , i , b , is ) => yes ( H . input = i , H . bottom = b , H . ids = is , ic . readOnly = true , val ( ic . parentElement ? . parentElement ? . dataset , d => d [ 'mbchcMode' ] = 'h' ) , b && void W . ElementScrollToEnd ( 'TextAreaChatLog' ) ) ,
exit : ( ic , r ) => yes ( ic . readOnly = false , val ( ic . parentElement ? . parentElement ? . dataset , d => del ( d , 'mbchcMode' ) ) , r && val ( H . input , i => ic . value = i ) , val ( H . bottom , b => b && void W . ElementScrollToEnd ( 'TextAreaChatLog' ) ) , W . ChatRoomLastMessageIndex = W . ChatRoomLastMessage . length )
}
ass ( 'MBCHC found, aborting loading' , W . MBCHC === $ )
@@ -229,17 +222,21 @@ const/**@type {SDK.Hook}*/after = (name, f) => mod.hookFunction(name, 0, (na, n)
version , /** Just in case someone used it for anything @deprecated*/ VERSION : version ,
Settings , TZ ,
SUBCOMMANDS _MBCHC , H , U , // debug, will go away
NEXT _MESSAGE : 1 ,
LOG _MESSAGES : false ,
LOADED : false ,
AUTOHACK _ENABLED : false ,
/**@type {number | undefined}*/ LAST _HACKED : $ ,
//HISTORY_MODE: false,
RE _PREF _ACTIVITY _ME : /^@/ ,
RE _PREF _ACTIVITY : /^@@/ ,
RE _ACT _CIDS : /^<(\d+)?:(\d+)?>/ ,
RE _LAST _WORD : /(^|\s)(\S*)$/ ,
RE _LAST _LETTER : /\w$/ ,
RE _ACTIVITY : new RegExp ( ` ^ ${ CommandsKey } activity ` ) ,
//PREF_ACTIVITY: `${CommandsKey}activity `,
UTC _OFFSET : new Date ( ) . getTimezoneOffset ( ) * 60 * 1000 ,
MAP _ACTIONS : { // ActivityFemale3DCG
// Action
@@ -448,6 +445,127 @@ const/**@type {SDK.Hook}*/after = (name, f) => mod.hookFunction(name, 0, (na, n)
mbchc . run _activity ( char , /**@type {AssetGroupItemName}*/ ( ag ) , /**@type {ActivityName}*/ ( action ) )
} catch ( x ) { U . report ( x ) }
} ,
//complete(options, space = true) {
// if (m_t(options)) return void U.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 ? ' ' : $S}`))
//},
///**
// * 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 (m_t(options)) return
// this.COMP_HINT.innerHTML = '<div>' + options.sort().reverse().map(s => `<div>${s}</div>`).join($S) + '</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)
//},
//char2targets(/**@type {Character}*/char) {
// const/**@type {Set<string>}*/result = new Set()
// val(U.cid(char)?.toString(), id => result.add(id).add(`=${id}`))
// for (const t of U.split(char.Name)) {
// result.add(t)
// result.add(`@${t}`)
// }
// if (char.Nickname !== $) for (const t of U.split(char.Nickname)) {
// result.add(t)
// result.add(`@${t}`)
// }
// return result
//},
//complete_target(/**@type {string}*/token, me2 = true, check_perms = false) {
// const [locase, found] = [token.toLocaleLowerCase(), new Set()]
// for (const c of U.crc) {
// 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')
// ass('somehow #InputChat is broken', E instanceof HTMLTextAreaElement)
// return [this, E.value, U.split(E.value)]
//},
//complete_mbchc(_args, _locase, _cmdline) {
// const [mbchc, _input, tokens] = W.MBCHC.complete_common(); // `this` is command object
// if (m_t(tokens)) return
// if (tokens.length < 2) return //mbchc.complete([`${CommandsKey}${this.Tag}`])
// const subname = tokens[1].toLocaleLowerCase()
// if (tokens.length < 3) return //mbchc.complete(Object.keys(SUBCOMMANDS_MBCHC).filter(c => c.startsWith(subname))) // Complete subcommand name
// const sub = 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))
// }
//},
//complete_fbc_anim(_args, _locase, _cmdline) {
// const [mbchc, _input, tokens] = W.MBCHC.complete_common(); // `this` is command object
// if (m_t(tokens)) return
// if (tokens.length < 2) return mbchc.complete([`${CommandsKey}${this.Tag}`])
// if (tokens.length > 2) return void U.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 (m_t(tokens)) return
// if (tokens.length < 2) return mbchc.complete([`${CommandsKey}${this.Tag}`])
// const pose = tokens.at(-1).toLocaleLowerCase()
// return mbchc.complete(W.PoseFemale3DCG.map(p => p.Name).filter(p => p.toLocaleLowerCase().startsWith(pose)))
//},
//focus_chat_checks() { // we only want to catch chat log 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(/**@type {KeyboardEvent}*/event) {
// if (event.ctrlKey && event.key === 'v') return true // Ctrl+V should paste
// return false
//},
//focus_chat(/**@type {KeyboardEvent}*/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 ( ) {
U . remove _loader _hook === $ || yes ( void U . remove _loader _hook ( ) , U . remove _loader _hook = $ )
if ( this . LOADED ) return
@@ -459,22 +577,18 @@ const/**@type {SDK.Hook}*/after = (name, f) => mod.hookFunction(name, 0, (na, n)
{ 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 : U . complete _do } ,
]
const /**@type (e: Event) => void*/ css _hook = e => void cur ( /**@type {HTMLStyl eElement}*/ ( e . target ) , css => U . scroll ( ) && void css . removeEventListener ( 'load' , css _hook ) )
mut ( D . createElement ( 'style' ) , c => void D . head . append ( c ) , c => void c . addEventListener ( 'load' , css _hook ) , c => c . textContent = `
mut ( D . creat eElement( 'style' ) , c => void D . head . append ( c ) , c => c . textContent = `
#TextAreaChatLog .mbchc { background-color: ${ U . RGB . Polly } ; margin-left: -0.4em; padding-left: 0.4em; }
#TextAreaChatLog[data-colortheme^="dark"] .mbchc { background-color: ${ U . RGB . Mute } ; }
# ${ C . e . id } { display: none; text-align: right; }
# ${ C . e . id } > div { overflow: auto; position: absolute; bottom: 0; right: 0; max-height: 100%; padding: 0 0.5ex; background-color: ${ U . RGB . Polly } ; color: black; }
# ${ C . e . id } [data-colortheme^="dark"] > div { background-color: ${ U . RGB . Mute } ; color: white; }
# ${ C . e . id } > div div { margin: 0.25ex 0; }
#chat-room-div #TextAreaChatLog::before { content: ''; display: block; height: 100%; background: repeating-linear-gradient(135deg, transparent 0 20px, #333 2px 22px); }
#chat-room-div[data-mbchc-mode="h"] #TextAreaChatLog::after {
content: '⟨𝘗𝘨𝘜𝘱/𝘋 𝘯 /↕⟩ 𝗌 𝖼 𝗋 𝗈 𝗅 𝗅 ⇅ ⟨𝘌𝘯𝘵𝘦𝘳⟩ 𝗌 𝖾 𝗇 𝖽 ↵ ⟨𝘛𝘢𝘣/↔/⌫⟩ 𝖾 𝖽 𝗂 𝗍 ⌨ ⟨𝘌𝘴𝘤⟩ 𝖺 𝖻 𝗈 𝗋 𝗍 ⟲ \\ A' attr(data-mbchc-h-h); whitespace: pre;
display: block; position: sticky; z-index: 1; bottom: 0; background: black; color: orange; padding: 0 0.2ex; animation: 0.2s cubic-bezier(0.19, 1, 0.22, 1) mbchc_hh_show;
}
#chat-room-div #TextAreaChatLog::before { content: ''; display: block; height: 100%; }
#chat-room-div[data-mbchc-mode="h"] #TextAreaChatLog::after { content: '𝗵 𝗶 𝘀 𝘁 𝗼 𝗿 𝘆 ⟨𝘗𝘨𝘜𝘱/𝘋𝘯⟩ 𝗌 𝖼 𝗋 𝗈 𝗅 𝗅 ⇅ ⟨𝘌𝘯𝘵𝘦𝘳⟩ 𝗌 𝖾 𝗇 𝖽 ↵ ⟨𝘛𝘢𝘣⟩ 𝖾 𝖽 𝗂 𝗍 ⌨ ⟨𝘌𝘴𝘤⟩ 𝖺 𝖻 𝗈 𝗋 𝗍 ⟲'; display: block; position: sticky; bottom: 0; background: black; color: orange; padding: 0 0.2ex; animation: 0.2s cubic-bezier(0.19, 1, 0.22, 1) mbchc_hist_hint_show; }
#InputChat:read-only { background: black; color: orange; }
@keyframes mbchc_hh _show { from {transform: translateX(-100%);} to {transform: translateX(0);} }
` ) // will always scroll the chat on CSS load, I can't be fucked to make it conditional
@keyframes mbchc_hist_hint _show { from {transform: translateX(-100%);} to {transform: translateX(0);} }
` )
// Actions
this . calculate _maps ( )
@@ -489,6 +603,14 @@ const/**@type {SDK.Hook}*/after = (name, f) => mod.hookFunction(name, 0, (na, n)
}
} )
prior ( 'ChatRoomSendChat' , ( ) => val ( U . ic , ic => ! ic . value . startsWith ( '@@@' ) && ic . value . startsWith ( '@' ) && ( ic . value = ic . value . replace ( U . RE [ '@' ] [ 1 ] , U . ACT ) . replace ( U . RE [ '@' ] [ 0 ] , U . replace _me ) ) ) )
//{
// 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)
// }
//})
after ( 'ChatRoomSendChat' , ( ) => { // FIXME actually make history a ring buffer of a given size. clear the array and push every string into it, compacting sequential equal strings into one.
const history = W . ChatRoomLastMessage
if ( ( history . length > 1 ) && ( history . at ( - 1 ) === history . at ( - 2 ) ) ) {
@@ -507,34 +629,45 @@ const/**@type {SDK.Hook}*/after = (name, f) => mod.hookFunction(name, 0, (na, n)
const /**@type {(this: HTMLTextAreaElement, e: KeyboardEvent) => void}*/ ickd = function ( e ) {
C . hide ( )
const ic = this // eslint-disable-line unicorn/no-this-assignment,@typescript-eslint/no-this-alias
if ( ic . readOnly && ! e . repeat ) switch ( e . key ) { // FIXME maybe deal with modifiers
case 'ArrowUp ' : case 'ArrowDown' : W . ChatRoomScrollHistory ( e . key === 'ArrowUp' ) ; break
case 'Escape' : case 'Tab' : case 'ArrowRight' : case 'ArrowLeft' :
if ( ic . readOnly ) switch ( e . key ) {
case 'Tab ' : case 'Escape' :
e . stopImmediatePropagation ( )
e . preventDefault ( ) // falls through
case 'Enter' : case 'Backspace' : H . exit ( ic , e ) // no default
case 'Enter' :
H . exit ( ic , e . key === 'Escape' ) // no default
}
}
after ( 'ChatRoomCreateElement' , ( ) => { // This thing runs on every frame actually.
C . e . parentElement ? ? D . body . append ( C . e )
val ( U . ic , ic => ic . dataset [ 'mbchc' ] ? ? void ic . addEventListener ( 'keydown' , ickd ) ? ? ( ic . dataset [ 'mbchc' ] = 'yes' ) )
//val(asa(HTMLDivElement, D.querySelector('#TextAreaChatLog')), c => c.scrollHeight > c.clientHeight || void U.pad_chat(c))
} )
prior ( 'ChatRoomClearAllElements' , ( ) => C . hide ( ) && void C . e . remove ( ) )
D . addEventListener ( 'click' , _ => C . hide ( ) ) // downstream handlers can capture clicks, but I can't be fucked to be honest
//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'
// }
//})
after ( 'ChatRoomResize' , ( ) => {
if ( W . CharacterGetCurrent ( ) === null && W . CurrentScreen === 'ChatRoom' && U . ic !== $ && D . querySelector ( '#TextAreaChatLog' ) !== null && ! C . hidden ) { // Upstream
W . ElementPositionFix ( C . e . id , ChatRoomFontSize , 800 , 65 , 200 , 835 )
}
} )
//D.addEventListener('keydown', event => void this.focus_chat(event)) // Looks like the club got better at this
mod . hookFunction ( 'ChatRoomScrollHistory' , 0 , ( [ up ] ) => void val ( U . ic , ic => { const input = ic . value , history = W . ChatRoomLastMessage
// FIXME we'll make it better when the history is a proper ring buffer
if ( ! ic . readOnly ) {
const /**@type {Map<string,number>}*/ map = new Map ( )
const /**@type {(l: string, i: number, I: string) => boolean}*/ cond = m _t ( input ) ? ( _ , i , _ _ ) => i > 0 : ( l , _ , i ) => l !== i && l . startsWith ( i )
if ( m _t ( history . reduce ( ( ax , l , i ) => cond ( l , i , input ) ? ax . set ( l , i ) : ax , map ) ) ) return U . bell ( )
val ( asa ( HTMLDivElemen t, D . querySelector ( '# TextAreaChatLog' ) ) , t => t . dataset [ 'mbchcHH' ] = ` 𝗵 𝗶 𝘀 𝘁 𝗼 𝗿 𝘆 : ${ m _t ( input ) ? 'Everything' : ` Prefix: ${ input } ` } ` )
H . enter ( ic , input , U . scrolled , new Set ( map . values ( ) ) )
H . enter ( ic , inpu t, W . ElementIsScrolledToEnd ( 'TextAreaChatLog' ) , new Set ( map . values ( ) ) )
}
if ( ic . readOnly ) { // this can't be an else, because we mutate state above. To be honest, this will always be true, but I want to make sure.
if ( H . ids === $ ) return U . bell ( ) // shouldn't happen?