Compare commits

..

6 Commits

Author SHA1 Message Date
c3d47fec77 Merge branch 'testing' 2024-08-26 14:52:29 +00:00
7e207b7e93 Indentation, amirite 2024-08-26 14:44:27 +00:00
1eaffc46be Adding elements to chat on every frame proved unwise, also made spellchecker happy. 2024-08-24 03:31:50 +00:00
966328327e stubs 107; history fixes 2024-08-22 18:13:04 +00:00
6034adae3c 107.13.0
another absolutely massive diff
* Disabled keyboard handler for focus management, the club got better at this
* Input history redone entirely, the code contains comments
* Completion now takes cursor position into account
* Dropped autoremoval of service messages in favour of future manual deletion
* Tons of typechecking cleanup and style improvements
* The code now has zero errors on the strictest TS settings I know
2024-08-18 01:15:56 +00:00
16308eccf1 105.13.0 semver; settings migration
[lots of internal change]
I'm determined to clean this mess up after all.
Instead of a total rewrite (which didn't work), I'll migrate the
code to JSDoc piece by piece. This is the first piece, setting up
some basic framework and testing out this and that.
Until I figure out how test this thing, I'll just ask peeps to import
the module from testing branch I guess.
Probably not gonna deal with anything new until I can turn "strict" on.
2024-07-13 21:55:45 +00:00
7 changed files with 6885 additions and 6452 deletions

459
ambient.d.ts vendored Normal file
View File

@ -0,0 +1,459 @@
interface ServerChatRoomMessage {
MBCHC_ID?: number
}
namespace BCE {
interface Matcher {
Tester: RegExp
Criteria?: {
TargetIsPlayer?: boolean
SenderIsPlayer?: boolean
DictionaryMatchers?: Array<OBJ<string>>
}
}
interface Trigger {
Event: string
Type: 'Emote' | 'Activity' | 'Action'
Matchers: Matcher[]
}
interface Patcher {
timer: number | undefined
patches: Array<[RegExp, string]>
cfs: {[k in 'anim' | 'pose']: () => Iterable<string>}
gen: (comp_func: () => Iterable<string>) => (this: Optional<ICommand, 'Tag'>) => undefined
copy(t: Trigger): Trigger
patch(): true | undefined
}
}
type SUBCOMMANDS = {
[k: string]: {
desc: string
args?: {[n: string]: OBJ}
cb: (mbchc: Window['MBCHC'], args: string[], __: unknown, ___?: unknown) => void
}
};
interface Window {
MBCHC: {
loader: () => void
DO_DATA: {
zones: OBJ<string>
verbs: {
[verb: string]: {
[zone: string]: {
self: string[]
others: string[]
}
}
}
}
MAP_ACTIONS: {
[verb: string]: {
[zones: string]: {
all?: string
self?: string
others?: string
}
}
}
MAP_ZONES: {[zone: string]: string[]}
LOADED: boolean
NEXT_MESSAGE: number
LOG_MESSAGES: boolean
LAST_HACKED: number | undefined
version: string
VERSION: string
Settings: Settings.Methods
TZ: TZ_Cache
SUBCOMMANDS_MBCHC: SUBCOMMANDS
H: InputHistory
U: Utils
AUTOHACK_ENABLED: boolean
RE_PREF_ACTIVITY_ME: RegExp
RE_PREF_ACTIVITY: RegExp
RE_ACT_CIDS: RegExp
RE_LAST_WORD: RegExp
RE_LAST_LETTER: RegExp
RE_ACTIVITY: RegExp
UTC_OFFSET: number
calculate_maps(): void
normalise_message(text: string, options?: OBJ<boolean>): string
donate_data(target: string): void
run_activity(char: Character, ag: AssetGroupItemName, action: ActivityName): void
send_activity(message: string): void
set_timezone(args: string[]): number | undefined
command_mbchc(this: Optional<ICommand, 'Tag'>, args: string, msg: string, parsed: string[]): undefined
command_activity(this: Optional<ICommand, 'Tag'>, args: string, msg: string, parsed: string[]): undefined
command_do(this: Optional<ICommand, 'Tag'>, args: string, msg: string, parsed: string[]): undefined
}
bcModSdk?: import('./node_modules/bondage-club-mod-sdk/dist/bcmodsdk.d.ts').ModSDKGlobalAPI;
FBC_VERSION?: string
bce_ActivityTriggers?: BCE.Trigger[]
bce_EventExpressions?: OBJ
}
type OBJ<T = unknown> = Record<string, T>;
type FUN = (...args: never[]) => unknown;
/**
* Stands for "Value or Function". Not remotely ideal, but the best I can come up with.
*/
type VOF<F extends FUN> = undefined | boolean | number | bigint | string | symbol | OBJ | Set<unknown> | Map<unknown, unknown> | FP.Interval | F;
/**
* I can write Haskell in every language.
* I was unbelievably tempted to go with emojis here, but I'm not a monster of this caliber.
* @see https://8fw.me/hell/a8aaaefcb6e30ec4b2e398c70d49b72a6b53232f.png
*/
namespace FP {
interface Interval {
proxy: object // eslint-disable-line @typescript-eslint/ban-types
min: number
max: number
mini: boolean
maxi: boolean
has(_: unknown, x: string): boolean
}
interface Pipeline<T> {
proxy: PipelineProxy<T>
me<N>(iterable: Iterable<N>): PipelineProxy<N>
[Symbol.iterator](): Iterator<T>
rdc<R>(initial: R, func: (accumulator: R, value: T) => R): R
any(func: (v: T) => boolean): boolean
all(func: (v: T) => boolean): boolean
map<R>(func: (value: T) => R): PipelineProxy<R>
sel(func: (value: T) => boolean): PipelineProxy<T>
}
interface PipelineProxy<T> extends Pipeline<T> {
[i: number]: T | undefined
}
/**
* Creates an iteration pipeline.
*/
type P = <T>(iterable: Iterable<T>) => PipelineProxy<T>;
/**
* A silly helper to kinda c[au]rry values. Basically equivalent to:
* `const a = value; return func(a)`
* Stands for "Carry, Use, Return".
*/
type cur = <T, R>(value: T, func: (value: T) => R) => R;
/**
* Convert `null` to `undefined`.
*/ // eslint-disable-next-line @typescript-eslint/ban-types
type n2u = <T>(value: T | undefined | null) => T | undefined;
/**
* Enumerate keys of an object. Type-safe, also shorter.
*/
type enu = <O extends OBJ>(object: O) => Array<keyof O>;
/**
* Like `carry()` but does nothing if `value` is `null` or `undefined`.
* Something like an `Option<>` when you want to just pass `undefined` along, but
* run a function on actual values. Also coverts `null` to `undefined`.
*/ // eslint-disable-next-line @typescript-eslint/ban-types
type val = <T, R>(value: T | null | undefined, func: (value: T) => R) => R | undefined;
/**
* `+` as a function
*/
type add = (x: number, y: number) => number;
/**
* `-` as a function
*/
type sub = (x: number, y: number) => number;
/**
* `>` as a function
*/
type cgt = (x: number, y: number) => boolean;
/**
* `>=` as a function
*/
type cge = (x: number, y: number) => boolean;
/**
* `<` as a function
*/
type clt = (x: number, y: number) => boolean;
/**
* `<=` as a function
*/
type cle = (x: number, y: number) => boolean;
/**
* Converts a string into a base 10 integer. Not only is it shorter than `Number.parseInt`,
* it also converts `NaN` to `undefined`, because I find it much easier to only have
* one nil value for everything.
*/
type int = (text: string) => number | undefined;
/**
* This is a type assertion helper for Typescript. Probably not very useful in general.
*/
type fun = <F extends FUN>(func: unknown) => func is F;
/**
* Takes anything, ignores non-functions, calls functions with supplied parameters.
*/
type run = <F extends FUN>(functions_or_values: Array<VOF<F>>, ...args: Parameters<F>) => true;
/**
* Takes anything, ignores non-functions, calls functions with `undefined` as a single parameter.
*/
type yes = (...args: Array<VOF<(_: undefined) => unknown>>) => true;
/**
* Takes anything, ignores non-functions, calls functions with a single provided parameter.
* Always returns the parameter itself.
*/
type mut = <T>(value: T, ...args: Array<VOF<(value: T) => unknown>>) => T;
/**
* `delete` as a function
*/ // eslint-disable-next-line @typescript-eslint/ban-types
type del = <T extends object>(object: T, property: keyof T) => T;
/**
* Short for `assert`. Throws, if value is `undefined`, or if `condition(value)` is false.
*/
type ass = <T>(error: string, value: T | undefined, condition?: (value: T) => boolean) => T | never;
/**
* Casts value as a given prototype or returns undefined.
*/
type asa = <T>(prototype: new () => T, value: unknown) => T | undefined;
/**
* `catch` as a function. Short for `rescue`.
*/
type rsc = (operation: (_: undefined) => unknown, exception_handler: (error: unknown) => unknown) => true;
/**
* Returns a proxy for `in` operator.
* Use it like `2 in rng(0, 4)`.
*/ // eslint-disable-next-line @typescript-eslint/ban-types
type rng = (min: number, max: number, min_inclusive?: boolean, max_inclusive?: boolean) => object;
/**
* Something slightly better than `Boolean()`.
* True is `null`, `undefined`, `false`, empty strings, sets, maps, arrays and objects.
* Short for "empty".
*/
type m_t = (value: unknown) => boolean;
/**
* `while()` as a function. Executes the second callback while the first one is true.
* Always bound by max number of iterations.
* Returns the last value from the action or undefined if the action never ran.
*/
type loo = <T>(max: number, condition: (_: undefined) => boolean, action: (_: undefined) => T) => T | undefined;
}
namespace SDK {
type GDPT<F> = import('./node_modules/bondage-club-mod-sdk/dist/bcmodsdk.d.ts').GetDotedPathType<typeof globalThis, F>;
type Void<F extends FUN> = (...args: Parameters<F>) => unknown;
type Hook = <F extends string>(name: F, hook: Void<GDPT<F>>) => () => unknown;
}
namespace Cons {
type MS = {w: 'warn', i: 'info', d: 'debug', l: 'log'};
type E = (error: unknown) => true;
type F = (message: string) => true;
interface Wrap {
readonly ms: MS
e: E
w: F
i: F
d: F
l: F
gen: (m: keyof MS) => F
}
}
namespace Settings {
/**
* The whole `Player.OnlineSettings`.
*/
type V0 = PlayerOnlineSettings & {MBCHC?: {timezones?: Record<number, number>}};
/**
* Specifically `MBCHC` inside `Player.ExtensionSettings`.
*/
interface V1 {
TZ: Record<number, number>
}
interface Methods {
migrate_0_1(v0: V0): true
save(func?: (v1: V1) => unknown): true
replace(new_v1: V1): true
'purge!'(): true
get v0(): V0 | undefined
get v1(): V1
}
}
/**
* We need a place to cache the timezones instead of the `Character` object itself.
*/
interface TZ_Cache {
map: Map<number, number>
RE: RegExp
parse(description: string | undefined): number | undefined
memo(member_number: number, description?: string | undefined): number | undefined
lookup(character: Character): number | undefined
}
interface Utils {
remove_loader_hook: (() => unknown) | undefined
RE: {SPACES: RegExp, REL: {L: RegExp, R: RegExp}, '@': [RegExp, RegExp]}
RGB: {Polly: string, Mute: string}
ACT: string,
get crc(): Character[]
get ic(): HTMLTextAreaElement | undefined
cid(character: Character): number | undefined
dn(character: Character): string
current(): string
style<T>(query: string, func: (s: CSSStyleDeclaration) => T): T | undefined
inform(html: string): true
report(error: unknown): true
/**
* Splits a string into words by continuous whitespace sequences. Some examples:
* ```
* "" => [""]
* " " => ["", ""]
* "f g" => ["f", "g"]
* " f g " => ["", "f", "g", ""]
* ```
*/
split(text: string): string[]
abs2char(index: number): Character
rel2char(target: string): Character
cid2char(cid: number): Character
target2char(target: string): Character
mkdiv(html?: string): HTMLDivElement
bell(): true
targets(me2?: boolean, check_perms?: boolean): Set<string>
complete_mbchc(this: Optional<ICommand, 'Tag'>): undefined
complete_do_target(actions: {self: unknown, others: unknown}): Set<string>
complete_do(this: Optional<ICommand, 'Tag'>): undefined
replace_me(_match: string, _offset: number, whole: string): string
//pad_chat(chat: HTMLDivElement): undefined
}
interface Complete {
S_OPTS: {behavior: 'instant'}
/**
* The suggestions panel.
* Its structure is (outer div) -> (container div) -> (multiple suggestion div elements)
*/
e: HTMLDivElement
/**
* The container div.
*/
get div(): Element | undefined
/**
* Longest common prefix, or an empty string for an empty set.
*/
lcp(candidates: Set<string>): string
/**
* !WARNING! Mutate the given set in accordance with the input.
* Returns the input with completion done and removes all failed candidates.
* If the set is empty, the completion failed and the result is the unmodified input.
* If the set has more than one value, these are all new candidates, and the input was completed with the lcp.
* Otherwise, the only successful candidate will remain in the set and the input was completed to it.
*/
complete_word(input: string, candidates: Set<string>, ignore_case?: boolean | undefined): string
/**
* Show the suggestions to the user.
*/
hint(candidates: Set<string>): true
/**
* Returns false if the suggestion panel is visible.
*/
get hidden(): boolean
/**
* Makes the suggestions panel disappear.
*/
hide(): true
/**
* The whole deal. Will read and modify the chat input window.
* Takes a callback that will receive current input split into words
* and should return all possible candidates for the word being completed.
* Note: if the input ends with whitespace, the last word will be empty.
*/
complete(func: (words: string[]) => Set<string>, ignore_case?: boolean | undefined): true
/**
* Case-insensitive complete.
*/
icomplete(func: (words: string[]) => Set<string>): true
}
/**
* So, this is what happens. We have two modes: input mode and history mode. In the history mode the element is read-only.
* In the input mode:
* If the input is empty, we just scroll the history as usual
* Otherwise, we build a history set using the input as prefix
* The history set filters through the history, keeping only unique lines that start with the prefix along with indices
* But we exclude lines that match the input exactly
* It keeps the original input in a separate place too
* If the set is empty, we bell out
* Otherwise, we enter the history mode and invoke the first search
* In the history mode:
* We search up or down, using the index as a starting point for the next match, treating the set as a ring
* If the found line is the same as the current line, we bell out
* Upon finding a next match, we replace the input with its text and set the index appropriately
* We exit the history mode using the InputChat keydown handler, on Tab, Escape or Enter
* Escape restores the saved input, discarding the history line
* Tab keeps the current text and unlocks the element, allowing it to be edited
* Enter keeps the current text and sends it as the message as usual
*/
interface InputHistory {
/**
* Whether the chat log is scrolled to the end when we enter history mode.
*/
bottom: boolean | undefined
/**
* The initial user input when we enter the history mode.
*/
input: string | undefined
/**
* The indices of the lines that match the prefix.
*/
ids: Set<number> | undefined
/**
* Enter the history mode.
*/
enter(textarea: HTMLTextAreaElement, input: string, bottom: boolean, ids: Set<number>): true
/**
* Exit the history mode and optionally restore original input.
*/
exit(textarea: HTMLTextAreaElement, restore_input: boolean): true
}
// FIXME spread around readonly where appropriate

View File

@ -2,17 +2,24 @@
"include": [
"node_modules/bc-stubs/bc/**/*.d.ts",
"node_modules/bondage-club-mod-sdk/dist/**/*.d.ts",
"typedef.d.ts",
"ambient.d.ts",
"mbchc.mjs",
"server.js"
],
"compilerOptions": {
"lib": [
"es2022",
"DOM"
],
"target": "es2023",
"allowJs": true,
"checkJs": true,
"strict": false,
"noImplicitOverride": true
"allowUnreachableCode": false,
"allowUnusedLabels": false,
"exactOptionalPropertyTypes": true,
"noImplicitOverride": true,
"noImplicitReturns": true,
"noPropertyAccessFromIndexSignature": true,
"noUncheckedIndexedAccess": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"strict": true,
"noEmit": true
}
}

