Compare commits
No commits in common. "master" and "v.1" have entirely different histories.
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1 +0,0 @@
|
||||||
node_modules
|
|
11
README.md
11
README.md
|
@ -1,10 +1,3 @@
|
||||||
# Mute's Bondage Club Hacks Collection
|
Mute's Bondage Club Hacks Collection
|
||||||
This document is updated with stable releases, for additional documentation please consult [[the wiki|https://code.fleshless.org/mute/MBCHC/wiki/Home]].
|
|
||||||
|
|
||||||
* Unstable doc: https://code.fleshless.org/mute/MBCHC/wiki/unstable
|
[[https://code.fleshless.org/mute/MBCHC/wiki]]
|
||||||
|
|
||||||
## PSA
|
|
||||||
The only supported version is unstable, don't use any other.
|
|
||||||
|
|
||||||
## Feedback
|
|
||||||
We are available at the club in a private room named `MBCHC`. If Mute isn't there, leave a message.
|
|
||||||
|
|
468
ambient.d.ts
vendored
468
ambient.d.ts
vendored
|
@ -1,468 +0,0 @@
|
||||||
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
|
|
||||||
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
|
|
||||||
scroll(): true
|
|
||||||
get scrolled(): boolean
|
|
||||||
rescroll(func: (_: undefined) => unknown): true
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Specific key handlers.
|
|
||||||
*/
|
|
||||||
key: Record<string, (event: KeyboardEvent, textarea: HTMLTextAreaElement) => true>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set or unset the readonly mode on the input.
|
|
||||||
*/
|
|
||||||
icro(textarea: HTMLTextAreaElement, readonly: boolean): true
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Enter the history mode.
|
|
||||||
*/
|
|
||||||
enter(textarea: HTMLTextAreaElement, input: string, bottom: boolean, ids: Set<number>): true
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Exit the history mode and proc the key event.
|
|
||||||
*/
|
|
||||||
exit(textarea: HTMLTextAreaElement, e: KeyboardEvent): true
|
|
||||||
}
|
|
||||||
|
|
||||||
// FIXME spread around readonly where appropriate
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -1,25 +0,0 @@
|
||||||
{
|
|
||||||
"include": [
|
|
||||||
"node_modules/bc-stubs/bc/**/*.d.ts",
|
|
||||||
"node_modules/bondage-club-mod-sdk/dist/**/*.d.ts",
|
|
||||||
"ambient.d.ts",
|
|
||||||
"mbchc.mjs",
|
|
||||||
"server.js"
|
|
||||||
],
|
|
||||||
"compilerOptions": {
|
|
||||||
"target": "es2023",
|
|
||||||
"allowJs": true,
|
|
||||||
"checkJs": true,
|
|
||||||
"allowUnreachableCode": false,
|
|
||||||
"allowUnusedLabels": false,
|
|
||||||
"exactOptionalPropertyTypes": true,
|
|
||||||
"noImplicitOverride": true,
|
|
||||||
"noImplicitReturns": true,
|
|
||||||
"noPropertyAccessFromIndexSignature": true,
|
|
||||||
"noUncheckedIndexedAccess": true,
|
|
||||||
"noUnusedLocals": true,
|
|
||||||
"noUnusedParameters": true,
|
|
||||||
"strict": true,
|
|
||||||
"noEmit": true
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,667 +1,248 @@
|
||||||
// ==UserScript==
|
// ==UserScript==
|
||||||
// @name MBCHC
|
// @name MBCHC
|
||||||
// @version dev.8
|
// @version dev.1
|
||||||
// @description Mute's Bondage Club Hacks Collection
|
// @description Mute's Bondage Club Hacks Collection
|
||||||
// @author codename.mute@proton.me
|
// @author codename.mute@proton.me
|
||||||
// @namespace https://code.fleshless.org/mute/
|
|
||||||
// @homepage https://code.fleshless.org/mute/MBCHC
|
// @homepage https://code.fleshless.org/mute/MBCHC
|
||||||
// @updateURL https://code.fleshless.org/mute/MBCHC/raw/branch/master/mbchc-dev.user.js
|
// @namespace https://code.fleshless.org/mute/
|
||||||
// @downloadURL https://code.fleshless.org/mute/MBCHC/raw/branch/master/mbchc-dev.user.js
|
|
||||||
// @match https://bondageprojects.elementfx.com/R*
|
// @match https://bondageprojects.elementfx.com/R*
|
||||||
// @match https://www.bondageprojects.elementfx.com/R*
|
// @match https://www.bondageprojects.elementfx.com/R*
|
||||||
// @match https://bondage-europe.com/R*
|
// @match https://bondage-europe.com/R*
|
||||||
// @match https://www.bondage-europe.com/R*
|
// @match https://www.bondage-europe.com/R*
|
||||||
// @match http://localhost:*/*
|
|
||||||
// @match http://127.0.0.1:*/*
|
|
||||||
// @grant none
|
// @grant none
|
||||||
// ==/UserScript==
|
// ==/UserScript==
|
||||||
|
|
||||||
(function() {
|
(function() {
|
||||||
"use strict";
|
'use strict';
|
||||||
if (!window.AsylumGGTSSAddItems) throw "AsylumGGTSSAddItems() not found, aborting MBCHC loading"
|
if (!window.AsylumGGTSSAddItems) throw "AsylumGGTSSAddItems() not found, aborting"
|
||||||
if (window.MBCHC) throw "MBCHC found, aborting loading"
|
|
||||||
window.MBCHC = {
|
|
||||||
VERSION: "dev.8",
|
|
||||||
TARGET_VERSION: "R93",
|
|
||||||
NEXT_MESSAGE: 1,
|
|
||||||
LOG_MESSAGES: false,
|
|
||||||
RETHROW: false,
|
|
||||||
LOADED: false,
|
|
||||||
AUTOHACK_ENABLED: false,
|
|
||||||
LAST_HACKED: null,
|
|
||||||
HISTORY_MODE: false,
|
|
||||||
RE_TITLE: /^[a-zA-Z]+$/,
|
|
||||||
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,
|
|
||||||
HIDE_SPECIAL: ["Activity","Emoticon"],
|
|
||||||
HIDE_BODY: ["Blush","BodyLower","BodyUpper","Eyebrows","Eyes","Eyes2","Face","Fluids","HairBack","HairFront","Hands","Head","LeftHand","Mouth","Nipples","Pussy","RightHand"],
|
|
||||||
HIDE_CLOTHES: [
|
|
||||||
"Cloth","ClothAccessory","Necklace","Suit","ClothLower","SuitLower","Bra","Corset","Panties",
|
|
||||||
"Socks","RightAnklet","LeftAnklet","Garters","Shoes","Hat","HairAccessory3","HairAccessory1","HairAccessory2",
|
|
||||||
"Gloves","Bracelet","Glasses","Mask","TailStraps","Wings"
|
|
||||||
],
|
|
||||||
HIDE_ITEMS: [
|
|
||||||
"ItemMisc","ItemEars","ItemHead","ItemNose","ItemHood","ItemAddon","ItemMouth","ItemMouth2","ItemMouth3",
|
|
||||||
"ItemArms","ItemNeckAccessories","ItemNeck","ItemNeckRestraints","ItemNipples","ItemNipplesPiercings","ItemBreast","ItemTorso","ItemTorso2",
|
|
||||||
"ItemHands","ItemPelvis","ItemVulva","ItemVulvaPiercings",
|
|
||||||
"ItemDevices","ItemLegs","ItemFeet","ItemBoots"
|
|
||||||
],
|
|
||||||
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
|
// Static data
|
||||||
"wiggle|shake": {"Arms,Breast,Boots,Butt,Ears,Feet,Hands,Nose,Pelvis,Torso": {self: "Wiggle"}},
|
window.MBCHC = {
|
||||||
|
NEXT_MESSAGE: 1,
|
||||||
|
LOG_MESSAGES: false,
|
||||||
|
LOADED: false,
|
||||||
|
VERSION: 'dev',
|
||||||
|
AUTOHACK_ENABLED: false,
|
||||||
|
LAST_HACKED: null,
|
||||||
|
RE_TITLE: /^[a-zA-Z]+$/,
|
||||||
|
HIDE_SPECIAL: ["Activity","Emoticon"],
|
||||||
|
HIDE_BODY: ["Blush","BodyLower","BodyUpper","Eyebrows","Eyes","Eyes2","Face","Fluids","HairBack","HairFront","Hands","Head","Mouth","Nipples","Pussy"],
|
||||||
|
HIDE_CLOTHES: [
|
||||||
|
"Cloth","ClothAccessory","Necklace","Suit","ClothLower","SuitLower","Bra","Corset","Panties",
|
||||||
|
"Socks","RightAnklet","LeftAnklet","Garters","Shoes","Hat","HairAccessory3","HairAccessory1","HairAccessory2",
|
||||||
|
"Gloves","Bracelet","Glasses","Mask","TailStraps","Wings"
|
||||||
|
],
|
||||||
|
HIDE_ITEMS: [
|
||||||
|
"ItemMisc","ItemEars","ItemHead","ItemNose","ItemHood","ItemAddon","ItemMouth","ItemMouth2","ItemMouth3",
|
||||||
|
"ItemArms","ItemNeckAccessories","ItemNeck","ItemNeckRestraints","ItemNipples","ItemNipplesPiercings","ItemBreast","ItemTorso",
|
||||||
|
"ItemHands","ItemPelvis","ItemVulva","ItemVulvaPiercings",
|
||||||
|
"ItemDevices","ItemLegs","ItemFeet","ItemBoots"
|
||||||
|
],
|
||||||
|
COMMANDS: [
|
||||||
|
{ Tag: "disappear",
|
||||||
|
Description: ": Become invisible; requires anal hook (hair)",
|
||||||
|
Action: (argline, cmdline, args) => {
|
||||||
|
try {
|
||||||
|
window.MBCHC.make_my_anal_hook_hide_body()
|
||||||
|
} catch (x) { window.ChatRoomSendLocal(`(Error: ${x.toString()})`) }
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ Tag: "title",
|
||||||
|
Description: "[Title]: (WIP) Set a custom title (one short word, letters only)",
|
||||||
|
Action: (argline, cmdline, args) => {
|
||||||
|
try {
|
||||||
|
window.MBCHC.action_title(args)
|
||||||
|
} catch (x) { window.ChatRoomSendLocal(`(Error: ${x.toString()})`) }
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ Tag: "donate",
|
||||||
|
Description: "[MemberNumber]: Buy data and send it to recipient",
|
||||||
|
Action: (argline, cmdline, args) => {
|
||||||
|
try {
|
||||||
|
window.MBCHC.action_donate(args)
|
||||||
|
} catch (x) { window.ChatRoomSendLocal(`(Error: ${x.toString()})`) }
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ Tag: "autohack",
|
||||||
|
Description: ": Toggle autohack mode",
|
||||||
|
Action: (argline, cmdline, args) => {
|
||||||
|
try {
|
||||||
|
window.MBCHC.action_autohack()
|
||||||
|
} catch (x) { window.ChatRoomSendLocal(`(Error: ${x.toString()})`) }
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ Tag: "nod",
|
||||||
|
Description: ": Nod",
|
||||||
|
Action: (argline, cmdline, args) => {
|
||||||
|
try {
|
||||||
|
window.MBCHC.run_activity(window.Player, "ItemHead", "Nod")
|
||||||
|
} catch (x) { window.ChatRoomSendLocal(`(Error: ${x.toString()})`) }
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ Tag: "shake",
|
||||||
|
Description: ": Shake your head",
|
||||||
|
Action: (argline, cmdline, args) => {
|
||||||
|
try {
|
||||||
|
window.MBCHC.run_activity(window.Player, "ItemHead", "Wiggle")
|
||||||
|
} catch (x) { window.ChatRoomSendLocal(`(Error: ${x.toString()})`) }
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ Tag: "shrug",
|
||||||
|
Description: ": Shrug",
|
||||||
|
Action: (argline, cmdline, args) => {
|
||||||
|
try {
|
||||||
|
window.MBCHC.send_activity(`${window.CharacterNickname(window.Player)} shrugs.`)
|
||||||
|
} catch (x) { window.ChatRoomSendLocal(`(Error: ${x.toString()})`) }
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ Tag: "myself",
|
||||||
|
Description: "[Message]: Send a custom activity as yourself (or \"@Message\")",
|
||||||
|
Action: (argline, cmdline, args) => {
|
||||||
|
if (!window.MBCHC.empty(argline)) {
|
||||||
|
try {
|
||||||
|
let message = window.MBCHC.add_full_stop(argline)
|
||||||
|
window.MBCHC.send_activity(`${window.CharacterNickname(window.Player)} ${message}`)
|
||||||
|
} catch (x) { window.ChatRoomSendLocal(`(Error: ${x.toString()})`) }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ Tag: "activity",
|
||||||
|
Description: "[Message]: Send a custom activity (or \"@@Message\")",
|
||||||
|
Action: (argline, cmdline, args) => {
|
||||||
|
if (!window.MBCHC.empty(argline)) {
|
||||||
|
try {
|
||||||
|
let message = window.MBCHC.add_full_stop(window.MBCHC.capitalise(argline))
|
||||||
|
window.MBCHC.send_activity(message)
|
||||||
|
} catch (x) { window.ChatRoomSendLocal(`(Error: ${x.toString()})`) }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
log: function(msg) {return("MBCHC: " + msg.toString())},
|
||||||
|
empty: function(text) {
|
||||||
|
if (!text) return(true)
|
||||||
|
if (String(text).trim().length < 1) return(true)
|
||||||
|
return(false)
|
||||||
|
},
|
||||||
|
add_full_stop: function(text) {
|
||||||
|
if (text.endsWith('.')) return(text)
|
||||||
|
return(`${text}.`)
|
||||||
|
},
|
||||||
|
capitalise: function(text, lower = false) {
|
||||||
|
let first = text.at(0).toLocaleUpperCase()
|
||||||
|
let rest = text.slice(1)
|
||||||
|
if (lower) rest = rest.toLocaleLowerCase()
|
||||||
|
return(first + rest)
|
||||||
|
},
|
||||||
|
// we need this one here, this is our main loading hook
|
||||||
|
orig_AsylumGGTSSAddItems: window.AsylumGGTSSAddItems,
|
||||||
|
} // MBCHC
|
||||||
|
|
||||||
// action target
|
// Loader
|
||||||
"whisper": {Ears: {others: "Whisper"}},
|
window.AsylumGGTSSAddItems = function() {
|
||||||
"choke": {Neck: {all: "Choke"}},
|
if (!window.MBCHC.LOADED) {
|
||||||
"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
|
// Save originals hopefully after patching
|
||||||
"item": {
|
window.MBCHC.orig_ChatRoomMessageInvolvesPlayer = window.ChatRoomMessageInvolvesPlayer
|
||||||
"Breast,Butt,Feet,Legs": {all: "SpankItem|TickleItem|RubItem|RollItem|MasturbateItem|PourItem|ShockItem|Inject"},
|
window.MBCHC.orig_ChatRoomReceiveSuitcaseMoney = window.ChatRoomReceiveSuitcaseMoney
|
||||||
"Nipples,Pelvis": {all: "SpankItem|TickleItem|RubItem|RollItem|MasturbateItem|PourItem|ShockItem"},
|
window.MBCHC.orig_ChatRoomSendChat = window.ChatRoomSendChat
|
||||||
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: {
|
|
||||||
"versions": {desc: "show the mod versions across the room", cb: mbchc => mbchc.inform(mbchc.gather_versions().map(c => `<div><b>${c.name}</b> (${c.cid}): ${c.version}</div>`).join(""))},
|
|
||||||
"autohack": {desc: "toggle the autohack feature", cb: mbchc => mbchc.inform(`Autohack is now ${(mbchc.AUTOHACK_ENABLED = !mbchc.AUTOHACK_ENABLED) ? "enabled" : "disabled"}`)},
|
|
||||||
"disappear": {desc: "become invisible (requires anal hook -> hair)", cb: mbchc => mbchc.disappear()},
|
|
||||||
"donate": {desc: "Buy data and send it to recipient", args: {TARGET: {}}, cb: (mbchc, args) => mbchc.donate_data(args[0])},
|
|
||||||
"title": {desc: "set a custom title (<b>WIP</b>)", args: {TITLE: {}}, cb: (mbchc, args) => mbchc.title(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 => {if (window.Player.OnlineSettings.MBCHC) {delete window.Player.OnlineSettings.MBCHC; mbchc.save_settings()}}},
|
|
||||||
},
|
|
||||||
ensure: function(error, callback) {
|
|
||||||
let result = callback.call(this)
|
|
||||||
if (!result) throw error
|
|
||||||
return(result)
|
|
||||||
},
|
|
||||||
calculate_maps: function() {
|
|
||||||
this.DO_DATA = {verbs: {}, zones: {}}
|
|
||||||
for (let [verbs, data] of Object.entries(this.MAP_ACTIONS)) {
|
|
||||||
let unwound = {}
|
|
||||||
for (let [zones, actions] of Object.entries(data)) {
|
|
||||||
let all = (actions.all) ? actions.all.split("|") : []
|
|
||||||
let processed = {self: (actions.self) ? actions.self.split("|").concat(all) : all, others: (actions.others) ? actions.others.split("|").concat(all) : all}
|
|
||||||
for (let zone of zones.split(",")) unwound[`Item${zone}`] = processed
|
|
||||||
}
|
|
||||||
for (let verb of verbs.split("|")) this.DO_DATA.verbs[verb] = unwound
|
|
||||||
}
|
|
||||||
for (let [ag, zones] of Object.entries(this.MAP_ZONES)) for (let zone of zones) this.DO_DATA.zones[zone] = ag
|
|
||||||
},
|
|
||||||
settings: function(setting = null) {
|
|
||||||
let settings = window.Player.OnlineSettings.MBCHC || {}
|
|
||||||
return(setting ? settings[setting] : settings)
|
|
||||||
},
|
|
||||||
save_settings: function(cb = null) {
|
|
||||||
if (cb) {
|
|
||||||
if (!window.Player.OnlineSettings.MBCHC) window.Player.OnlineSettings.MBCHC = {}
|
|
||||||
cb.call(this, window.Player.OnlineSettings.MBCHC)
|
|
||||||
}
|
|
||||||
window.ServerAccountUpdate.QueueData({OnlineSettings: window.Player.OnlineSettings})
|
|
||||||
},
|
|
||||||
log: function(level, msg) {console[level]("MBCHC: " + String(msg))},
|
|
||||||
empty: function(text) {
|
|
||||||
if (!text) return(true)
|
|
||||||
if (String(text).trim().length < 1) return(true)
|
|
||||||
return(false)
|
|
||||||
},
|
|
||||||
normalise_message: function(text, options = {}) {
|
|
||||||
let result = text
|
|
||||||
if (options.trim) result = result.trim()
|
|
||||||
if (options.low) result = result.toLocaleLowerCase()
|
|
||||||
if (options.up) {
|
|
||||||
let first = result.at(0).toLocaleUpperCase()
|
|
||||||
let rest = result.slice(1)
|
|
||||||
result = first + rest
|
|
||||||
}
|
|
||||||
if (options.dot && result.match(this.RE_LAST_LETTER)) result = `${result}.`
|
|
||||||
return(result)
|
|
||||||
},
|
|
||||||
tokenise: function(text) { return text.replace(this.RE_SPACES, " ").split(" ") },
|
|
||||||
inform: function(html, timeout = 60000) { window.ChatRoomSendLocal(`<div class="mbchc">${html}</div>`, timeout) },
|
|
||||||
report: function(x) {
|
|
||||||
this.inform(`Error: ${x.toString()}`)
|
|
||||||
if (this.RETHROW) throw x
|
|
||||||
},
|
|
||||||
in: function(x, floor, ceiling) { return((x >= floor) && (x <= ceiling)) },
|
|
||||||
cid2char: function(cid) {
|
|
||||||
cid = Number.parseInt(cid)
|
|
||||||
if (cid === window.Player.cid) return(window.Player)
|
|
||||||
return(this.ensure(`character ${cid} not found in the room`, () => window.ChatRoomCharacter.find(c => c.cid === cid)))
|
|
||||||
},
|
|
||||||
pos2char: function(pos) {
|
|
||||||
if (!this.in(pos, 0, window.ChatRoomCharacter.length - 1)) throw `invalid position ${pos}`
|
|
||||||
return(window.ChatRoomCharacter[pos])
|
|
||||||
},
|
|
||||||
rel2char: function(target) {
|
|
||||||
let me = this.ensure("can't find my position", () => window.ChatRoomCharacter.findIndex(char => char.IsPlayer()) + 1) - 1 // 0 is falsy, but is valid index
|
|
||||||
let pos = null
|
|
||||||
if (target.match(this.RE_ALL_LEFT)) pos = me - target.length
|
|
||||||
if (target.match(this.RE_ALL_RIGHT)) pos = me + target.length
|
|
||||||
if (null === pos) throw `failed to parse target "${target}"`
|
|
||||||
pos = pos % window.ChatRoomCharacter.length
|
|
||||||
if (pos < 0) pos = pos + window.ChatRoomCharacter.length
|
|
||||||
return(this.pos2char(pos))
|
|
||||||
},
|
|
||||||
target2char: function(target) { // target should be lowcase
|
|
||||||
let input = target
|
|
||||||
if (this.empty(target)) return(window.Player)
|
|
||||||
let int = Number.parseInt(target)
|
|
||||||
target = String(target)
|
|
||||||
let found = []
|
|
||||||
if (target.startsWith("=")) return(this.cid2char(target.slice(1)))
|
|
||||||
if (target.startsWith("<") || target.startsWith(">")) return(this.rel2char(target))
|
|
||||||
if (!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))
|
|
||||||
found.push(...window.ChatRoomCharacter.filter(c => c.cid.toString().indexOf(target) > -1))
|
|
||||||
}
|
|
||||||
if (target.startsWith("@")) target = target.slice(1)
|
|
||||||
found.push(...window.ChatRoomCharacter.filter(c => c.Name.toLocaleLowerCase().indexOf(target) > -1))
|
|
||||||
found.push(...window.ChatRoomCharacter.filter(c => c.Nickname && (c.Nickname.toLocaleLowerCase().indexOf(target) > -1)))
|
|
||||||
let map = {}
|
|
||||||
found.forEach(c => {if (!map[c.cid]) map[c.cid] = c} )
|
|
||||||
found = Object.values(map)
|
|
||||||
if (found.length < 1) throw `target "${input}": no match`
|
|
||||||
if (found.length > 1) throw `target "${input}": multiple matches (${found.map(c => `${c.cid}|${c.Name}|${c.Nickname || c.Name}`).join(",")})`
|
|
||||||
return(found[0])
|
|
||||||
},
|
|
||||||
char2targets: function(char) {
|
|
||||||
let [result, cid] = [new Set(), char.cid.toString()]
|
|
||||||
result.add(cid).add(`=${cid}`)
|
|
||||||
this.tokenise(char.Name).forEach(t => {result.add(t); result.add(`@${t}`)})
|
|
||||||
if (char.Nickname) this.tokenise(char.Nickname).forEach(t => {result.add(t); result.add(`@${t}`)})
|
|
||||||
return result
|
|
||||||
},
|
|
||||||
donate_data: function(target) {
|
|
||||||
let char = this.target2char(target)
|
|
||||||
if (char.IsPlayer()) throw "target must not be you"
|
|
||||||
if (!char.IsRestrained()) throw "target must be bound"
|
|
||||||
const cost = Math.round((Math.random() * 10 + 15))
|
|
||||||
if (window.Player.Money < cost) throw "not enough money"
|
|
||||||
window.CharacterChangeMoney(window.Player, -cost)
|
|
||||||
window.ServerSend("ChatRoomChat", {Content: "ReceiveSuitcaseMoney", Type: "Hidden", Target: char.cid})
|
|
||||||
window.ChatRoomMessage({Sender: window.Player.cid, Type: "Action", Content: `You've bought data for $${cost} and sent it to ${char.dn}.`, Dictionary: [{Tag: "MISSING PLAYER DIALOG: ", Text: ""}]})
|
|
||||||
},
|
|
||||||
run_activity: function(char, ag, action) { try {
|
|
||||||
if (!window.ActivityAllowed()) throw "activities disabled in this room"
|
|
||||||
if (!window.ServerChatRoomGetAllowItem(window.Player, char)) throw "no permissions"
|
|
||||||
char.FocusGroup = this.ensure("invalid AssetGroup", () => window.AssetGroupGet(char.AssetFamily, ag))
|
|
||||||
let activity = this.ensure("invalid activity", () => window.ActivityAllowedForGroup(char, char.FocusGroup.Name, true).find(a => a.Name === action || a.Activity?.Name === action))
|
|
||||||
if ((activity.Name || activity.Activity.Name).endsWith("Item")) {
|
|
||||||
const item = this.ensure("no toy found", () => window.Player.Inventory.find(i => i.Asset?.Name === "SpankingToys" && i.Asset.Group?.Name === char.FocusGroup.Name && window.AssetSpankingToys.DynamicActivity(char) === (activity.Name || activity.Activity.Name)))
|
|
||||||
window.DialogPublishAction(char, item)
|
|
||||||
} else window.ActivityRun(window.Player, char, char.FocusGroup, activity)
|
|
||||||
} finally {char.FocusGroup = null} },
|
|
||||||
replace_me: function(match, offset, string) {
|
|
||||||
let text = string.slice(1)
|
|
||||||
let suffix = " "
|
|
||||||
if (text.startsWith("'") || text.startsWith(" ")) suffix = ""
|
|
||||||
return `${window.MBCHC.PREF_ACTIVITY}<${window.Player.cid}:>SourceCharacter${suffix}`
|
|
||||||
},
|
|
||||||
cid2dict: function(type, cid) { return({Tag: `${type}Character`, MemberNumber: cid, Text: this.cid2char(cid).dn}) },
|
|
||||||
send_activity: function(msg) {
|
|
||||||
let dict = [{Tag: "MISSING PLAYER DIALOG: ", Text: "\u200C"}] // zero-width non-joiner, used to break up ligatures, does nothing here, but an empty string is a falsy value
|
|
||||||
let cids = msg.match(this.RE_ACT_CIDS)
|
|
||||||
if (cids) {
|
|
||||||
msg = msg.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]))
|
|
||||||
}
|
|
||||||
window.ServerSend("ChatRoomChat", {Type: "Action", Content: msg, Dictionary: dict})
|
|
||||||
},
|
|
||||||
receive: function(data) {
|
|
||||||
let char = this.cid2char(data.Sender)
|
|
||||||
if (char.IsPlayer()) return true // this is our own message, sent back to us
|
|
||||||
let payload = this.ensure("Empty message", () => data.Dictionary[0])
|
|
||||||
switch (payload.type) {
|
|
||||||
case "greetings": case "hello":
|
|
||||||
char.MBCHC = payload.value
|
|
||||||
if ("greetings" === payload.type) this.hello(char)
|
|
||||||
break
|
|
||||||
default: // if we don't know the type it may be from a newer version
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
},
|
|
||||||
hello: function(char = null) {
|
|
||||||
let payload = {type: "greetings", value: window.Player.MBCHC}
|
|
||||||
if (char) payload.type = "hello"
|
|
||||||
let message = {Content: "MBCHC", Type: "Hidden", Dictionary: [payload]}
|
|
||||||
if (char) message.Target = char.cid
|
|
||||||
window.ServerSend("ChatRoomChat", message)
|
|
||||||
},
|
|
||||||
disappear: function() {
|
|
||||||
let item = window.InventoryGet(window.Player, "ItemButt")
|
|
||||||
if (!item || !item.Asset || !item.Asset.Name) throw "butt seems empty"
|
|
||||||
if (item.Asset.Name !== "AnalHook") throw "butt seems occupied by something other than the anal hook"
|
|
||||||
if (!item.Property.Type || item.Property.Type !== "Hair") throw "anal hook seems not tied to hair"
|
|
||||||
item.Property = {Type: "Hair", Hide: this.HIDE_ALL}
|
|
||||||
window.CharacterRefresh(window.Player, true, true)
|
|
||||||
},
|
|
||||||
title: function(title) { // WIP
|
|
||||||
if (this.empty(title)) throw "empty title"
|
|
||||||
title = this.normalise_message(title, {trim: true, up: true, low: true})
|
|
||||||
if (title.length > 16) throw "title too long"
|
|
||||||
if (!title.match(this.RE_TITLE)) throw "invalid title"
|
|
||||||
window.TitleSet(title)
|
|
||||||
//window.TitleList.push({Name: title, Requirement: () => true}) // check for existing first
|
|
||||||
},
|
|
||||||
copy_fbc_trigger: function(trigger) {
|
|
||||||
let 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: function() {
|
|
||||||
this.remove_fbc_hook()
|
|
||||||
delete this.remove_fbc_hook
|
|
||||||
window.bce_ActivityTriggers.push(...window.bce_ActivityTriggers.filter(t => "Emote" === t.Type).map(t => this.copy_fbc_trigger(t)))
|
|
||||||
/* (["anim", "pose"]).forEach(tag => {let cmd = window.Commands.find(c => tag === c.Tag); if (cmd) cmd.AutoComplete = this[`complete_fbc_${tag}`]}) */ // this line explodes, don't ask me why
|
|
||||||
let cmd = window.Commands.find(c => "anim" === c.Tag)
|
|
||||||
if (cmd) cmd.AutoComplete = this.complete_fbc_anim
|
|
||||||
cmd = window.Commands.find(c => "pose" === c.Tag)
|
|
||||||
if (cmd) cmd.AutoComplete = this.complete_fbc_pose
|
|
||||||
},
|
|
||||||
gather_versions: function() { return(window.ChatRoomCharacter.filter(c => c.MBCHC).map(c => ({name: c.dn, cid: c.cid, version: c.MBCHC.VERSION}))) },
|
|
||||||
find_timezone: function(char) {
|
|
||||||
const timezones = this.settings("timezones")
|
|
||||||
if (timezones && "number" === typeof timezones[char.cid]) return(timezones[char.cid])
|
|
||||||
const match = (char.Description) ? char.Description.match(this.RE_TZ) : null
|
|
||||||
const int = match ? Number.parseInt(match[1] + match[2]) : 42
|
|
||||||
if (this.in(int, -12, 12)) return(int)
|
|
||||||
return(null)
|
|
||||||
},
|
|
||||||
player_enters_room: function() { // or if the mod is loaded while player is in the room
|
|
||||||
this.hello()
|
|
||||||
},
|
|
||||||
set_timezone: function(args) {
|
|
||||||
let tz = Number.parseInt(args[0])
|
|
||||||
if (isNaN(tz)) throw `invalid offset "${args[0]}"`
|
|
||||||
if (!this.in(tz, -12, 12)) throw "offset should be [-12,12]"
|
|
||||||
let char = this.target2char(args[1])
|
|
||||||
char.MBCHC_LOCAL.TZ = tz
|
|
||||||
this.save_settings(s => {if (!s.timezones) s.timezones = {}; s.timezones[char.cid] = tz})
|
|
||||||
},
|
|
||||||
update_char: function(char) {
|
|
||||||
char.cid = char.MemberNumber // Club ID (shorter)
|
|
||||||
char.dn = window.CharacterNickname(char) // DisplayName (shortcut)
|
|
||||||
if (!char.MBCHC_LOCAL) char.MBCHC_LOCAL = {}
|
|
||||||
if (!char.MBCHC_LOCAL.TZ) char.MBCHC_LOCAL.TZ = this.find_timezone(char)
|
|
||||||
},
|
|
||||||
command_mbchc: function(argline, cmdline, args) { const mbchc = window.MBCHC; try { // `this` is command object
|
|
||||||
if (args.length < 1) 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("")))
|
|
||||||
let cmd = String(args.shift())
|
|
||||||
let sub = mbchc.ensure(`unknown subcommand "${cmd}"`, () => mbchc.SUBCOMMANDS_MBCHC[cmd])
|
|
||||||
sub.cb.call(mbchc, mbchc, args, argline, cmdline)
|
|
||||||
} catch (x) { mbchc.report(x) } },
|
|
||||||
command_activity: function(argline, cmdline, args) { const mbchc = window.MBCHC; if (!mbchc.empty(argline)) { try { // `this` is command object
|
|
||||||
let message = mbchc.normalise_message(cmdline.replace(mbchc.RE_ACTIVITY, ''), {trim: true, dot: true, up: true})
|
|
||||||
mbchc.send_activity(message)
|
|
||||||
} catch (x) { mbchc.report(x) } } },
|
|
||||||
command_do: function(argline, cmdline, args) { const mbchc = window.MBCHC; try { // `this` is command object
|
|
||||||
if (args.length < 1) return(mbchc.inform("<div>Usage: /do VERB [ZONE] [TARGET]</div><div>Available verbs:</div>" + Object.keys(mbchc.MAP_ACTIONS).join(", ") + "<div>Available zones:</div>" + Object.keys(mbchc.DO_DATA.zones).join(", ")))
|
|
||||||
let [verb, zone, target] = args
|
|
||||||
let zones = mbchc.ensure(`unknown verb "${verb}"`, () => mbchc.DO_DATA.verbs[verb])
|
|
||||||
if (1 === Object.keys(zones).length) {
|
|
||||||
if (!target) target = zone
|
|
||||||
zone = mbchc.MAP_ZONES[Object.keys(zones)[0]][0]
|
|
||||||
}
|
|
||||||
if (!zone) throw "zone missing"
|
|
||||||
let ag = mbchc.ensure(`unknown zone "${zone}"`, () => mbchc.DO_DATA.zones[zone])
|
|
||||||
let types = mbchc.ensure(`zone "${zone}" invalid for "${verb}"`, () => zones[ag])
|
|
||||||
let char = window.Player
|
|
||||||
if (target && ((types.self.length < 1) || (types.others.length > 0))) char = mbchc.target2char(target)
|
|
||||||
let type = char.IsPlayer() ? "self" : "others"
|
|
||||||
let available = window.ActivityAllowedForGroup(char, ag)
|
|
||||||
let toy = window.InventoryGet(window.Player, "ItemHands")
|
|
||||||
if (toy && toy.Asset.Name === "SpankingToys") available.push(window.AssetAllActivities(char.AssetFamily).find(a => a.Name === window.InventorySpankingToysGetActivity?.(window.Player)))
|
|
||||||
let actions = mbchc.ensure(`zone "${zone}" invalid for ("${verb}" "${type}")`, () => types[type])
|
|
||||||
let action = mbchc.ensure(`invalid action (${verb} ${zone} ${target})`, () => actions.find(name => available.find(a => a.Name === name || a.Activity?.Name === name)))
|
|
||||||
mbchc.run_activity(char, ag, action)
|
|
||||||
} catch (x) { mbchc.report(x) } },
|
|
||||||
bell: function() {
|
|
||||||
setTimeout(() => {document.getElementById("InputChat").style.outline = ""}, 100)
|
|
||||||
document.getElementById("InputChat").style.outline = "solid red"
|
|
||||||
},
|
|
||||||
complete: function(options, space = true) {
|
|
||||||
if (options.length < 1) return(this.bell())
|
|
||||||
if (options.length > 1) {
|
|
||||||
let width = Math.max(...options.map(o => o.length))
|
|
||||||
let pref = null
|
|
||||||
for (let i = width; i > 0; i -= 1) { let test = options[0].slice(0, i); if (options.every(o => o.startsWith(test))) {pref = test; break} }
|
|
||||||
if (pref) this.complete([pref], false)
|
|
||||||
this.complete_hint(options)
|
|
||||||
} else window.ElementValue("InputChat", document.getElementById("InputChat").value.replace(this.RE_LAST_WORD, `$1${options[0]}${space ? " " : ""}`))
|
|
||||||
},
|
|
||||||
complete_hint: function(options) {
|
|
||||||
this.COMP_HINT.innerHTML = options.sort().map(s => `<div>${s}</div>`).join("")
|
|
||||||
this.COMP_HINT.style.display = "flex"
|
|
||||||
window.ElementSetDataAttribute(this.COMP_HINT.id, "colortheme", (window.Player.ChatSettings.ColorTheme || "Light"))
|
|
||||||
let rescroll = window.ElementIsScrolledToEnd("TextAreaChatLog")
|
|
||||||
window.ChatRoomResize(false)
|
|
||||||
if (rescroll) window.ElementScrollToEnd("TextAreaChatLog")
|
|
||||||
},
|
|
||||||
comp_hint_visible: function() {return(this.COMP_HINT.parentElement && "flex" === this.COMP_HINT.style.display)},
|
|
||||||
complete_hint_hide: function(options) {if (!this.comp_hint_visible()) return; this.COMP_HINT.style.display = "none"; window.ChatRoomResize(false)},
|
|
||||||
complete_target: function(token, me2 = true, check_perms = false) {
|
|
||||||
let [locase, found] = [token.toLocaleLowerCase(), new Set()]
|
|
||||||
for (let c of window.ChatRoomCharacter) {
|
|
||||||
if ((c.IsPlayer() && !me2) || (check_perms && !window.ServerChatRoomGetAllowItem(window.Player, c))) continue
|
|
||||||
this.char2targets(c).forEach(s => {if (s.toLocaleLowerCase().startsWith(locase)) found.add(s)})
|
|
||||||
}
|
|
||||||
this.complete(Array.from(found))
|
|
||||||
},
|
|
||||||
complete_common: function() {
|
|
||||||
let input = document.getElementById("InputChat").value
|
|
||||||
return([this, input, this.tokenise(input)])
|
|
||||||
},
|
|
||||||
complete_mbchc: function(args, locase, cmdline) { const [mbchc, input, tokens] = window.MBCHC.complete_common(); // `this` is command object
|
|
||||||
if (tokens.length < 1) return
|
|
||||||
if (tokens.length < 2) return(mbchc.complete([`${mbchc.CommandsKey}${this.Tag}`]))
|
|
||||||
let subname = tokens[1].toLocaleLowerCase()
|
|
||||||
if (tokens.length < 3) return(mbchc.complete(Object.keys(mbchc.SUBCOMMANDS_MBCHC).filter(c => c.startsWith(subname)))) // complete subcommand name
|
|
||||||
let sub = mbchc.SUBCOMMANDS_MBCHC[subname]
|
|
||||||
if (sub && sub.args) {
|
|
||||||
let argname = Object.keys(sub.args)[tokens.length - 3]
|
|
||||||
if ("TARGET" === argname) return(mbchc.complete_target(tokens[tokens.length - 1], false))
|
|
||||||
if ("[TARGET]" === argname) return(mbchc.complete_target(tokens[tokens.length - 1]), true)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
complete_do_target: function(actions, token) {
|
|
||||||
if (!actions) return
|
|
||||||
let me2 = (actions.self.length > 0)
|
|
||||||
if (me2 && actions.others.length < 1) return(this.complete([window.Player.cid.toString()])) // target is always the player
|
|
||||||
this.complete_target(token, me2, true)
|
|
||||||
},
|
|
||||||
complete_do: function(args, locase, cmdline) { const [mbchc, input, tokens] = window.MBCHC.complete_common(); // `this` is command object
|
|
||||||
if (tokens.length < 1) 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
|
|
||||||
let 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
|
|
||||||
let 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: function(args, locase, cmdline) { const [mbchc, input, tokens] = window.MBCHC.complete_common(); // `this` is command object
|
|
||||||
if (tokens.length < 1) return
|
|
||||||
if (tokens.length < 2) return(mbchc.complete([`${mbchc.CommandsKey}${this.Tag}`]))
|
|
||||||
if (tokens.length > 2) return(mbchc.bell())
|
|
||||||
let anim = tokens[1].toLocaleLowerCase()
|
|
||||||
return(mbchc.complete(Object.keys(window.bce_EventExpressions).filter(a => a.toLocaleLowerCase().startsWith(anim))))
|
|
||||||
},
|
|
||||||
complete_fbc_pose: function(args, locase, cmdline) { const [mbchc, input, tokens] = window.MBCHC.complete_common(); // `this` is command object
|
|
||||||
if (tokens.length < 1) return
|
|
||||||
if (tokens.length < 2) return(mbchc.complete([`${mbchc.CommandsKey}${this.Tag}`]))
|
|
||||||
let pose = tokens[tokens.length - 1].toLocaleLowerCase()
|
|
||||||
return(mbchc.complete(window.PoseFemale3DCG.map(p => p.Name).filter(p => p.toLocaleLowerCase().startsWith(pose))))
|
|
||||||
},
|
|
||||||
history: function(down) {
|
|
||||||
let [text, history] = [window.ElementValue("InputChat"), window.ChatRoomLastMessage]
|
|
||||||
if (!this.HISTORY_MODE) {history.push(text); this.HISTORY_MODE = true}
|
|
||||||
let ids = history.map((t,i) => [t,i]).filter(([t,i]) => t.startsWith(history[history.length - 1])).map(([t,i]) => i)
|
|
||||||
if (!down) ids.reverse()
|
|
||||||
let found = ids.find(id => (down) ? id > window.ChatRoomLastMessageIndex : id < window.ChatRoomLastMessageIndex)
|
|
||||||
if (!found) return(this.bell())
|
|
||||||
window.ElementValue("InputChat", history[found])
|
|
||||||
window.ChatRoomLastMessageIndex = found
|
|
||||||
},
|
|
||||||
focus_chat_whitelist(event) {
|
|
||||||
if (event.ctrlKey && "v" === event.key) return true // ctrl+V should paste
|
|
||||||
return false
|
|
||||||
},
|
|
||||||
focus_chat(event) { // TODO: this is not ideal, but it will have to do for now
|
|
||||||
if (event.repeat) return // only unique presses please
|
|
||||||
if ("inline" !== document.getElementById("InputChat")?.style.display) return // input chat missing
|
|
||||||
if (["InputChat","bce-message-input"].includes(document.activeElement.id)) return // focus already set
|
|
||||||
if ([event.altKey, event.ctrlKey, event.metaKey].some(i => i) && !this.focus_chat_whitelist(event)) return // alt, ctrl and meta should all be false
|
|
||||||
window.ElementFocus("InputChat")
|
|
||||||
},
|
|
||||||
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.HIDE_ALL = this.HIDE_SPECIAL.concat(this.HIDE_BODY).concat(this.HIDE_CLOTHES).concat(this.HIDE_ITEMS)
|
|
||||||
this.CommandsKey = CommandsKey /* eslint-disable-line no-undef */ // window.CommandsKey is undefined
|
|
||||||
this.RE_ACTIVITY = RegExp(`^${this.CommandsKey}activity `)
|
|
||||||
this.PREF_ACTIVITY = `${this.CommandsKey}activity `
|
|
||||||
this.COMP_HINT = document.createElement("div")
|
|
||||||
this.COMP_HINT.id = "mbchcCompHint"
|
|
||||||
let css = document.createElement("style")
|
|
||||||
css.type = "text/css"
|
|
||||||
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} {
|
|
||||||
flex-flow: column wrap;
|
|
||||||
overflow: auto;
|
|
||||||
display: none;
|
|
||||||
background-color: ${this.RGB_POLLY};
|
|
||||||
color: black;
|
|
||||||
}
|
|
||||||
#${this.COMP_HINT.id}[data-colortheme="dark"], #${this.COMP_HINT.id}[data-colortheme="dark2"] {
|
|
||||||
background-color: ${this.RGB_MUTE};
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
#${this.COMP_HINT.id} div {
|
|
||||||
margin: 0 0.5ex;
|
|
||||||
}
|
|
||||||
`
|
|
||||||
document.head.appendChild(css)
|
|
||||||
// Actions
|
|
||||||
this.calculate_maps()
|
|
||||||
window.Player.MBCHC = {VERSION: this.VERSION}
|
|
||||||
window.CommandCombine(COMMANDS)
|
|
||||||
|
|
||||||
// Hooks
|
// Functions
|
||||||
this.remove_fbc_hook = this.before("MainRun", () => window.bce_ActivityTriggers && this.patch_fbc())
|
window.MBCHC.HIDE_ALL = window.MBCHC.HIDE_SPECIAL.concat(window.MBCHC.HIDE_BODY).concat(window.MBCHC.HIDE_CLOTHES).concat(window.MBCHC.HIDE_ITEMS)
|
||||||
this.after("CharacterOnlineRefresh", char => this.update_char(char))
|
window.MBCHC.make_my_anal_hook_hide_body = function() {
|
||||||
this.after("ChatRoomReceiveSuitcaseMoney", () => {
|
let item = window.InventoryGet(window.Player, "ItemButt")
|
||||||
if (this.AUTOHACK_ENABLED && this.LAST_HACKED) {
|
if (!item || !item.Asset || !item.Asset.Name) throw "butt seems empty"
|
||||||
window.CurrentCharacter = this.cid2char(this.LAST_HACKED)
|
if (item.Asset.Name !== "AnalHook") throw "butt seems occupied by something other than the anal hook"
|
||||||
this.LAST_HACKED = null
|
if (!item.Property.Type || item.Property.Type !== "Hair") throw "anal hook seems not tied to hair"
|
||||||
window.ChatRoomTryToTakeSuitcase()
|
item.Property = {Type: "Hair", Hide: this.HIDE_ALL}
|
||||||
}
|
window.CharacterRefresh(window.Player, true, true)
|
||||||
})
|
}
|
||||||
this.before("ChatRoomSendChat", () => {
|
window.MBCHC.donate_data = function(id) {
|
||||||
let input = window.ElementValue("InputChat")
|
if (id == window.Player.MemberNumber) throw "recipient must not be you"
|
||||||
if (!input.startsWith("@@@") && input.startsWith("@")) {
|
const char = window.ChatRoomCharacter.find( c => c.MemberNumber == id )
|
||||||
input = input.replace(this.RE_PREF_ACTIVITY, this.PREF_ACTIVITY)
|
if (!char) throw "recipient not found"
|
||||||
input = input.replace(this.RE_PREF_ACTIVITY_ME, this.replace_me)
|
if (!char.IsRestrained()) throw "recipient must be bound"
|
||||||
window.ElementValue("InputChat", input)
|
const cost = (Math.random() * 10 + 15).toFixed(0)
|
||||||
}
|
if (window.Player.Money < cost) throw "not enough money"
|
||||||
})
|
window.CharacterChangeMoney(window.Player, -cost)
|
||||||
this.after("ChatRoomSendChat", () => {
|
window.ServerSend("ChatRoomChat", {Content: "ReceiveSuitcaseMoney", Type: "Hidden", Target: id})
|
||||||
const history = window.ChatRoomLastMessage
|
return({cost: cost, name: (char.Nickname || char.Name)})
|
||||||
if ((history.length > 1) && (history[history.length - 1] === history[history.length - 2])) {history.pop(); window.ChatRoomLastMessageIndex -= 1}
|
}
|
||||||
})
|
window.MBCHC.run_activity = function(char, ag, action) {
|
||||||
this.before("ChatRoomDrawCharacterOverlay", (C, CharX, CharY, Zoom, Pos) => {
|
try {
|
||||||
window.ChatRoomHideIconState < 1 && C.MBCHC && window.DrawRect(CharX + 175 * Zoom, CharY, 50 * Zoom, 50 * Zoom, C.MBCHC.VERSION === window.Player.MBCHC.VERSION ? this.RGB_POLLY : this.RGB_MUTE)
|
char.FocusGroup = window.AssetGroupGet(char.AssetFamily, ag)
|
||||||
if (window.ChatRoomHideIconState < 1 && C.MBCHC_LOCAL && "number" === typeof C.MBCHC_LOCAL.TZ) {
|
if (!char.FocusGroup) throw "invalid AssetGroup"
|
||||||
const hours = new Date(window.CommonTime() + this.UTC_OFFSET + C.MBCHC_LOCAL.TZ * 60 * 60 * 1000).getHours()
|
let activity = window.ActivityAllowedForGroup(char, char.FocusGroup.Name).find( a => a.Name === action)
|
||||||
window.DrawTextFit(hours < 10 ? "0" + hours.toString() : hours.toString(), CharX + 200 * Zoom, CharY + 25 * Zoom, 46 * Zoom, "white", "black")
|
if (!activity) throw "invalid activity"
|
||||||
}
|
window.ActivityRun(char, activity)
|
||||||
})
|
} finally {
|
||||||
this.after("ElementValue", (ID, Value, cc = window.CurrentCharacter) => "bce_LayerPriority" === ID && cc?.FocusGroup && window.InventoryGet(cc, cc.FocusGroup.Name)?.Difficulty.toString() === Value && window.InventoryLocked(cc, cc.FocusGroup.Name, true) && window.ElementSetAttribute(ID, "disabled", true))
|
char.FocusGroup = null
|
||||||
this.after("ChatRoomCreateElement", () => this.COMP_HINT.parentElement || document.body.appendChild(this.COMP_HINT))
|
}
|
||||||
this.before("ChatRoomClearAllElements", () => !void this.complete_hint_hide() && this.COMP_HINT.remove())
|
}
|
||||||
this.before("ChatRoomClick", () => this.complete_hint_hide())
|
window.MBCHC.send_activity = function(msg) {
|
||||||
this.after("ChatRoomResize", () => {
|
window.ServerSend("ChatRoomChat", {Type: "Action", Content: "lef_wie234jf_owhwi", Dictionary: [{Tag: 'MISSING PLAYER DIALOG: lef_wie234jf_owhwi', Text: msg}]})
|
||||||
if (window.CharacterGetCurrent() == null && window.CurrentScreen == "ChatRoom" && document.getElementById("InputChat") && document.getElementById("TextAreaChatLog") && this.comp_hint_visible()) { // upstream
|
}
|
||||||
const fontsize = ChatRoomFontSize /* eslint-disable-line no-undef */ // window.ChatRoomFontSize is undefined
|
// Command actions
|
||||||
window.ElementPositionFix("TextAreaChatLog", fontsize, 1005, 66, 988, 630)
|
window.MBCHC.action_title = function(args) {
|
||||||
window.ElementPositionFix(this.COMP_HINT.id, fontsize, 1005, 701, 988, 200)
|
let title = args.shift()
|
||||||
this.COMP_HINT.style.display = "flex"
|
if (!title || !title.length || title.length < 1) throw "empty title"
|
||||||
}
|
if (title.length > 16) throw "title too long"
|
||||||
})
|
if (!title.match(window.MBCHC.RE_TITLE)) throw "invalid title"
|
||||||
document.addEventListener("keydown", event => this.focus_chat(event))
|
let first = title.at(0).toLocaleUpperCase()
|
||||||
this.SDK.hookFunction("ChatRoomKeyDown", 0, (nextargs, next) => { // this fires on chat input events
|
let rest = title.slice(1).toLocaleLowerCase()
|
||||||
let [event] = nextargs
|
title = first + rest
|
||||||
window.MBCHC.complete_hint_hide()
|
window.TitleSet(title)
|
||||||
if ((window.KeyPress == 33) || (window.KeyPress == 34)) { // better history
|
// TODO: this needs much more work. at least don't push a second title
|
||||||
event.preventDefault()
|
// we need to patch the text cache
|
||||||
return(window.MBCHC.history(window.KeyPress - 33))
|
// we need to check for other players' custom titles
|
||||||
}
|
window.TitleList.push({Name: title, Requirement: () => {return true}})
|
||||||
if (window.MBCHC.HISTORY_MODE) {
|
}
|
||||||
window.ChatRoomLastMessage.pop()
|
window.MBCHC.action_donate = function(args) {
|
||||||
window.MBCHC.HISTORY_MODE = false
|
let id = Number.parseInt(args.shift())
|
||||||
}
|
if (isNaN(id)) throw "empty or invalid member number"
|
||||||
return(next(nextargs))
|
let result = window.MBCHC.donate_data(id)
|
||||||
})
|
window.ChatRoomSendLocal(`(You've bought data for $${result.cost} and sent it to ${result.name})`)
|
||||||
|
}
|
||||||
|
window.MBCHC.action_autohack = function() {
|
||||||
|
window.MBCHC.AUTOHACK_ENABLED = !window.MBCHC.AUTOHACK_ENABLED
|
||||||
|
window.ChatRoomSendLocal(`(Autohack is now ${window.MBCHC.AUTOHACK_ENABLED ? "enabled" : "disabled"})`)
|
||||||
|
}
|
||||||
|
|
||||||
// Chat room handlers
|
// Hooks
|
||||||
window.ChatRoomRegisterMessageHandler({ Priority: -220, Description: "MBCHC preprocessor", Callback: (data, sender, msg, metadata) => {
|
window.ChatRoomMessageInvolvesPlayer = function(data) {
|
||||||
data.MBCHC_ID = this.NEXT_MESSAGE
|
if (!data.MBCHC_ID) {
|
||||||
this.NEXT_MESSAGE += 1
|
data.MBCHC_ID = window.MBCHC.NEXT_MESSAGE
|
||||||
if (this.LOG_MESSAGES) console.debug({data, sender, msg, metadata})
|
window.MBCHC.NEXT_MESSAGE += 1
|
||||||
}})
|
if (window.MBCHC.LOG_MESSAGES) console.debug(data)
|
||||||
window.ChatRoomRegisterMessageHandler({ Priority: -219, Description: "MBCHC room enter hook",
|
if (("ReceiveSuitcaseMoney" === data.Content) && ("Hidden" === data.Type)) { window.MBCHC.LAST_HACKED = data.Sender }
|
||||||
Callback: (data, sender, msg, metadata) => { if (("Action" === data.Type) && ("ServerEnter" === data.Content) && (data.Sender === window.Player.cid)) this.player_enters_room() }
|
}
|
||||||
})
|
return(window.MBCHC.orig_ChatRoomMessageInvolvesPlayer(data))
|
||||||
window.ChatRoomRegisterMessageHandler({ Priority: -219, Description: "MBCHC specific consumer",
|
}
|
||||||
Callback: (data, sender, msg, metadata) => { if (("Hidden" === data.Type) && ("MBCHC" === data.Content)) return this.receive(data) }
|
window.ChatRoomReceiveSuitcaseMoney = function() {
|
||||||
})
|
let result = window.MBCHC.orig_ChatRoomReceiveSuitcaseMoney()
|
||||||
window.ChatRoomRegisterMessageHandler({ Priority: -219, Description: "MBCHC autohack lookup",
|
if (window.MBCHC.AUTOHACK_ENABLED && window.MBCHC.LAST_HACKED) {
|
||||||
Callback: (data, sender, msg, metadata) => { if (("Hidden" === data.Type) && ("ReceiveSuitcaseMoney" === data.Content)) this.LAST_HACKED = data.Sender }
|
window.CurrentCharacter = {MemberNumber: window.MBCHC.LAST_HACKED}
|
||||||
})
|
window.MBCHC.LAST_HACKED = null
|
||||||
|
window.ChatRoomTryToTakeSuitcase()
|
||||||
|
}
|
||||||
|
return(result)
|
||||||
|
}
|
||||||
|
window.ChatRoomSendChat = function() {
|
||||||
|
let input = window.ElementValue("InputChat")
|
||||||
|
if (!input.startsWith("@@@")) {
|
||||||
|
input = input.replace(/^@@/, "/activity ")
|
||||||
|
input = input.replace(/^@/, "/myself ")
|
||||||
|
window.ElementValue("InputChat", input)
|
||||||
|
}
|
||||||
|
return(window.MBCHC.orig_ChatRoomSendChat())
|
||||||
|
}
|
||||||
|
|
||||||
// footer
|
// Actions
|
||||||
this.LOADED = true
|
window.CommandCombine(window.MBCHC.COMMANDS)
|
||||||
this.log("info", `loaded version ${this.VERSION}`)
|
if (window.bcModSdk) window.bcModSdk.registerMod("MBCHC", window.MBCHC.VERSION)
|
||||||
if (window.GameVersion !== this.TARGET_VERSION) this.log("warn", `Game version doesn't match the target ("${this.TARGET_VERSION}"), beware of incompatibilities`) // TODO: betas are like R86Beta1; cheat?
|
window.MBCHC.LOADED = true
|
||||||
if (("Online" === window.CurrentModule) && ("ChatRoom" === window.CurrentScreen)) {
|
console.info(window.MBCHC.log("loaded version " + window.MBCHC.VERSION))
|
||||||
window.ChatRoomCharacter.forEach(c => this.update_char(c))
|
}
|
||||||
this.player_enters_room()
|
return(window.MBCHC.orig_AsylumGGTSSAddItems())
|
||||||
}
|
} // Loader
|
||||||
},
|
|
||||||
preloader() {
|
|
||||||
this.SDK = window.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 (x) {console.error(x)} finally {return next(nextargs)}})
|
|
||||||
this.after = (name, cb) => this.SDK.hookFunction(name, 0, (nextargs,next) => {const result = next(nextargs); try {cb?.(...nextargs)} catch (x) {console.error(x)} finally {return result}})
|
|
||||||
if (window.CurrentModule && window.CurrentScreen && !("Character" === window.CurrentModule && "Login" === window.CurrentScreen)) return this.loader()
|
|
||||||
this.remove_load_hook = this.before("AsylumGGTSSAddItems", () => this.loader())
|
|
||||||
}
|
|
||||||
} // MBCHC
|
|
||||||
|
|
||||||
fetch("https://code.fleshless.org/mute/MBCHC/raw/branch/master/bondage-club-mod-sdk-1.1.0.js").then(r=>r.text()).then(r=>eval(r)).then(_=>window.MBCHC.preloader()).catch(x=>console.error(x)) /* eslint-disable-line no-eval */
|
|
||||||
})()
|
})()
|
||||||
|
|
|
@ -1,667 +0,0 @@
|
||||||
// ==UserScript==
|
|
||||||
// @name MBCHC-local
|
|
||||||
// @version trunk
|
|
||||||
// @description Mute's Bondage Club Hacks Collection (development version)
|
|
||||||
// @author codename.mute@proton.me
|
|
||||||
// @namespace https://code.fleshless.org/mute/
|
|
||||||
// @homepage https://code.fleshless.org/mute/MBCHC
|
|
||||||
// @updateURL https://code.fleshless.org/mute/MBCHC/raw/branch/master/mbchc-local.user.js
|
|
||||||
// @downloadURL https://code.fleshless.org/mute/MBCHC/raw/branch/master/mbchc-local.user.js
|
|
||||||
// @match https://bondageprojects.elementfx.com/R*
|
|
||||||
// @match https://www.bondageprojects.elementfx.com/R*
|
|
||||||
// @match https://bondage-europe.com/R*
|
|
||||||
// @match https://www.bondage-europe.com/R*
|
|
||||||
// @match http://localhost:*/*
|
|
||||||
// @match http://127.0.0.1:*/*
|
|
||||||
// @grant none
|
|
||||||
// ==/UserScript==
|
|
||||||
|
|
||||||
(function() {
|
|
||||||
"use strict";
|
|
||||||
if (!window.AsylumGGTSSAddItems) throw "AsylumGGTSSAddItems() not found, aborting MBCHC loading"
|
|
||||||
if (window.MBCHC) throw "MBCHC found, aborting loading"
|
|
||||||
window.MBCHC = {
|
|
||||||
VERSION: "trunk",
|
|
||||||
TARGET_VERSION: "R86",
|
|
||||||
NEXT_MESSAGE: 1,
|
|
||||||
LOG_MESSAGES: false,
|
|
||||||
RETHROW: false,
|
|
||||||
LOADED: false,
|
|
||||||
AUTOHACK_ENABLED: false,
|
|
||||||
LAST_HACKED: null,
|
|
||||||
HISTORY_MODE: false,
|
|
||||||
RE_TITLE: /^[a-zA-Z]+$/,
|
|
||||||
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,
|
|
||||||
HIDE_SPECIAL: ["Activity","Emoticon"],
|
|
||||||
HIDE_BODY: ["Blush","BodyLower","BodyUpper","Eyebrows","Eyes","Eyes2","Face","Fluids","HairBack","HairFront","Hands","Head","LeftHand","Mouth","Nipples","Pussy","RightHand"],
|
|
||||||
HIDE_CLOTHES: [
|
|
||||||
"Cloth","ClothAccessory","Necklace","Suit","ClothLower","SuitLower","Bra","Corset","Panties",
|
|
||||||
"Socks","RightAnklet","LeftAnklet","Garters","Shoes","Hat","HairAccessory3","HairAccessory1","HairAccessory2",
|
|
||||||
"Gloves","Bracelet","Glasses","Mask","TailStraps","Wings"
|
|
||||||
],
|
|
||||||
HIDE_ITEMS: [
|
|
||||||
"ItemMisc","ItemEars","ItemHead","ItemNose","ItemHood","ItemAddon","ItemMouth","ItemMouth2","ItemMouth3",
|
|
||||||
"ItemArms","ItemNeckAccessories","ItemNeck","ItemNeckRestraints","ItemNipples","ItemNipplesPiercings","ItemBreast","ItemTorso","ItemTorso2",
|
|
||||||
"ItemHands","ItemPelvis","ItemVulva","ItemVulvaPiercings",
|
|
||||||
"ItemDevices","ItemLegs","ItemFeet","ItemBoots"
|
|
||||||
],
|
|
||||||
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: {
|
|
||||||
"versions": {desc: "show the mod versions across the room", cb: mbchc => mbchc.inform(mbchc.gather_versions().map(c => `<div><b>${c.name}</b> (${c.cid}): ${c.version}</div>`).join(""))},
|
|
||||||
"autohack": {desc: "toggle the autohack feature", cb: mbchc => mbchc.inform(`Autohack is now ${(mbchc.AUTOHACK_ENABLED = !mbchc.AUTOHACK_ENABLED) ? "enabled" : "disabled"}`)},
|
|
||||||
"disappear": {desc: "become invisible (requires anal hook -> hair)", cb: mbchc => mbchc.disappear()},
|
|
||||||
"donate": {desc: "Buy data and send it to recipient", args: {TARGET: {}}, cb: (mbchc, args) => mbchc.donate_data(args[0])},
|
|
||||||
"title": {desc: "set a custom title (<b>WIP</b>)", args: {TITLE: {}}, cb: (mbchc, args) => mbchc.title(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 => {if (window.Player.OnlineSettings.MBCHC) {delete window.Player.OnlineSettings.MBCHC; mbchc.save_settings()}}},
|
|
||||||
},
|
|
||||||
ensure: function(error, callback) {
|
|
||||||
let result = callback.call(this)
|
|
||||||
if (!result) throw error
|
|
||||||
return(result)
|
|
||||||
},
|
|
||||||
calculate_maps: function() {
|
|
||||||
this.DO_DATA = {verbs: {}, zones: {}}
|
|
||||||
for (let [verbs, data] of Object.entries(this.MAP_ACTIONS)) {
|
|
||||||
let unwound = {}
|
|
||||||
for (let [zones, actions] of Object.entries(data)) {
|
|
||||||
let all = (actions.all) ? actions.all.split("|") : []
|
|
||||||
let processed = {self: (actions.self) ? actions.self.split("|").concat(all) : all, others: (actions.others) ? actions.others.split("|").concat(all) : all}
|
|
||||||
for (let zone of zones.split(",")) unwound[`Item${zone}`] = processed
|
|
||||||
}
|
|
||||||
for (let verb of verbs.split("|")) this.DO_DATA.verbs[verb] = unwound
|
|
||||||
}
|
|
||||||
for (let [ag, zones] of Object.entries(this.MAP_ZONES)) for (let zone of zones) this.DO_DATA.zones[zone] = ag
|
|
||||||
},
|
|
||||||
settings: function(setting = null) {
|
|
||||||
let settings = window.Player.OnlineSettings.MBCHC || {}
|
|
||||||
return(setting ? settings[setting] : settings)
|
|
||||||
},
|
|
||||||
save_settings: function(cb = null) {
|
|
||||||
if (cb) {
|
|
||||||
if (!window.Player.OnlineSettings.MBCHC) window.Player.OnlineSettings.MBCHC = {}
|
|
||||||
cb.call(this, window.Player.OnlineSettings.MBCHC)
|
|
||||||
}
|
|
||||||
window.ServerAccountUpdate.QueueData({OnlineSettings: window.Player.OnlineSettings})
|
|
||||||
},
|
|
||||||
log: function(level, msg) {console[level]("MBCHC: " + String(msg))},
|
|
||||||
empty: function(text) {
|
|
||||||
if (!text) return(true)
|
|
||||||
if (String(text).trim().length < 1) return(true)
|
|
||||||
return(false)
|
|
||||||
},
|
|
||||||
normalise_message: function(text, options = {}) {
|
|
||||||
let result = text
|
|
||||||
if (options.trim) result = result.trim()
|
|
||||||
if (options.low) result = result.toLocaleLowerCase()
|
|
||||||
if (options.up) {
|
|
||||||
let first = result.at(0).toLocaleUpperCase()
|
|
||||||
let rest = result.slice(1)
|
|
||||||
result = first + rest
|
|
||||||
}
|
|
||||||
if (options.dot && result.match(this.RE_LAST_LETTER)) result = `${result}.`
|
|
||||||
return(result)
|
|
||||||
},
|
|
||||||
tokenise: function(text) { return text.replace(this.RE_SPACES, " ").split(" ") },
|
|
||||||
inform: function(html, timeout = 60000) { window.ChatRoomSendLocal(`<div class="mbchc">${html}</div>`, timeout) },
|
|
||||||
report: function(x) {
|
|
||||||
this.inform(`Error: ${x.toString()}`)
|
|
||||||
if (this.RETHROW) throw x
|
|
||||||
},
|
|
||||||
in: function(x, floor, ceiling) { return((x >= floor) && (x <= ceiling)) },
|
|
||||||
cid2char: function(cid) {
|
|
||||||
cid = Number.parseInt(cid)
|
|
||||||
if (cid === window.Player.cid) return(window.Player)
|
|
||||||
return(this.ensure(`character ${cid} not found in the room`, () => window.ChatRoomCharacter.find(c => c.cid === cid)))
|
|
||||||
},
|
|
||||||
pos2char: function(pos) {
|
|
||||||
if (!this.in(pos, 0, window.ChatRoomCharacter.length - 1)) throw `invalid position ${pos}`
|
|
||||||
return(window.ChatRoomCharacter[pos])
|
|
||||||
},
|
|
||||||
rel2char: function(target) {
|
|
||||||
let me = this.ensure("can't find my position", () => window.ChatRoomCharacter.findIndex(char => char.IsPlayer()) + 1) - 1 // 0 is falsy, but is valid index
|
|
||||||
let pos = null
|
|
||||||
if (target.match(this.RE_ALL_LEFT)) pos = me - target.length
|
|
||||||
if (target.match(this.RE_ALL_RIGHT)) pos = me + target.length
|
|
||||||
if (null === pos) throw `failed to parse target "${target}"`
|
|
||||||
pos = pos % window.ChatRoomCharacter.length
|
|
||||||
if (pos < 0) pos = pos + window.ChatRoomCharacter.length
|
|
||||||
return(this.pos2char(pos))
|
|
||||||
},
|
|
||||||
target2char: function(target) { // target should be lowcase
|
|
||||||
let input = target
|
|
||||||
if (this.empty(target)) return(window.Player)
|
|
||||||
let int = Number.parseInt(target)
|
|
||||||
target = String(target)
|
|
||||||
let found = []
|
|
||||||
if (target.startsWith("=")) return(this.cid2char(target.slice(1)))
|
|
||||||
if (target.startsWith("<") || target.startsWith(">")) return(this.rel2char(target))
|
|
||||||
if (!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))
|
|
||||||
found.push(...window.ChatRoomCharacter.filter(c => c.cid.toString().indexOf(target) > -1))
|
|
||||||
}
|
|
||||||
if (target.startsWith("@")) target = target.slice(1)
|
|
||||||
found.push(...window.ChatRoomCharacter.filter(c => c.Name.toLocaleLowerCase().indexOf(target) > -1))
|
|
||||||
found.push(...window.ChatRoomCharacter.filter(c => c.Nickname && (c.Nickname.toLocaleLowerCase().indexOf(target) > -1)))
|
|
||||||
let map = {}
|
|
||||||
found.forEach(c => {if (!map[c.cid]) map[c.cid] = c} )
|
|
||||||
found = Object.values(map)
|
|
||||||
if (found.length < 1) throw `target "${input}": no match`
|
|
||||||
if (found.length > 1) throw `target "${input}": multiple matches (${found.map(c => `${c.cid}|${c.Name}|${c.Nickname || c.Name}`).join(",")})`
|
|
||||||
return(found[0])
|
|
||||||
},
|
|
||||||
char2targets: function(char) {
|
|
||||||
let [result, cid] = [new Set(), char.cid.toString()]
|
|
||||||
result.add(cid).add(`=${cid}`)
|
|
||||||
this.tokenise(char.Name).forEach(t => {result.add(t); result.add(`@${t}`)})
|
|
||||||
if (char.Nickname) this.tokenise(char.Nickname).forEach(t => {result.add(t); result.add(`@${t}`)})
|
|
||||||
return result
|
|
||||||
},
|
|
||||||
donate_data: function(target) {
|
|
||||||
let char = this.target2char(target)
|
|
||||||
if (char.IsPlayer()) throw "target must not be you"
|
|
||||||
if (!char.IsRestrained()) throw "target must be bound"
|
|
||||||
const cost = Math.round((Math.random() * 10 + 15))
|
|
||||||
if (window.Player.Money < cost) throw "not enough money"
|
|
||||||
window.CharacterChangeMoney(window.Player, -cost)
|
|
||||||
window.ServerSend("ChatRoomChat", {Content: "ReceiveSuitcaseMoney", Type: "Hidden", Target: char.cid})
|
|
||||||
window.ChatRoomMessage({Sender: window.Player.cid, Type: "Action", Content: `You've bought data for $${cost} and sent it to ${char.dn}.`, Dictionary: [{Tag: "MISSING PLAYER DIALOG: ", Text: ""}]})
|
|
||||||
},
|
|
||||||
run_activity: function(char, ag, action) { try {
|
|
||||||
if (!window.ActivityAllowed()) throw "activities disabled in this room"
|
|
||||||
if (!window.ServerChatRoomGetAllowItem(window.Player, char)) throw "no permissions"
|
|
||||||
char.FocusGroup = this.ensure("invalid AssetGroup", () => window.AssetGroupGet(char.AssetFamily, ag))
|
|
||||||
let activity = this.ensure("invalid activity", () => window.ActivityAllowedForGroup(char, char.FocusGroup.Name, true).find(a => a.Name === action || a.Activity?.Name === action))
|
|
||||||
if ((activity.Name || activity.Activity.Name).endsWith("Item")) {
|
|
||||||
const item = this.ensure("no toy found", () => window.Player.Inventory.find(i => i.Asset?.Name === "SpankingToys" && i.Asset.Group?.Name === char.FocusGroup.Name && window.AssetSpankingToys.DynamicActivity(char) === (activity.Name || activity.Activity.Name)))
|
|
||||||
window.DialogPublishAction(char, item)
|
|
||||||
} else window.ActivityRun(char, activity)
|
|
||||||
} finally {char.FocusGroup = null} },
|
|
||||||
replace_me: function(match, offset, string) {
|
|
||||||
let text = string.slice(1)
|
|
||||||
let suffix = " "
|
|
||||||
if (text.startsWith("'") || text.startsWith(" ")) suffix = ""
|
|
||||||
return `${window.MBCHC.PREF_ACTIVITY}<${window.Player.cid}:>SourceCharacter${suffix}`
|
|
||||||
},
|
|
||||||
cid2dict: function(type, cid) { return({Tag: `${type}Character`, MemberNumber: cid, Text: this.cid2char(cid).dn}) },
|
|
||||||
send_activity: function(msg) {
|
|
||||||
let dict = [{Tag: "MISSING PLAYER DIALOG: ", Text: "\u200C"}] // zero-width non-joiner, used to break up ligatures, does nothing here, but an empty string is a falsy value
|
|
||||||
let cids = msg.match(this.RE_ACT_CIDS)
|
|
||||||
if (cids) {
|
|
||||||
msg = msg.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]))
|
|
||||||
}
|
|
||||||
window.ServerSend("ChatRoomChat", {Type: "Action", Content: msg, Dictionary: dict})
|
|
||||||
},
|
|
||||||
receive: function(data) {
|
|
||||||
let char = this.cid2char(data.Sender)
|
|
||||||
if (char.IsPlayer()) return true // this is our own message, sent back to us
|
|
||||||
let payload = this.ensure("Empty message", () => data.Dictionary[0])
|
|
||||||
switch (payload.type) {
|
|
||||||
case "greetings": case "hello":
|
|
||||||
char.MBCHC = payload.value
|
|
||||||
if ("greetings" === payload.type) this.hello(char)
|
|
||||||
break
|
|
||||||
default: // if we don't know the type it may be from a newer version
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
},
|
|
||||||
hello: function(char = null) {
|
|
||||||
let payload = {type: "greetings", value: window.Player.MBCHC}
|
|
||||||
if (char) payload.type = "hello"
|
|
||||||
let message = {Content: "MBCHC", Type: "Hidden", Dictionary: [payload]}
|
|
||||||
if (char) message.Target = char.cid
|
|
||||||
window.ServerSend("ChatRoomChat", message)
|
|
||||||
},
|
|
||||||
disappear: function() {
|
|
||||||
let item = window.InventoryGet(window.Player, "ItemButt")
|
|
||||||
if (!item || !item.Asset || !item.Asset.Name) throw "butt seems empty"
|
|
||||||
if (item.Asset.Name !== "AnalHook") throw "butt seems occupied by something other than the anal hook"
|
|
||||||
if (!item.Property.Type || item.Property.Type !== "Hair") throw "anal hook seems not tied to hair"
|
|
||||||
item.Property = {Type: "Hair", Hide: this.HIDE_ALL}
|
|
||||||
window.CharacterRefresh(window.Player, true, true)
|
|
||||||
},
|
|
||||||
title: function(title) { // WIP
|
|
||||||
if (this.empty(title)) throw "empty title"
|
|
||||||
title = this.normalise_message(title, {trim: true, up: true, low: true})
|
|
||||||
if (title.length > 16) throw "title too long"
|
|
||||||
if (!title.match(this.RE_TITLE)) throw "invalid title"
|
|
||||||
window.TitleSet(title)
|
|
||||||
//window.TitleList.push({Name: title, Requirement: () => true}) // check for existing first
|
|
||||||
},
|
|
||||||
copy_fbc_trigger: function(trigger) {
|
|
||||||
let 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: function() {
|
|
||||||
this.remove_fbc_hook()
|
|
||||||
delete this.remove_fbc_hook
|
|
||||||
window.bce_ActivityTriggers.push(...window.bce_ActivityTriggers.filter(t => "Emote" === t.Type).map(t => this.copy_fbc_trigger(t)))
|
|
||||||
/* (["anim", "pose"]).forEach(tag => {let cmd = window.Commands.find(c => tag === c.Tag); if (cmd) cmd.AutoComplete = this[`complete_fbc_${tag}`]}) */ // this line explodes, don't ask me why
|
|
||||||
let cmd = window.Commands.find(c => "anim" === c.Tag)
|
|
||||||
if (cmd) cmd.AutoComplete = this.complete_fbc_anim
|
|
||||||
cmd = window.Commands.find(c => "pose" === c.Tag)
|
|
||||||
if (cmd) cmd.AutoComplete = this.complete_fbc_pose
|
|
||||||
},
|
|
||||||
gather_versions: function() { return(window.ChatRoomCharacter.filter(c => c.MBCHC).map(c => ({name: c.dn, cid: c.cid, version: c.MBCHC.VERSION}))) },
|
|
||||||
find_timezone: function(char) {
|
|
||||||
const timezones = this.settings("timezones")
|
|
||||||
if (timezones && "number" === typeof timezones[char.cid]) return(timezones[char.cid])
|
|
||||||
const match = (char.Description) ? char.Description.match(this.RE_TZ) : null
|
|
||||||
const int = match ? Number.parseInt(match[1] + match[2]) : 42
|
|
||||||
if (this.in(int, -12, 12)) return(int)
|
|
||||||
return(null)
|
|
||||||
},
|
|
||||||
player_enters_room: function() { // or if the mod is loaded while player is in the room
|
|
||||||
this.hello()
|
|
||||||
},
|
|
||||||
set_timezone: function(args) {
|
|
||||||
let tz = Number.parseInt(args[0])
|
|
||||||
if (isNaN(tz)) throw `invalid offset "${args[0]}"`
|
|
||||||
if (!this.in(tz, -12, 12)) throw "offset should be [-12,12]"
|
|
||||||
let char = this.target2char(args[1])
|
|
||||||
char.MBCHC_LOCAL.TZ = tz
|
|
||||||
this.save_settings(s => {if (!s.timezones) s.timezones = {}; s.timezones[char.cid] = tz})
|
|
||||||
},
|
|
||||||
update_char: function(char) {
|
|
||||||
char.cid = char.MemberNumber // Club ID (shorter)
|
|
||||||
char.dn = window.CharacterNickname(char) // DisplayName (shortcut)
|
|
||||||
if (!char.MBCHC_LOCAL) char.MBCHC_LOCAL = {}
|
|
||||||
if (!char.MBCHC_LOCAL.TZ) char.MBCHC_LOCAL.TZ = this.find_timezone(char)
|
|
||||||
},
|
|
||||||
command_mbchc: function(argline, cmdline, args) { const mbchc = window.MBCHC; try { // `this` is command object
|
|
||||||
if (args.length < 1) 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("")))
|
|
||||||
let cmd = String(args.shift())
|
|
||||||
let sub = mbchc.ensure(`unknown subcommand "${cmd}"`, () => mbchc.SUBCOMMANDS_MBCHC[cmd])
|
|
||||||
sub.cb.call(mbchc, mbchc, args, argline, cmdline)
|
|
||||||
} catch (x) { mbchc.report(x) } },
|
|
||||||
command_activity: function(argline, cmdline, args) { const mbchc = window.MBCHC; if (!mbchc.empty(argline)) { try { // `this` is command object
|
|
||||||
let message = mbchc.normalise_message(cmdline.replace(mbchc.RE_ACTIVITY, ''), {trim: true, dot: true, up: true})
|
|
||||||
mbchc.send_activity(message)
|
|
||||||
} catch (x) { mbchc.report(x) } } },
|
|
||||||
command_do: function(argline, cmdline, args) { const mbchc = window.MBCHC; try { // `this` is command object
|
|
||||||
if (args.length < 1) return(mbchc.inform("<div>Usage: /do VERB [ZONE] [TARGET]</div><div>Available verbs:</div>" + Object.keys(mbchc.MAP_ACTIONS).join(", ") + "<div>Available zones:</div>" + Object.keys(mbchc.DO_DATA.zones).join(", ")))
|
|
||||||
let [verb, zone, target] = args
|
|
||||||
let zones = mbchc.ensure(`unknown verb "${verb}"`, () => mbchc.DO_DATA.verbs[verb])
|
|
||||||
if (1 === Object.keys(zones).length) {
|
|
||||||
if (!target) target = zone
|
|
||||||
zone = mbchc.MAP_ZONES[Object.keys(zones)[0]][0]
|
|
||||||
}
|
|
||||||
if (!zone) throw "zone missing"
|
|
||||||
let ag = mbchc.ensure(`unknown zone "${zone}"`, () => mbchc.DO_DATA.zones[zone])
|
|
||||||
let types = mbchc.ensure(`zone "${zone}" invalid for "${verb}"`, () => zones[ag])
|
|
||||||
let char = window.Player
|
|
||||||
if (target && ((types.self.length < 1) || (types.others.length > 0))) char = mbchc.target2char(target)
|
|
||||||
let type = char.IsPlayer() ? "self" : "others"
|
|
||||||
let available = window.ActivityAllowedForGroup(char, ag)
|
|
||||||
let toy = window.InventoryGet(window.Player, "ItemHands")
|
|
||||||
if (toy && toy.Asset.Name === "SpankingToys") available.push(window.AssetAllActivities(char.AssetFamily).find(a => a.Name === window.InventorySpankingToysGetActivity?.(window.Player)))
|
|
||||||
let actions = mbchc.ensure(`zone "${zone}" invalid for ("${verb}" "${type}")`, () => types[type])
|
|
||||||
let action = mbchc.ensure(`invalid action (${verb} ${zone} ${target})`, () => actions.find(name => available.find(a => a.Name === name || a.Activity?.Name === name)))
|
|
||||||
mbchc.run_activity(char, ag, action)
|
|
||||||
} catch (x) { mbchc.report(x) } },
|
|
||||||
bell: function() {
|
|
||||||
setTimeout(() => {document.getElementById("InputChat").style.outline = ""}, 100)
|
|
||||||
document.getElementById("InputChat").style.outline = "solid red"
|
|
||||||
},
|
|
||||||
complete: function(options, space = true) {
|
|
||||||
if (options.length < 1) return(this.bell())
|
|
||||||
if (options.length > 1) {
|
|
||||||
let width = Math.max(...options.map(o => o.length))
|
|
||||||
let pref = null
|
|
||||||
for (let i = width; i > 0; i -= 1) { let test = options[0].slice(0, i); if (options.every(o => o.startsWith(test))) {pref = test; break} }
|
|
||||||
if (pref) this.complete([pref], false)
|
|
||||||
this.complete_hint(options)
|
|
||||||
} else window.ElementValue("InputChat", document.getElementById("InputChat").value.replace(this.RE_LAST_WORD, `$1${options[0]}${space ? " " : ""}`))
|
|
||||||
},
|
|
||||||
complete_hint: function(options) {
|
|
||||||
this.COMP_HINT.innerHTML = options.sort().map(s => `<div>${s}</div>`).join("")
|
|
||||||
this.COMP_HINT.style.display = "flex"
|
|
||||||
window.ElementSetDataAttribute(this.COMP_HINT.id, "colortheme", (window.Player.ChatSettings.ColorTheme || "Light"))
|
|
||||||
let rescroll = window.ElementIsScrolledToEnd("TextAreaChatLog")
|
|
||||||
window.ChatRoomResize(false)
|
|
||||||
if (rescroll) window.ElementScrollToEnd("TextAreaChatLog")
|
|
||||||
},
|
|
||||||
comp_hint_visible: function() {return(this.COMP_HINT.parentElement && "flex" === this.COMP_HINT.style.display)},
|
|
||||||
complete_hint_hide: function(options) {if (!this.comp_hint_visible()) return; this.COMP_HINT.style.display = "none"; window.ChatRoomResize(false)},
|
|
||||||
complete_target: function(token, me2 = true, check_perms = false) {
|
|
||||||
let [locase, found] = [token.toLocaleLowerCase(), new Set()]
|
|
||||||
for (let c of window.ChatRoomCharacter) {
|
|
||||||
if ((c.IsPlayer() && !me2) || (check_perms && !window.ServerChatRoomGetAllowItem(window.Player, c))) continue
|
|
||||||
this.char2targets(c).forEach(s => {if (s.toLocaleLowerCase().startsWith(locase)) found.add(s)})
|
|
||||||
}
|
|
||||||
this.complete(Array.from(found))
|
|
||||||
},
|
|
||||||
complete_common: function() {
|
|
||||||
let input = document.getElementById("InputChat").value
|
|
||||||
return([this, input, this.tokenise(input)])
|
|
||||||
},
|
|
||||||
complete_mbchc: function(args, locase, cmdline) { const [mbchc, input, tokens] = window.MBCHC.complete_common(); // `this` is command object
|
|
||||||
if (tokens.length < 1) return
|
|
||||||
if (tokens.length < 2) return(mbchc.complete([`${mbchc.CommandsKey}${this.Tag}`]))
|
|
||||||
let subname = tokens[1].toLocaleLowerCase()
|
|
||||||
if (tokens.length < 3) return(mbchc.complete(Object.keys(mbchc.SUBCOMMANDS_MBCHC).filter(c => c.startsWith(subname)))) // complete subcommand name
|
|
||||||
let sub = mbchc.SUBCOMMANDS_MBCHC[subname]
|
|
||||||
if (sub && sub.args) {
|
|
||||||
let argname = Object.keys(sub.args)[tokens.length - 3]
|
|
||||||
if ("TARGET" === argname) return(mbchc.complete_target(tokens[tokens.length - 1], false))
|
|
||||||
if ("[TARGET]" === argname) return(mbchc.complete_target(tokens[tokens.length - 1]), true)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
complete_do_target: function(actions, token) {
|
|
||||||
if (!actions) return
|
|
||||||
let me2 = (actions.self.length > 0)
|
|
||||||
if (me2 && actions.others.length < 1) return(this.complete([window.Player.cid.toString()])) // target is always the player
|
|
||||||
this.complete_target(token, me2, true)
|
|
||||||
},
|
|
||||||
complete_do: function(args, locase, cmdline) { const [mbchc, input, tokens] = window.MBCHC.complete_common(); // `this` is command object
|
|
||||||
if (tokens.length < 1) 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
|
|
||||||
let 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
|
|
||||||
let 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: function(args, locase, cmdline) { const [mbchc, input, tokens] = window.MBCHC.complete_common(); // `this` is command object
|
|
||||||
if (tokens.length < 1) return
|
|
||||||
if (tokens.length < 2) return(mbchc.complete([`${mbchc.CommandsKey}${this.Tag}`]))
|
|
||||||
if (tokens.length > 2) return(mbchc.bell())
|
|
||||||
let anim = tokens[1].toLocaleLowerCase()
|
|
||||||
return(mbchc.complete(Object.keys(window.bce_EventExpressions).filter(a => a.toLocaleLowerCase().startsWith(anim))))
|
|
||||||
},
|
|
||||||
complete_fbc_pose: function(args, locase, cmdline) { const [mbchc, input, tokens] = window.MBCHC.complete_common(); // `this` is command object
|
|
||||||
if (tokens.length < 1) return
|
|
||||||
if (tokens.length < 2) return(mbchc.complete([`${mbchc.CommandsKey}${this.Tag}`]))
|
|
||||||
let pose = tokens[tokens.length - 1].toLocaleLowerCase()
|
|
||||||
return(mbchc.complete(window.PoseFemale3DCG.map(p => p.Name).filter(p => p.toLocaleLowerCase().startsWith(pose))))
|
|
||||||
},
|
|
||||||
history: function(down) {
|
|
||||||
let [text, history] = [window.ElementValue("InputChat"), window.ChatRoomLastMessage]
|
|
||||||
if (!this.HISTORY_MODE) {history.push(text); this.HISTORY_MODE = true}
|
|
||||||
let ids = history.map((t,i) => [t,i]).filter(([t,i]) => t.startsWith(history[history.length - 1])).map(([t,i]) => i)
|
|
||||||
if (!down) ids.reverse()
|
|
||||||
let found = ids.find(id => (down) ? id > window.ChatRoomLastMessageIndex : id < window.ChatRoomLastMessageIndex)
|
|
||||||
if (!found) return(this.bell())
|
|
||||||
window.ElementValue("InputChat", history[found])
|
|
||||||
window.ChatRoomLastMessageIndex = found
|
|
||||||
},
|
|
||||||
focus_chat_whitelist(event) {
|
|
||||||
if (event.ctrlKey && "v" === event.key) return true // ctrl+V should paste
|
|
||||||
return false
|
|
||||||
},
|
|
||||||
focus_chat(event) { // TODO: this is not ideal, but it will have to do for now
|
|
||||||
if (event.repeat) return // only unique presses please
|
|
||||||
if ("inline" !== document.getElementById("InputChat")?.style.display) return // input chat missing
|
|
||||||
if (["InputChat","bce-message-input"].includes(document.activeElement.id)) return // focus already set
|
|
||||||
if ([event.altKey, event.ctrlKey, event.metaKey].some(i => i) && !this.focus_chat_whitelist(event)) return // alt, ctrl and meta should all be false
|
|
||||||
window.ElementFocus("InputChat")
|
|
||||||
},
|
|
||||||
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.HIDE_ALL = this.HIDE_SPECIAL.concat(this.HIDE_BODY).concat(this.HIDE_CLOTHES).concat(this.HIDE_ITEMS)
|
|
||||||
this.CommandsKey = CommandsKey /* eslint-disable-line no-undef */ // window.CommandsKey is undefined
|
|
||||||
this.RE_ACTIVITY = RegExp(`^${this.CommandsKey}activity `)
|
|
||||||
this.PREF_ACTIVITY = `${this.CommandsKey}activity `
|
|
||||||
this.COMP_HINT = document.createElement("div")
|
|
||||||
this.COMP_HINT.id = "mbchcCompHint"
|
|
||||||
let css = document.createElement("style")
|
|
||||||
css.type = "text/css"
|
|
||||||
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} {
|
|
||||||
flex-flow: column wrap;
|
|
||||||
overflow: auto;
|
|
||||||
display: none;
|
|
||||||
background-color: ${this.RGB_POLLY};
|
|
||||||
color: black;
|
|
||||||
}
|
|
||||||
#${this.COMP_HINT.id}[data-colortheme="dark"], #${this.COMP_HINT.id}[data-colortheme="dark2"] {
|
|
||||||
background-color: ${this.RGB_MUTE};
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
#${this.COMP_HINT.id} div {
|
|
||||||
margin: 0 0.5ex;
|
|
||||||
}
|
|
||||||
`
|
|
||||||
document.head.appendChild(css)
|
|
||||||
// Actions
|
|
||||||
this.calculate_maps()
|
|
||||||
window.Player.MBCHC = {VERSION: this.VERSION}
|
|
||||||
window.CommandCombine(COMMANDS)
|
|
||||||
|
|
||||||
// Hooks
|
|
||||||
this.remove_fbc_hook = this.before("MainRun", () => window.bce_ActivityTriggers && this.patch_fbc())
|
|
||||||
this.after("CharacterOnlineRefresh", char => this.update_char(char))
|
|
||||||
this.after("ChatRoomReceiveSuitcaseMoney", () => {
|
|
||||||
if (this.AUTOHACK_ENABLED && this.LAST_HACKED) {
|
|
||||||
window.CurrentCharacter = this.cid2char(this.LAST_HACKED)
|
|
||||||
this.LAST_HACKED = null
|
|
||||||
window.ChatRoomTryToTakeSuitcase()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
this.before("ChatRoomSendChat", () => {
|
|
||||||
let input = window.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)
|
|
||||||
window.ElementValue("InputChat", input)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
this.after("ChatRoomSendChat", () => {
|
|
||||||
const history = window.ChatRoomLastMessage
|
|
||||||
if ((history.length > 1) && (history[history.length - 1] === history[history.length - 2])) {history.pop(); window.ChatRoomLastMessageIndex -= 1}
|
|
||||||
})
|
|
||||||
this.before("ChatRoomDrawCharacterOverlay", (C, CharX, CharY, Zoom, Pos) => {
|
|
||||||
window.ChatRoomHideIconState < 1 && C.MBCHC && window.DrawRect(CharX + 175 * Zoom, CharY, 50 * Zoom, 50 * Zoom, C.MBCHC.VERSION === window.Player.MBCHC.VERSION ? this.RGB_POLLY : this.RGB_MUTE)
|
|
||||||
if (window.ChatRoomHideIconState < 1 && C.MBCHC_LOCAL && "number" === typeof C.MBCHC_LOCAL.TZ) {
|
|
||||||
const hours = new Date(window.CommonTime() + this.UTC_OFFSET + C.MBCHC_LOCAL.TZ * 60 * 60 * 1000).getHours()
|
|
||||||
window.DrawTextFit(hours < 10 ? "0" + hours.toString() : hours.toString(), CharX + 200 * Zoom, CharY + 25 * Zoom, 46 * Zoom, "white", "black")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
this.after("ElementValue", (ID, Value, cc = window.CurrentCharacter) => "bce_LayerPriority" === ID && cc?.FocusGroup && window.InventoryGet(cc, cc.FocusGroup.Name)?.Difficulty.toString() === Value && window.InventoryLocked(cc, cc.FocusGroup.Name, true) && window.ElementSetAttribute(ID, "disabled", true))
|
|
||||||
this.after("ChatRoomCreateElement", () => this.COMP_HINT.parentElement || document.body.appendChild(this.COMP_HINT))
|
|
||||||
this.before("ChatRoomClearAllElements", () => !void this.complete_hint_hide() && this.COMP_HINT.remove())
|
|
||||||
this.before("ChatRoomClick", () => this.complete_hint_hide())
|
|
||||||
this.after("ChatRoomResize", () => {
|
|
||||||
if (window.CharacterGetCurrent() == null && window.CurrentScreen == "ChatRoom" && document.getElementById("InputChat") && document.getElementById("TextAreaChatLog") && this.comp_hint_visible()) { // upstream
|
|
||||||
const fontsize = ChatRoomFontSize /* eslint-disable-line no-undef */ // window.ChatRoomFontSize is undefined
|
|
||||||
window.ElementPositionFix("TextAreaChatLog", fontsize, 1005, 66, 988, 630)
|
|
||||||
window.ElementPositionFix(this.COMP_HINT.id, fontsize, 1005, 701, 988, 200)
|
|
||||||
this.COMP_HINT.style.display = "flex"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
document.addEventListener("keydown", event => this.focus_chat(event))
|
|
||||||
this.SDK.hookFunction("ChatRoomKeyDown", 0, (nextargs, next) => { // this fires on chat input events
|
|
||||||
let [event] = nextargs
|
|
||||||
window.MBCHC.complete_hint_hide()
|
|
||||||
if ((window.KeyPress == 33) || (window.KeyPress == 34)) { // better history
|
|
||||||
event.preventDefault()
|
|
||||||
return(window.MBCHC.history(window.KeyPress - 33))
|
|
||||||
}
|
|
||||||
if (window.MBCHC.HISTORY_MODE) {
|
|
||||||
window.ChatRoomLastMessage.pop()
|
|
||||||
window.MBCHC.HISTORY_MODE = false
|
|
||||||
}
|
|
||||||
return(next(nextargs))
|
|
||||||
})
|
|
||||||
|
|
||||||
// Chat room handlers
|
|
||||||
window.ChatRoomRegisterMessageHandler({ Priority: -220, Description: "MBCHC preprocessor", Callback: (data, sender, msg, metadata) => {
|
|
||||||
data.MBCHC_ID = this.NEXT_MESSAGE
|
|
||||||
this.NEXT_MESSAGE += 1
|
|
||||||
if (this.LOG_MESSAGES) console.debug({data, sender, msg, metadata})
|
|
||||||
}})
|
|
||||||
window.ChatRoomRegisterMessageHandler({ Priority: -219, Description: "MBCHC room enter hook",
|
|
||||||
Callback: (data, sender, msg, metadata) => { if (("Action" === data.Type) && ("ServerEnter" === data.Content) && (data.Sender === window.Player.cid)) this.player_enters_room() }
|
|
||||||
})
|
|
||||||
window.ChatRoomRegisterMessageHandler({ Priority: -219, Description: "MBCHC specific consumer",
|
|
||||||
Callback: (data, sender, msg, metadata) => { if (("Hidden" === data.Type) && ("MBCHC" === data.Content)) return this.receive(data) }
|
|
||||||
})
|
|
||||||
window.ChatRoomRegisterMessageHandler({ Priority: -219, Description: "MBCHC autohack lookup",
|
|
||||||
Callback: (data, sender, msg, metadata) => { if (("Hidden" === data.Type) && ("ReceiveSuitcaseMoney" === data.Content)) this.LAST_HACKED = data.Sender }
|
|
||||||
})
|
|
||||||
|
|
||||||
// footer
|
|
||||||
this.LOADED = true
|
|
||||||
this.log("info", `loaded version ${this.VERSION}`)
|
|
||||||
if (window.GameVersion !== this.TARGET_VERSION) this.log("warn", `Game version doesn't match the target ("${this.TARGET_VERSION}"), beware of incompatibilities`) // TODO: betas are like R86Beta1; cheat?
|
|
||||||
if (("Online" === window.CurrentModule) && ("ChatRoom" === window.CurrentScreen)) {
|
|
||||||
window.ChatRoomCharacter.forEach(c => this.update_char(c))
|
|
||||||
this.player_enters_room()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
preloader() {
|
|
||||||
this.SDK = window.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 (x) {console.error(x)} finally {return next(nextargs)}})
|
|
||||||
this.after = (name, cb) => this.SDK.hookFunction(name, 0, (nextargs,next) => {const result = next(nextargs); try {cb?.(...nextargs)} catch (x) {console.error(x)} finally {return result}})
|
|
||||||
if (window.CurrentModule && window.CurrentScreen && !("Character" === window.CurrentModule && "Login" === window.CurrentScreen)) return this.loader()
|
|
||||||
this.remove_load_hook = this.before("AsylumGGTSSAddItems", () => this.loader())
|
|
||||||
}
|
|
||||||
} // MBCHC
|
|
||||||
|
|
||||||
fetch("https://code.fleshless.org/mute/MBCHC/raw/branch/master/bondage-club-mod-sdk-1.1.0.js").then(r=>r.text()).then(r=>eval(r)).then(_=>window.MBCHC.preloader()).catch(x=>console.error(x)) /* eslint-disable-line no-eval */
|
|
||||||
})()
|
|
592
mbchc.mjs
592
mbchc.mjs
|
@ -1,592 +0,0 @@
|
||||||
// Take a look at the .d.ts for comments.
|
|
||||||
export const version = '108.13.1'
|
|
||||||
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
|
|
||||||
|
|
||||||
const/**@type {FP.cur}*/cur = (v, f) => f(v)
|
|
||||||
const/**@type {FP.n2u}*/n2u = v => v === null ? $ : v
|
|
||||||
const/**@type {FP.enu}*/enu = o => Object.keys(o)
|
|
||||||
const/**@type {FP.val}*/val = (v, f) => v === $ || v === null ? $ : f(v)
|
|
||||||
const/**@type {FP.add}*/add = (x, y) => x + y
|
|
||||||
const/**@type {FP.sub}*/sub = (x, y) => x - y
|
|
||||||
const/**@type {FP.cgt}*/cgt = (x, y) => x > y
|
|
||||||
const/**@type {FP.cge}*/cge = (x, y) => x >= y
|
|
||||||
const/**@type {FP.clt}*/clt = (x, y) => x < y
|
|
||||||
const/**@type {FP.cle}*/cle = (x, y) => x <= y
|
|
||||||
const/**@type {FP.int}*/int = s => cur(Number.parseInt(s, 10), n => Number.isNaN(n) ? $ : n)
|
|
||||||
/**@type {FP.fun}*/const fun = f => typeof f === 'function'
|
|
||||||
const/**@type {FP.run}*/run = (fs, ...args) => {fs.forEach(f => fun(f) && void f(...args)); return true}
|
|
||||||
const/**@type {FP.yes}*/yes = (...args) => run(args, $)
|
|
||||||
const/**@type {FP.mut}*/mut = (v, ...args) => run(args, v) && v
|
|
||||||
const/**@type {FP.del}*/del = (o, p) => mut(o, Reflect.deleteProperty(o, p))
|
|
||||||
const/**@type {FP.ass}*/ass = (x, v, c = Boolean) => {if (v === $ || !c(v)) throw new Error(x); return v}
|
|
||||||
const/**@type {FP.asa}*/asa = (p, v) => v instanceof p ? v : $
|
|
||||||
const/**@type {FP.rsc}*/rsc = (f, r) => {try {f($)} catch (x) {r(x)} return true}
|
|
||||||
const/**@type {FP.Interval}*/range = new (class { proxy = new Proxy($O, this); min = 0; max = 0; mini = false; maxi = false
|
|
||||||
/**@type {FP.Interval['has']}*/has = (_, x) => val(int(x), x => (this.mini ? cge : cgt)(x, this.min) && (this.maxi ? cle : clt)(x, this.max)) ?? false
|
|
||||||
})()
|
|
||||||
const/**@type {FP.rng}*/rng = (min, max, mini = true, maxi = true) => mut(range.proxy, Object.assign(range, {min, max, mini, maxi}))
|
|
||||||
const/**@type {FP.m_t}*/m_t = v => { if (typeof v === 'string') return v.length === 0
|
|
||||||
if (v === $ || typeof v === 'object') return v === $ || v === null
|
|
||||||
|| ['length', 'size'].some(n => n in v && cur(/**@type {OBJ}*/(v)[n], x => typeof x === 'number' && x === 0))
|
|
||||||
|| (Object.getPrototypeOf(v) === Object.prototype && Reflect.ownKeys(v).length === 0)
|
|
||||||
return typeof v === 'boolean' && !v
|
|
||||||
}
|
|
||||||
//const/**@type {FP.loo}*/loo = (m, c, a) => {let r; for (let n = 0; c($) && n < m; n++) r = a($); return r}
|
|
||||||
|
|
||||||
/**@template T*/const Pipe = /**@implements {FP.Pipeline}*/class PipeClass {
|
|
||||||
/**@type {FP.Pipeline<T>['proxy']}*/proxy = new Proxy(/**@type {this & Record<number, T | undefined>}*/(this), {get(t, n, r) {return cur(typeof n === 'string' && int(n), i => {
|
|
||||||
if (typeof i !== 'number' || i < 0) return Reflect.get(t, n, r)
|
|
||||||
let c = 0; for (const v of t) if (++c > i) return v; return $
|
|
||||||
})}})
|
|
||||||
constructor(/**@type {Iterable<T>}*/iterable) {this.iterable = iterable}
|
|
||||||
/**@type {FP.Pipeline<T>['me']}*/me(iterable) {return (new PipeClass(iterable)).proxy}
|
|
||||||
[Symbol.iterator]() {return this.iterable[Symbol.iterator]()}
|
|
||||||
/**@type {FP.Pipeline<T>['rdc']}*/rdc(i, f) {let ax = i; for (const v of this) ax = f(ax, v); return ax}
|
|
||||||
/**@type {FP.Pipeline<T>['any']}*/any(f) {for (const v of this) if (f(v)) return true; return false}
|
|
||||||
/**@type {FP.Pipeline<T>['all']}*/all(f) {for (const v of this) if (!f(v)) return false; return true}
|
|
||||||
/**@type {FP.Pipeline<T>['map']}*/map(f) {return this.me((function*(i, f) {for (const v of i) yield f(v)})(this, f))}
|
|
||||||
/**@type {FP.Pipeline<T>['sel']}*/sel(f) {return this.me((function*(i, f) {for (const v of i) if (f(v)) yield v})(this, f))}
|
|
||||||
}
|
|
||||||
const/**@type {FP.P}*/P = I => (new Pipe(I)).proxy
|
|
||||||
|
|
||||||
const/**@type {Cons.Wrap}*/CW = new (class {
|
|
||||||
/**@type {Cons.MS}*/ms = {w: 'warn', i: 'info', d: 'debug', l: 'log'}; w = this.gen('w'); i = this.gen('i'); d = this.gen('d'); l = this.gen('l')
|
|
||||||
/**@type {Cons.E}*/e = x => yes(void console.error(x))
|
|
||||||
/**@type {Cons.Wrap['gen']}*/gen(m) {return msg => yes(void console[this.ms[m]](`MBCHC: ${msg}`))}
|
|
||||||
})()
|
|
||||||
|
|
||||||
const/**@type {Settings.Methods}*/Settings = { // FIXME separate a proper V1 type from an unknown object in the ExtensionSettings
|
|
||||||
/**I hate change*/migrate_0_1(v0) { if (v0.MBCHC === $) return true
|
|
||||||
val(v0.MBCHC.timezones, tz => this.save(v1 => v1.TZ = {...tz, ...v1.TZ}))
|
|
||||||
W.ServerAccountUpdate.QueueData({OnlineSettings: del(v0, 'MBCHC')})
|
|
||||||
return CW.w('MBCHC: settings migration done (v0 -> v1). This should never happen again on the same account.')
|
|
||||||
},
|
|
||||||
save(f = $) {W.Player.ExtensionSettings['MBCHC'] ||= {}; f?.(this.v1); return yes(void W.ServerPlayerExtensionSettingsSync('MBCHC'))},
|
|
||||||
replace: v1 => yes(void (W.Player.ExtensionSettings['MBCHC'] = v1), void Settings.save()),
|
|
||||||
'purge!': () => Settings.replace(/** @type {Settings.V1} */({})),
|
|
||||||
get v0() {return Reflect.get(W.Player, 'OnlineSettings')},
|
|
||||||
get v1() {return mut(/**@type {Settings.V1}*/(W.Player.ExtensionSettings['MBCHC']) ?? {}, v1 => { // we need to check and repair the whole object every time we access it
|
|
||||||
v1.TZ ||= {}
|
|
||||||
})},
|
|
||||||
}
|
|
||||||
|
|
||||||
const/**@type {TZ_Cache}*/TZ = { map: new Map(), RE: /(?:gmt|utc)\s*([+-])\s*(\d\d?)/i,
|
|
||||||
parse: desc => val(desc, v => val(TZ.RE.exec(v), m => val(int(`${m[1] ?? $S}${m[2] ?? $S}`), n => n in rng(-12, 12) ? n : $))),
|
|
||||||
memo: (cid, desc = $) => val(Settings.v1.TZ[cid] ?? TZ.parse(desc), n => mut(n, TZ.map.set(cid, n))),
|
|
||||||
lookup: c => val(U.cid(c), cid => TZ.map.get(cid) ?? TZ.memo(cid, c.Description)),
|
|
||||||
}
|
|
||||||
|
|
||||||
const/**@type {Utils}*/U = { remove_loader_hook: $, RGB: {Polly: '#81b1e7', Mute: '#6c2132'}, ACT: `${CommandsKey}activity `,
|
|
||||||
RE: { SPACES: /\s+/gu, REL: {L: /^<+$/, R: /^>+$/}, '@': [/^@/, /^@@/] },
|
|
||||||
get crc() {return W.ChatRoomCharacter},
|
|
||||||
get ic() {return asa(HTMLTextAreaElement, D.querySelector('#InputChat'))},
|
|
||||||
cid: c => c.MemberNumber,
|
|
||||||
dn: c => W.CharacterNickname(c),
|
|
||||||
current: () => `${W.CurrentModule}/${W.CurrentScreen}`,
|
|
||||||
style: (q, f) => cur(D.querySelector(q), e => e instanceof HTMLElement ? f(e.style) : $),
|
|
||||||
inform: html => yes(void W.ChatRoomSendLocal(`<div class="mbchc">${html}</div>`)),
|
|
||||||
report: x => U.inform(String(x)) && CW.e(x),
|
|
||||||
split: text => text.split(U.RE.SPACES),
|
|
||||||
abs2char: pos => ass(`invalid position ${pos}`, U.crc[pos]),
|
|
||||||
rel2char: t => cur(ass('can\'t find my position', U.crc.findIndex(char => char.IsPlayer()), n => n >= 0), me =>
|
|
||||||
cur(ass(`failed to parse target "${t}"`, U.RE.REL.L.test(t) ? sub : U.RE.REL.R.test(t) ? add : $)(me, t.length), pos =>
|
|
||||||
cur(pos % U.crc.length, p => U.abs2char(p < 0 ? p + U.crc.length : p)))),
|
|
||||||
cid2char: id => id === U.cid(W.Player) ? W.Player : ass(`character ${id} not found in the room`, U.crc.find(c => id === U.cid(c))),
|
|
||||||
target2char(target) { let t = target.trim(); const /**@type {Set<Character>}*/f = new Set() // FIXME Target should be low case (take a look at this later)
|
|
||||||
if (m_t(t)) return W.Player
|
|
||||||
if (t.at(0) === '=') return U.cid2char(ass(`invalid member number "${target}"`, int(t.slice(1))))
|
|
||||||
if ('<>'.includes(t.at(0) ?? '-')) return U.rel2char(t)
|
|
||||||
const n = int(t)
|
|
||||||
if (n !== $ && n.toString() === t) { // We got a number
|
|
||||||
if (n in rng(0, 9)) return U.abs2char(n)
|
|
||||||
if (n in rng(11, 15)) return U.abs2char(n - 11)
|
|
||||||
if (n in rng(21, 25)) return U.abs2char(n - 16)
|
|
||||||
U.crc.filter(c => U.cid(c)?.toString().includes(t)).forEach(c => f.add(c)) // or union with `new Set(array)`
|
|
||||||
}
|
|
||||||
|
|
||||||
if (t.at(0) === '@') t = t.slice(1)
|
|
||||||
U.crc.filter(c => c.Name.toLocaleLowerCase().includes(t)).forEach(c => f.add(c))
|
|
||||||
U.crc.filter(c => val(c.Nickname, nn => nn.toLocaleLowerCase().includes(t))).forEach(c => f.add(c))
|
|
||||||
|
|
||||||
const found = [...f.keys()]
|
|
||||||
ass(`target "${target}": multiple matches (${found.map(c => `${U.cid(c)}|${c.Name}|${c.Nickname ?? c.Name}`).join(',')})`, found.length, n => n < 2) // make the list better
|
|
||||||
return ass(`target "${target}": no match`, found[0])
|
|
||||||
},
|
|
||||||
mkdiv: html => mut(D.createElement('div'), e => html === $ || (e.innerHTML = html)),
|
|
||||||
bell: () => yes(U.style('#InputChat', s => s.outline = 'solid red'), void setTimeout(() => {U.style('#InputChat', s => s.outline = $S)}, 100)),
|
|
||||||
targets: (reject_player = false, check_perms = false) => mut(new Set(), r => {
|
|
||||||
const wrap_text = (/**@type {string}*/text) => int(text) === $ ? text : `@${text}`
|
|
||||||
const wrap_int = (/**@type {number}*/i) => i in rng(0, 9) || i in rng(11, 15) || i in rng(21, 25) ? `=${i}` : i.toString()
|
|
||||||
U.crc.filter(c => !(reject_player && c.IsPlayer()) && !(check_perms && !W.ServerChatRoomGetAllowItem(W.Player, c))).forEach(c => {
|
|
||||||
U.split(c.Name).forEach(t => r.add(wrap_text(t)))
|
|
||||||
val(c.Nickname, n => U.split(n))?.forEach(t => r.add(wrap_text(t)))
|
|
||||||
val(U.cid(c), cid => r.add(wrap_int(cid)))
|
|
||||||
})
|
|
||||||
r.delete($S)
|
|
||||||
}),
|
|
||||||
complete_mbchc() {C.icomplete(ws =>
|
|
||||||
ws.length < 2 ? new Set([`${CommandsKey}${this.Tag}`])
|
|
||||||
: ws.length < 3 ? new Set(Object.keys(SUBCOMMANDS_MBCHC)) // FIXME no idea why enu() thinks there's a number in there
|
|
||||||
: val(ws[1], w => val(SUBCOMMANDS_MBCHC[w], sub => val(sub.args, as => val(enu(as)[ws.length - 3], a =>
|
|
||||||
a === 'TARGET' ? U.targets(true) : a === '[TARGET]' ? U.targets() : $)))) ?? $Ss
|
|
||||||
)},
|
|
||||||
complete_do_target(actions) {
|
|
||||||
if (m_t(actions)) return $Ss
|
|
||||||
if (m_t(actions.others)) return val(U.cid(W.Player), cid => new Set([cid.toString()])) ?? $Ss // Target is always the player
|
|
||||||
return U.targets(m_t(actions.self), true)
|
|
||||||
},
|
|
||||||
complete_do() {C.icomplete(ws => {
|
|
||||||
if (ws.length < 2) return new Set([`${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 = (ws[1] ?? $S).toLocaleLowerCase()
|
|
||||||
const DD = W.MBCHC.DO_DATA
|
|
||||||
if (ws.length < 3) return new Set(enu(DD.verbs).map(String)) // Complete verb
|
|
||||||
const ags = DD.verbs[low]
|
|
||||||
if (ags === $) return $Ss
|
|
||||||
low = (ws[2] ?? $S).toLocaleLowerCase()
|
|
||||||
if (ws.length < 4) { // Complete zone or target
|
|
||||||
if (enu(ags).length < 2) return val(ags[enu(ags)[0] ?? $S], t => U.complete_do_target(t)) ?? $Ss // Zone implied, complete target
|
|
||||||
return new Set(Object.entries(DD.zones).filter(([zone, ag]) => zone.startsWith(low) && ags[ag]).map(([zone, _ag]) => zone))
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ws.length < 5) { // Complete target where it belongs
|
|
||||||
if (enu(ags).length < 2) return $Ss // Zone implied, target already given
|
|
||||||
return val(ags[DD.zones[low] ?? $S], t => U.complete_do_target(t)) ?? $Ss
|
|
||||||
}
|
|
||||||
|
|
||||||
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())),
|
|
||||||
}
|
|
||||||
|
|
||||||
const/**@type {SUBCOMMANDS}*/SUBCOMMANDS_MBCHC = {
|
|
||||||
autohack: {desc: 'toggle the autohack feature', cb: mbchc => void U.inform(`Autohack is now ${((mbchc.AUTOHACK_ENABLED = !mbchc.AUTOHACK_ENABLED)) ? 'enabled' : 'disabled'}`)},
|
|
||||||
donate: {desc: 'buy data and send it to recipient', args: {TARGET: $O}, cb: (mbchc, args) => void mbchc.donate_data(args[0] ?? $S)},
|
|
||||||
tz: {desc: 'set target\'s UTC offset', args: {OFFSET: $O, '[TARGET]': $O}, cb: (mbchc, args) => mbchc.set_timezone(args)},
|
|
||||||
'purge!': {desc: 'delete MBCHC online saved data', cb: () => Settings['purge!']()},
|
|
||||||
}
|
|
||||||
|
|
||||||
const/**@type {Complete}*/C = { S_OPTS: {behavior: 'instant'},
|
|
||||||
e: mut(Object.assign(U.mkdiv(), {id: 'mbchcCompHint'}), div => void div.append(U.mkdiv())),
|
|
||||||
get div() {return C.e.firstElementChild ?? $},
|
|
||||||
lcp: cs => m_t(cs) ? $S : mut({p: ''}, r => void cur(P(cs), P => {for (let on = true, i = Math.max(...P.map(o => o.length)); on && i > 0; i -= 1) {
|
|
||||||
cur((P[0] ?? $S).slice(0, i), p => P.all(o => o.startsWith(p)) && yes(r.p = p) && (on = false))
|
|
||||||
}})).p,
|
|
||||||
complete_word: (i, cs, ci = false) => cur(void cs.forEach(c => (ci ? c.toLocaleLowerCase() : c).startsWith(ci ? i.toLocaleLowerCase() : i) || cs.delete(c)), _ => cs.size < 2 ? P(cs)[0] ?? i : C.lcp(cs)),
|
|
||||||
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))
|
|
||||||
C.div?.lastElementChild?.scrollIntoView(C.S_OPTS)
|
|
||||||
}),
|
|
||||||
get hidden() {return C.e.parentElement === null || C.e.style.display === 'none'},
|
|
||||||
hide: () => C.hidden || yes(C.e.style.display = 'none', void W.ChatRoomResize(false)),
|
|
||||||
complete(f, ci = false) {
|
|
||||||
const e = ass('Somehow #InputChat is broken', U.ic)
|
|
||||||
if (e.selectionStart !== e.selectionEnd) return U.bell()
|
|
||||||
const input = e.value
|
|
||||||
const before_cursor = input.slice(0, e.selectionStart)
|
|
||||||
const words = U.split(before_cursor)
|
|
||||||
const cs = f(words)
|
|
||||||
const word = words.at(-1) ?? ''
|
|
||||||
const cword = C.complete_word(word, cs, ci)
|
|
||||||
const additions = cword.slice(word.length)
|
|
||||||
if (!m_t(additions)) e.setRangeText(additions, e.selectionStart, e.selectionEnd, 'end')
|
|
||||||
if (cs.size > 1) C.hint(cs)
|
|
||||||
if (cs.size === 1) e.setRangeText(' ', e.selectionStart, e.selectionEnd, 'end')
|
|
||||||
if (cs.size === 0) U.bell()
|
|
||||||
return true
|
|
||||||
},
|
|
||||||
icomplete: f => C.complete(f, true),
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
ass('MBCHC found, aborting loading', W.MBCHC === $)
|
|
||||||
ass('AsylumGGTSSAddItems() not found, aborting MBCHC loading', W.AsylumGGTSSAddItems)
|
|
||||||
const sdk = ass('SDK not found, please load with (or after) FUSAM or any other mod that uses SDK', W.bcModSdk)
|
|
||||||
const mod = sdk.registerMod({name: 'MBCHC', fullName: 'Mute\'s Bondage Club Hacks Collection', version, repository: 'https://code.fleshless.org/mute/MBCHC/'})
|
|
||||||
const/**@type {SDK.Hook}*/prior = (name, f) => mod.hookFunction(name, 0, (na, n) => rsc(_ => f(...na), CW.e) && n(na)) // eslint-disable-line @typescript-eslint/no-unsafe-return
|
|
||||||
const/**@type {SDK.Hook}*/after = (name, f) => mod.hookFunction(name, 0, (na, n) => mut(n(na), rsc(_ => f(...na), CW.e))) // eslint-disable-line @typescript-eslint/no-unsafe-return
|
|
||||||
|
|
||||||
// ^ type-safe, new and much improved, but still work in progress
|
|
||||||
// =================================================================================
|
|
||||||
// v legacy mess, also type-safe now?
|
|
||||||
|
|
||||||
/**@type {Window['MBCHC']}*/W.MBCHC = {
|
|
||||||
version, /** Just in case someone used it for anything @deprecated*/VERSION: version,
|
|
||||||
Settings, TZ,
|
|
||||||
|
|
||||||
NEXT_MESSAGE: 1,
|
|
||||||
LOG_MESSAGES: false,
|
|
||||||
LOADED: false,
|
|
||||||
AUTOHACK_ENABLED: false,
|
|
||||||
/**@type {number | undefined}*/LAST_HACKED: $,
|
|
||||||
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 `),
|
|
||||||
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'],
|
|
||||||
},
|
|
||||||
DO_DATA: {verbs: {}, zones: {}},
|
|
||||||
calculate_maps() {
|
|
||||||
for (const [verbs, data] of Object.entries(this.MAP_ACTIONS)) {
|
|
||||||
const/**@type {{[k: string]: {self: string[]; others: string[]}}}*/unwound = {}
|
|
||||||
for (const [zones, actions] of Object.entries(data)) {
|
|
||||||
const all = val(actions.all, a => a.split('|')) ?? []
|
|
||||||
const processed = {self: val(actions.self, s => [...s.split('|'), ...all]) ?? all, others: val(actions.others, o => [...o.split('|'), ...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
|
|
||||||
},
|
|
||||||
normalise_message(/**@type {string}*/text, /**@type {OBJ<boolean>}*/options = $O) {
|
|
||||||
let result = text
|
|
||||||
if (options['trim'] ?? false) result = result.trim()
|
|
||||||
if (options['low'] ?? false) result = result.toLocaleLowerCase()
|
|
||||||
if (options['up'] ?? false) {
|
|
||||||
const first = result.at(0)?.toLocaleUpperCase() ?? $S
|
|
||||||
const rest = result.slice(1)
|
|
||||||
result = first + rest
|
|
||||||
}
|
|
||||||
if ((options['dot'] ?? false) && this.RE_LAST_LETTER.test(result)) result = `${result}.`
|
|
||||||
return result
|
|
||||||
},
|
|
||||||
donate_data(/**@type {string}*/target) {
|
|
||||||
const char = U.target2char(target)
|
|
||||||
ass('target must not be you', !char.IsPlayer())
|
|
||||||
ass('target must be bound', char.IsRestrained())
|
|
||||||
const cost = Math.round(((Math.random() * 10) + 15))
|
|
||||||
ass('not enough money', W.Player.Money >= cost)
|
|
||||||
W.CharacterChangeMoney(W.Player, -cost)
|
|
||||||
W.ServerSend('ChatRoomChat', {Content: 'ReceiveSuitcaseMoney', Type: 'Hidden', Target: U.cid(char)})
|
|
||||||
W.ChatRoomMessage({Sender: ass('...', 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(/**@type {Character}*/char, /**@type {AssetGroupItemName}*/ag, /**@type {ActivityName}*/action) {
|
|
||||||
try {
|
|
||||||
ass('activities disabled in this room', W.ActivityAllowed())
|
|
||||||
ass('no permissions', W.ServerChatRoomGetAllowItem(W.Player, char))
|
|
||||||
char.FocusGroup = ass('invalid AssetGroup', n2u(W.AssetGroupGet(char.AssetFamily, ag)))
|
|
||||||
const activity = ass('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 // eslint-disable-line unicorn/no-null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
send_activity(/**@type {string}*/message) { let Content = message; const /**@type {ChatMessageDictionary}*/Dictionary = [MISSING_PLAYER_DIALOG]
|
|
||||||
val(this.RE_ACT_CIDS.exec(Content), cids => {
|
|
||||||
Content = Content.replace(this.RE_ACT_CIDS, $S)
|
|
||||||
val(cids[1], cid => val(int(cid), n => Dictionary.push({SourceCharacter: n})))
|
|
||||||
val(cids[2], cid => val(int(cid), n => Dictionary.push({TargetCharacter: n})))
|
|
||||||
})
|
|
||||||
W.ServerSend('ChatRoomChat', {Type: 'Action', Content, Dictionary})
|
|
||||||
},
|
|
||||||
set_timezone(/**@type {string[]}*/args) {
|
|
||||||
const tz = ass(`invalid offset "${args[0]}"`, int(args[0] ?? $S))
|
|
||||||
ass('offset should be [-12,12]', tz in rng(-12, 12))
|
|
||||||
const char = U.target2char(args[1] ?? $S)
|
|
||||||
return val(U.cid(char), cid => Settings.save(v1 => v1.TZ[cid] = tz) && TZ.memo(cid))
|
|
||||||
},
|
|
||||||
command_mbchc(argline, cmdline, args) {
|
|
||||||
const mbchc = W.MBCHC
|
|
||||||
try { // `this` is command object
|
|
||||||
if (m_t(args)) return void U.inform(Object.entries(SUBCOMMANDS_MBCHC).map(([cmd, sub]) => `<div>/mbchc ${cmd} ${val(sub.args, a => enu(a).join(' ')) ?? $S}: ${sub.desc}</div>`).join($S))
|
|
||||||
const cmd = String(args.shift())
|
|
||||||
const sub = ass(`unknown subcommand "${cmd}"`, SUBCOMMANDS_MBCHC[cmd])
|
|
||||||
sub.cb.call(mbchc, mbchc, args, argline, cmdline)
|
|
||||||
} catch (x) {
|
|
||||||
U.report(x)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
command_activity(argline, cmdline, _) {
|
|
||||||
const mbchc = W.MBCHC
|
|
||||||
if (!m_t(argline.trim())) {
|
|
||||||
try { // `this` is command object
|
|
||||||
const message = mbchc.normalise_message(cmdline.replace(mbchc.RE_ACTIVITY, $S), {trim: true, dot: true, up: true})
|
|
||||||
mbchc.send_activity(message)
|
|
||||||
} catch (x) {
|
|
||||||
U.report(x)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
command_do(_argline, _cmdline, args) {
|
|
||||||
const mbchc = W.MBCHC
|
|
||||||
try { // `this` is command object
|
|
||||||
if (m_t(args)) return void U.inform('<div>Usage: /do VERB [ZONE] [TARGET]</div><div>Available verbs:</div>' + enu(mbchc.MAP_ACTIONS).join(', ') + '<div>Available zones:</div>' + enu(mbchc.DO_DATA.zones).join(', '))
|
|
||||||
let [verb, zone, target] = args
|
|
||||||
const zones = ass(`unknown verb "${verb}"`, mbchc.DO_DATA.verbs[verb ?? $S])
|
|
||||||
if (enu(zones).length === 1) {
|
|
||||||
if (target === $) target = zone
|
|
||||||
zone = mbchc.MAP_ZONES[enu(zones)[0] ?? $S]?.[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
zone = ass('zone missing', zone)
|
|
||||||
const ag = ass(`unknown zone "${zone}"`, mbchc.DO_DATA.zones[zone])
|
|
||||||
const types = ass(`zone "${zone}" invalid for "${verb}"`, zones[ag])
|
|
||||||
let/**@type {Character}*/char = W.Player
|
|
||||||
if (target !== $ && (m_t(types.self) || !m_t(types.others))) char = U.target2char(target)
|
|
||||||
const type = char.IsPlayer() ? 'self' : 'others'
|
|
||||||
const available = W.ActivityAllowedForGroup(char, /**@type {AssetGroupItemName}*/(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 = ass(`zone "${zone}" invalid for ("${verb}" "${type}")`, types[type])
|
|
||||||
const action = ass(`invalid action (${verb} ${zone} ${target})`, actions.find(name => available.find(a => a.Activity?.Name === name)))
|
|
||||||
mbchc.run_activity(char, /**@type {AssetGroupItemName}*/(ag), /**@type {ActivityName}*/(action))
|
|
||||||
} catch (x) { U.report(x) }
|
|
||||||
},
|
|
||||||
loader() {
|
|
||||||
U.remove_loader_hook === $ || yes(void U.remove_loader_hook(), U.remove_loader_hook = $)
|
|
||||||
if (this.LOADED) return
|
|
||||||
val(Settings.v0, v0 => Settings.migrate_0_1(v0))
|
|
||||||
|
|
||||||
// Calculated values
|
|
||||||
const/**@type {ICommand[]}*/COMMANDS = [
|
|
||||||
{Tag: 'mbchc', Description: ': Utility functions ("/mbchc" for help)', Action: this.command_mbchc, AutoComplete: U.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: U.complete_do},
|
|
||||||
]
|
|
||||||
const/**@type (e: Event) => void*/css_hook = e => void cur(/**@type {HTMLStyleElement}*/(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 = `
|
|
||||||
#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;
|
|
||||||
}
|
|
||||||
#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
|
|
||||||
|
|
||||||
// Actions
|
|
||||||
this.calculate_maps()
|
|
||||||
W.CommandCombine(COMMANDS)
|
|
||||||
|
|
||||||
// Hooks
|
|
||||||
after('ChatRoomReceiveSuitcaseMoney', () => {
|
|
||||||
if (this.AUTOHACK_ENABLED && this.LAST_HACKED !== $) {
|
|
||||||
W.CurrentCharacter = U.cid2char(this.LAST_HACKED)
|
|
||||||
this.LAST_HACKED = $
|
|
||||||
W.ChatRoomTryToTakeSuitcase()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
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))))
|
|
||||||
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))) {
|
|
||||||
history.pop()
|
|
||||||
W.ChatRoomLastMessageIndex -= 1
|
|
||||||
}
|
|
||||||
})
|
|
||||||
prior('ChatRoomCharacterViewDrawOverlay', (C, CX, CY, Z) => {
|
|
||||||
// if (w.ChatRoomHideIconState < 1 && C.MBCHC) {
|
|
||||||
// w.DrawRect(CharX + (175 * Zoom), CharY, 50 * Zoom, 50 * Zoom, C.MBCHC.VERSION === w.Player.MBCHC.VERSION ? U.RGB.Polly : U.RGB.Mute)
|
|
||||||
// }
|
|
||||||
val(TZ.lookup(C), ctz => W.ChatRoomHideIconState < 1 && void cur(new Date(W.CommonTime() + this.UTC_OFFSET + (ctz * 60 * 60 * 1000)).getHours(), hr =>
|
|
||||||
void W.DrawTextFit(`${hr < 10 ? '0' : ''}${hr}`, CX + (Z * 200), CY + (Z * 25), Z * 46, 'white', 'black')))
|
|
||||||
})
|
|
||||||
|
|
||||||
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':
|
|
||||||
e.stopImmediatePropagation()
|
|
||||||
e.preventDefault() // falls through
|
|
||||||
case 'Enter': case 'Backspace': H.exit(ic, e) // 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'))
|
|
||||||
})
|
|
||||||
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' && U.ic !== $ && D.querySelector('#TextAreaChatLog') !== null && !C.hidden) { // Upstream
|
|
||||||
W.ElementPositionFix(C.e.id, ChatRoomFontSize, 800, 65, 200, 835)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
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(HTMLDivElement, D.querySelector('#TextAreaChatLog')), t => t.dataset['mbchcHH'] = `𝗵𝗶𝘀𝘁𝗼𝗿𝘆: ${m_t(input) ? 'Everything' : `Prefix: ${input}`}`)
|
|
||||||
H.enter(ic, input, U.scrolled, 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?
|
|
||||||
let found = -1
|
|
||||||
let first = -1
|
|
||||||
let last = -1
|
|
||||||
for (const i of H.ids) { // these aren't necessarily in order
|
|
||||||
if (up) {
|
|
||||||
if (i < W.ChatRoomLastMessageIndex && i > found) found = i // the largest i that is less than index
|
|
||||||
if (i > last) last = i
|
|
||||||
} else {
|
|
||||||
if (first < 0 || i < first) first = i
|
|
||||||
if (i > W.ChatRoomLastMessageIndex && (i < found || found < 0)) found = i // the smallest i that is greater than index
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (found < 0) found = up ? last : first
|
|
||||||
const line = history[found] ?? $S
|
|
||||||
if (line === input) return U.bell()
|
|
||||||
ic.value = line
|
|
||||||
W.ChatRoomLastMessageIndex = found
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}))
|
|
||||||
|
|
||||||
// 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 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
|
|
||||||
CW.i(`loaded version ${version}`)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
U.current() === 'Character/Login' ? U.remove_loader_hook = prior('AsylumGGTSSAddItems', () => void W.MBCHC.loader()) : W.MBCHC.loader()
|
|
||||||
|
|
||||||
const/**@type {BCE.Patcher}*/BP = { timer: undefined, patches: [[/^\^('s)?( )?/g, '^SourceCharacter$1\\s+'], [/([^\\])\$/g, '$1\\.?$$']],
|
|
||||||
cfs: { anim: () => enu(W.bce_EventExpressions ?? $O), pose: () => W.PoseFemale3DCG.map(p => p.Name) },
|
|
||||||
gen(f) {return function() {C.icomplete(ws => ws.length < 2 ? new Set([`${CommandsKey}${this.Tag}`]) : ws.length < 3 ? new Set(f()) : $Ss)}},
|
|
||||||
copy: t => ({Type: 'Action', Event: t.Event, Matchers: t.Matchers.map(m => ({Tester: new RegExp(BP.patches.reduce((ax, [f, r]) => ax.replaceAll(f, r), m.Tester.source), $S)}))}),
|
|
||||||
patch: () => val(W.bce_ActivityTriggers, ts => val(W.FBC_VERSION, _ =>
|
|
||||||
yes(void clearInterval(BP.timer), void enu(BP.cfs).forEach(t => val(W.GetCommands().find(c => t === c.Tag), cmd => cmd.AutoComplete = BP.gen(BP.cfs[t]))), void ts.forEach(t => t.Type === 'Emote' && ts.push(BP.copy(t))))
|
|
||||||
)),
|
|
||||||
}
|
|
||||||
BP.timer = W.setInterval(BP.patch, 100)
|
|
5818
package-lock.json
generated
5818
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
94
package.json
94
package.json
|
@ -1,94 +0,0 @@
|
||||||
{
|
|
||||||
"name": "mbchc",
|
|
||||||
"version": "108.13.1",
|
|
||||||
"description": "Mute's Bondage Club Hacks Collection",
|
|
||||||
"author": "Mute",
|
|
||||||
"private": true,
|
|
||||||
"type": "module",
|
|
||||||
"devDependencies": {
|
|
||||||
"bc-stubs": "^108.0.0",
|
|
||||||
"bondage-club-mod-sdk": "^1.2.0",
|
|
||||||
"typescript": "^5.5.2",
|
|
||||||
"xo": "^0.56.0"
|
|
||||||
},
|
|
||||||
"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",
|
|
||||||
"complexity": "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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
31
server.js
31
server.js
|
@ -1,31 +0,0 @@
|
||||||
import {readFileSync} from 'node:fs'
|
|
||||||
import {createServer} from 'node:http'
|
|
||||||
import {argv} from 'node:process'
|
|
||||||
|
|
||||||
const config = {host: '127.0.0.1', port: 9696, filename: argv[2] ?? 'mbchc.mjs'}
|
|
||||||
|
|
||||||
const h_cors = {
|
|
||||||
'Access-Control-Max-Age': '86400',
|
|
||||||
'Access-Control-Allow-Private-Network': 'true',
|
|
||||||
'Access-Control-Allow-Origin': '*',
|
|
||||||
'Access-Control-Allow-Methods': 'GET, OPTIONS',
|
|
||||||
'Access-Control-Allow-Headers': '*',
|
|
||||||
// 'Access-Control-Allow-Credentials': 'false', // omit this header to disallow
|
|
||||||
}
|
|
||||||
const h_all = {
|
|
||||||
'Content-Type': 'text/javascript',
|
|
||||||
'Cache-Control': 'no-cache',
|
|
||||||
...h_cors
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @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) => {
|
|
||||||
const handler = resp[rq.method ?? '']
|
|
||||||
handler !== undefined && (new URL(`http://${config.host}:${config.port}${rq.url}`)).pathname === '/' ? handler(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, () => void console.log(`Server started at http://${config.host}:${config.port} for ${config.filename}`))
|
|
Loading…
Reference in New Issue
Block a user