1117
mbchc.mjs

File diff suppressed because it is too large Load Diff

15
package-lock.json generated
View File

@ -1,16 +1,17 @@
{
"name": "mbchc",
"version": "0.0.12",
"version": "107.13.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "mbchc",
"version": "0.0.12",
"license": "SEE LICENSE IN LICENSE.",
"version": "107.13.0",
"license": "SEE LICENSE IN LICENSE",
"devDependencies": {
"bc-stubs": "^105.0.0",
"bc-stubs": "^107.0.0",
"bondage-club-mod-sdk": "^1.2.0",
"typescript": "^5.5.2",
"xo": "^0.56.0"
}
},
@ -1073,9 +1074,9 @@
"dev": true
},
"node_modules/bc-stubs": {
"version": "105.0.3",
"resolved": "https://registry.npmjs.org/bc-stubs/-/bc-stubs-105.0.3.tgz",
"integrity": "sha512-haKRphxOdPQT/9W6s5L0x5DFDttOrk0xBddFfoLnMQlz7AiXnU3TGdlJ+biWpmQtyHc7WrJARbYd4Wf7+a4j8g==",
"version": "107.0.0",
"resolved": "https://registry.npmjs.org/bc-stubs/-/bc-stubs-107.0.0.tgz",
"integrity": "sha512-PUEvMGe7dDm+lKy6YJIWv1xWCOBVt/WAn4xrG8mBUoGobZVBqOrg+x5lOV0HvG9fcBB8K5NEaXMciE5TOTWkBA==",
"dev": true,
"dependencies": {
"socket.io-client": "4.6.1"

View File

@ -1,38 +1,92 @@
{
"name": "mbchc",
"version": "0.0.12",
"version": "107.13.0",
"description": "Mute's Bondage Club Hacks Collection",
"author": "Mute",
"type": "module",
"devDependencies": {
"xo": "^0.56.0",
"bc-stubs": "^105.0.0",
"bondage-club-mod-sdk": "^1.2.0"
"bc-stubs": "^107.0.0",
"bondage-club-mod-sdk": "^1.2.0",
"typescript": "^5.5.2",
"xo": "^0.56.0"
},
"license": "SEE LICENSE IN LICENSE.",
"xo": {
"env": [
"browser",
"node"
],
"license": "SEE LICENSE IN LICENSE",
"eslintConfig": {
"root": true,
"extends": ["xo", "xo-typescript", "plugin:unicorn/recommended"],
"parser": "@typescript-eslint/parser",
"parserOptions": { "project": ["./jsconfig.json"] },
"plugins": ["@typescript-eslint", "unicorn"],
"rules": {
"@typescript-eslint/brace-style": "off",
"@typescript-eslint/comma-dangle": ["error", "only-multiline"],
"@typescript-eslint/consistent-indexed-object-style": "off",
"@typescript-eslint/consistent-type-definitions": "off",
"@typescript-eslint/consistent-type-imports": "off",
"@typescript-eslint/dot-notation": "off",
"@typescript-eslint/lines-between-class-members": "off",
"@typescript-eslint/member-delimiter-style": "off",
"@typescript-eslint/naming-convention": "off",
"@typescript-eslint/no-confusing-void-expression": ["error", {
"ignoreVoidOperator": true
}],
"@typescript-eslint/no-explicit-any": "error",
"@typescript-eslint/no-meaningless-void-operator": "off",
"@typescript-eslint/no-unused-expressions": "off",
"@typescript-eslint/object-curly-spacing": "off",
"@typescript-eslint/padding-line-between-statements": "off",
"@typescript-eslint/semi": "off",
"@typescript-eslint/space-before-function-paren": "off",
"@typescript-eslint/strict-boolean-expressions": ["error", {
"allowString": false,
"allowNumber": false,
"allowNullableObject": false
}],
"array-element-newline": "off",
"brace-style": "off",
"camelcase": "off",
"capitalized-comments": "off",
"curly": "off",
"generator-star-spacing": "off",
"max-nested-callbacks": "off",
"max-params": "off",
"max-statements-per-line": "off",
"new-cap": "off",
"no-mixed-spaces-and-tabs": ["error", "smart-tabs"],
"no-return-assign": "off",
"no-unused-expressions": "off",
"no-unused-vars": ["error", {
"argsIgnorePattern": "^_",
"destructuredArrayIgnorePattern": "^_"
}],
"no-void": "off",
"padding-line-between-statements": "off",
"object-curly-newline": "off",
"one-var": "off",
"one-var-declaration-per-line": "off",
"semi": "off",
"space-before-function-paren": "off",
"spaced-comment": "off",
"unicorn/catch-error-name": ["error", {"name": "x"}],
"unicorn/consistent-function-scoping": "off",
"unicorn/no-array-callback-reference": "off",
"unicorn/no-array-for-each": "off",
"unicorn/no-array-reduce": "off",
"unicorn/no-nested-ternary": "off",
"unicorn/prevent-abbreviations": ["error", {
"allowList": {"cur": true, "args": true, "func": true, "val": true, "mod": true, "msg": true, "i": true, "e": true}
}],
"unicorn/switch-case-braces": ["error", "avoid"],
"fake/fuck-commas": "off"
},
"overrides": [
{
"files": ["*.d.ts"],
"rules": {
"@typescript-eslint/semi": "error",
"no-unused-vars": "off"
}
}
]
}
}

View File

@ -1,7 +1,8 @@
import {readFileSync} from 'node:fs'
import {createServer} from 'node:http'
import {argv} from 'node:process'
const config = {host: '127.0.0.1', port: 9696}
const config = {host: '127.0.0.1', port: 9696, filename: argv[2] ?? 'mbchc.mjs'}
const h_cors = {
'Access-Control-Max-Age': '86400',
@ -11,23 +12,19 @@ const h_cors = {
'Access-Control-Allow-Headers': '*',
// 'Access-Control-Allow-Credentials': 'false', // omit this header to disallow
}
const h_all = Object.assign({
const h_all = {
'Content-Type': 'text/javascript',
'Cache-Control': 'no-cache',
}, h_cors)
...h_cors
}
/**
* @typedef {import('node:http').ServerResponse} ServerResponse
* @type {Record<string,function(ServerResponse):void>}
*/
const resp = {
GET(rx) { rx.writeHead(200, h_all); rx.write(readFileSync('./mbchc.mjs')) },
/** @type {Record<string,((request: import('node:http').ServerResponse) => void)>} */ const resp = {
GET(rx) { rx.writeHead(200, h_all); rx.write(readFileSync(config.filename)) },
OPTIONS(rx) { rx.writeHead(204, h_cors) },
}
const server = createServer((rq, rx) => {
resp[rq.method] && (new URL(`http://${config.host}:${config.port}${rq.url}`)).pathname === '/' ? resp[rq.method](rx) : rx.writeHead(400)
rx.end()
console.log('%s %d %s %s', (new Date()).toISOString(), rx.statusCode, rq.method, rq.url)
rq.method !== undefined && resp[rq.method] !== undefined && (new URL(`http://${config.host}:${config.port}${rq.url}`)).pathname === '/' ? resp[rq.method](rx) : rx.writeHead(400)
rx.end(() => void console.log('%s %d %s %s', (new Date()).toISOString(), rx.statusCode, rq.method, rq.url))
})
server.listen(config.port, config.host, () => console.log(`Server started at http://${config.host}:${config.port}`))
server.listen(config.port, config.host, () => void console.log(`Server started at http://${config.host}:${config.port} for ${config.filename}`))

10
typedef.d.ts vendored
View File

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