Skip to content

Commit

Permalink
MAINT: Restructure the way MBS handles the detection of BC and BC logins
Browse files Browse the repository at this point in the history
  • Loading branch information
bananarama92 committed Feb 12, 2025
1 parent 71fa67f commit 36403b0
Show file tree
Hide file tree
Showing 19 changed files with 754 additions and 610 deletions.
14 changes: 8 additions & 6 deletions src/api/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { logger, waitFor } from "../common";
import { settingsMBSLoaded, bcLoaded } from "../common_bc";
import { logger } from "../common";
import { waitForBC, settingsMBSLoaded } from "../common_bc";

import * as wheelOutfits from "./wheel_outfits";
import * as css from "./css";
Expand Down Expand Up @@ -40,8 +40,10 @@ export const getDebug: typeof mbs.getDebug = function getDebug() {
};

// Register the debugger to FUSAM
waitFor(bcLoaded).then(() => {
if (typeof FUSAM !== "undefined" && typeof FUSAM.registerDebugMethod === "function") {
FUSAM.registerDebugMethod("mbs", getDebug);
}
waitForBC("api", {
async afterLogin() {
if (typeof FUSAM !== "undefined" && typeof FUSAM.registerDebugMethod === "function") {
FUSAM.registerDebugMethod("mbs", getDebug);
}
},
});
146 changes: 74 additions & 72 deletions src/backport.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import { sortBy } from "lodash-es";

// @ts-ignore: ignore "variable is declared but never read" warnings; always keep the symbol in accessible
import { MBS_MOD_API } from "./common";
import { waitFor, logger } from "./common";
import { bcLoaded } from "./common_bc";
import { logger } from "./common";
import { waitForBC } from "./common_bc";
import { BC_MIN_VERSION } from "./sanity_checks";

import styles from "./backport.scss";
Expand All @@ -16,79 +16,81 @@ const BC_NEXT = BC_MIN_VERSION + 1;
/** A set with the pull request IDs of all applied bug fix backports */
export const backportIDs: Set<number> = new Set();

waitFor(bcLoaded).then(() => {
switch (GameVersion) {
case "R112":
if (
MBS_MOD_API.getOriginalHash("DialogMenuMapping.items._ReloadStatus") === "F29AC8A4"
&& MBS_MOD_API.getOriginalHash("DialogMenuMapping.items.clickStatusCallbacks.InventoryGroupIsAvailable") === "86642438"
) {
backportIDs.add(5377);
MBS_MOD_API.hookFunction("DialogMenuMapping.items._ReloadStatus", 0, (args) => {
const [root, _status, C, focusGroup, options] = args as never as [HTMLElement, HTMLElement, Character, AssetItemGroup, Pick<DialogMenu.ReloadOptions, "status" | "statusTimer">];
const asset: undefined | Asset = undefined;
let showIcon = false;
let textContent = options.status;
if (textContent == null) {
if (InventoryGroupIsBlockedByOwnerRule(C, focusGroup.Name)) {
textContent = InterfaceTextGet("ZoneBlockedOwner");
showIcon = true;
} else if (InventoryIsBlockedByDistance(C)) {
textContent = InterfaceTextGet("ZoneBlockedRange");
showIcon = true;
} else if (InventoryGroupIsBlocked(C, focusGroup.Name)) {
textContent = InterfaceTextGet("ZoneBlocked");
showIcon = true;
} else if (!Player.CanInteract()) {
textContent = InterfaceTextGet("AccessBlocked");
showIcon = true;
} else {
textContent = InterfaceTextGet("SelectItemGroup");
waitForBC("backport", {
async afterLoad() {
switch (GameVersion) {
case "R112":
if (
MBS_MOD_API.getOriginalHash("DialogMenuMapping.items._ReloadStatus") === "F29AC8A4"
&& MBS_MOD_API.getOriginalHash("DialogMenuMapping.items.clickStatusCallbacks.InventoryGroupIsAvailable") === "86642438"
) {
backportIDs.add(5377);
MBS_MOD_API.hookFunction("DialogMenuMapping.items._ReloadStatus", 0, (args) => {
const [root, _status, C, focusGroup, options] = args as never as [HTMLElement, HTMLElement, Character, AssetItemGroup, Pick<DialogMenu.ReloadOptions, "status" | "statusTimer">];
const asset: undefined | Asset = undefined;
let showIcon = false;
let textContent = options.status;
if (textContent == null) {
if (InventoryGroupIsBlockedByOwnerRule(C, focusGroup.Name)) {
textContent = InterfaceTextGet("ZoneBlockedOwner");
showIcon = true;
} else if (InventoryIsBlockedByDistance(C)) {
textContent = InterfaceTextGet("ZoneBlockedRange");
showIcon = true;
} else if (InventoryGroupIsBlocked(C, focusGroup.Name)) {
textContent = InterfaceTextGet("ZoneBlocked");
showIcon = true;
} else if (!Player.CanInteract()) {
textContent = InterfaceTextGet("AccessBlocked");
showIcon = true;
} else {
textContent = InterfaceTextGet("SelectItemGroup");
}
}
}

root.toggleAttribute("data-show-icon", showIcon);
DialogSetStatus(textContent, options.statusTimer ?? 0, { asset, group: focusGroup, C });
});
MBS_MOD_API.patchFunction("DialogMenuMapping.items.clickStatusCallbacks.InventoryGroupIsAvailable", {
"return equippedItem ? InventoryGroupIsAvailable(C, C.FocusGroup.Name, false) : null;":
"return equippedItem ? InventoryGroupIsAvailable(C, clickedItem.Asset.Group.Name, false) : null;",
});
}
root.toggleAttribute("data-show-icon", showIcon);
DialogSetStatus(textContent, options.statusTimer ?? 0, { asset, group: focusGroup, C });
});
MBS_MOD_API.patchFunction("DialogMenuMapping.items.clickStatusCallbacks.InventoryGroupIsAvailable", {
"return equippedItem ? InventoryGroupIsAvailable(C, C.FocusGroup.Name, false) : null;":
"return equippedItem ? InventoryGroupIsAvailable(C, clickedItem.Asset.Group.Name, false) : null;",
});
}

if (
MBS_MOD_API.getOriginalHash("CharacterSetFacialExpression") === "EC032BEE"
&& MBS_MOD_API.getOriginalHash("DialogLeave") === "AD3A0840"
&& MBS_MOD_API.getOriginalHash("DialogLeaveFocusItemHandlers.DialogFocusItem.Appearance") === "C1F40E3E"
) {
backportIDs.add(5378);
backportIDs.add(5389);
MBS_MOD_API.patchFunction("CharacterSetFacialExpression", {
"CharacterRefresh(C, !inChatRoom && !isTransient);":
"CharacterRefresh(C, !inChatRoom && !isTransient, false);",
});
MBS_MOD_API.patchFunction("DialogLeave", {
"if (CurrentCharacter) {":
"if (StruggleMinigameIsRunning()) { StruggleMinigameStop(); } AudioDialogStop(); Player.FocusGroup = null; if (CurrentCharacter) { CurrentCharacter.FocusGroup = null;",
"DialogChangeFocusToGroup(CurrentCharacter, null);":
";",
});
MBS_MOD_API.patchFunction("DialogLeaveFocusItemHandlers.DialogFocusItem.Appearance", {
"DialogLeave()":
"{ const focusGroup = Player.FocusGroup; DialogLeave(); Player.FocusGroup = focusGroup; }",
});
}
if (
MBS_MOD_API.getOriginalHash("CharacterSetFacialExpression") === "EC032BEE"
&& MBS_MOD_API.getOriginalHash("DialogLeave") === "AD3A0840"
&& MBS_MOD_API.getOriginalHash("DialogLeaveFocusItemHandlers.DialogFocusItem.Appearance") === "C1F40E3E"
) {
backportIDs.add(5378);
backportIDs.add(5389);
MBS_MOD_API.patchFunction("CharacterSetFacialExpression", {
"CharacterRefresh(C, !inChatRoom && !isTransient);":
"CharacterRefresh(C, !inChatRoom && !isTransient, false);",
});
MBS_MOD_API.patchFunction("DialogLeave", {
"if (CurrentCharacter) {":
"if (StruggleMinigameIsRunning()) { StruggleMinigameStop(); } AudioDialogStop(); Player.FocusGroup = null; if (CurrentCharacter) { CurrentCharacter.FocusGroup = null;",
"DialogChangeFocusToGroup(CurrentCharacter, null);":
";",
});
MBS_MOD_API.patchFunction("DialogLeaveFocusItemHandlers.DialogFocusItem.Appearance", {
"DialogLeave()":
"{ const focusGroup = Player.FocusGroup; DialogLeave(); Player.FocusGroup = focusGroup; }",
});
}

if (!document.getElementById("mbs-backport-style")) {
backportIDs.add(5383);
document.body.append(<style id="mbs-backport-style">{styles.toString()}</style>);
}
break;
}
if (!document.getElementById("mbs-backport-style")) {
backportIDs.add(5383);
document.body.append(<style id="mbs-backport-style">{styles.toString()}</style>);
}
break;
}

if (backportIDs.size) {
logger.log(`Initializing R${BC_NEXT} backports`, sortBy(Array.from(backportIDs)));
} else {
logger.log(`No R${BC_NEXT} backports`);
}
if (backportIDs.size) {
logger.log(`Initializing R${BC_NEXT} backports`, sortBy(Array.from(backportIDs)));
} else {
logger.log(`No R${BC_NEXT} backports`);
}
},
});
170 changes: 2 additions & 168 deletions src/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
import { range, sample } from "lodash-es";
import bcModSdk from "bondage-club-mod-sdk";

import { isArray, includes, keys, entries, fromEntries, logger, waitFor, isInteger } from "./lpgl/common";
import { isArray, includes, keys, entries, fromEntries, logger, isInteger, Version, validateInt, toStringTemplate } from "./lpgl/common";

export { isArray, includes, keys, entries, fromEntries, logger, waitFor, isInteger };
export { isArray, includes, keys, entries, fromEntries, logger, isInteger, Version, validateInt, toStringTemplate };

/** An array with all alpha-numerical characters. */
const ALPHABET = Object.freeze([
Expand All @@ -18,33 +18,6 @@ const ALPHABET = Object.freeze([
"Y", "Z",
]);

/** Regular expression for the MBS version */
const MBS_VERSION_PATTERN = /^(v?)([0-9]+)\.([0-9]+)\.([0-9]+)(\.\S+)?$/;

/**
* Check whether an integer falls within the specified range and raise otherwise.
* @param int The to-be validate integer
* @param varName The name of the variable
* @param min The minimum allowed value of the integer, defaults to {@link Number.MIN_SAFE_INTEGER}
* @param max The maximum allowed value of the integer, defaults to {@link Number.MAX_SAFE_INTEGER}
*/
export function validateInt(
int: number,
varName: string,
min: number = Number.MIN_SAFE_INTEGER,
max: number = Number.MAX_SAFE_INTEGER,
): void {
if (!(Number.isInteger(int) && int >= min && int <= max)) {
if (typeof int !== "number") {
throw new TypeError(`Invalid "${varName}" type: ${typeof int}`);
} else if (!Number.isInteger(int)) {
throw new Error(`"${varName}" must be an integer: ${int}`);
} else {
throw new RangeError(`"${varName}" must fall in the [${min}, ${max}] interval: ${int}`);
}
}
}

/**
* Pad an array with a given value to a given length if necessary.
* Performs an inplace update of the passed array.
Expand Down Expand Up @@ -121,19 +94,6 @@ class ModAPIProxy implements BCX_ModAPI {
/** A lazily-loaded version of the BCX mod API */
export const BCX_MOD_API = new ModAPIProxy();

/**
* Helper function for creating {@link Object.prototype.toString} methods.
* @param typeName The name of the object
* @param obj The object
* @returns A stringified version of the passed `obj`
*/
export function toStringTemplate(typeName: string, obj: object): string {
let ret = `${typeName}(`;
ret += Object.values(obj).map(i => String(i)).join(", ");
ret += ")";
return ret;
}

export class LoopIterator<T> {
/** The iterator's underlying array. */
readonly #list: readonly T[];
Expand Down Expand Up @@ -254,129 +214,3 @@ export function generateIDs(
export function isIterable(obj: unknown): obj is Iterable<unknown> {
return (Symbol.iterator in Object(obj));
}

/** A class for representing semantic versions */
export class Version {
/** The major semantic version */
readonly major: number;
/** The minor semantic version */
readonly minor: number;
/** The micro semantic version */
readonly micro: number;
/** Whether this concerns a beta version or not */
readonly beta: boolean;
/** Length-4 UTF16 string encoding the major, minor, micro & beta components of the version. */
readonly #string: string;

constructor(major: number = 0, minor: number = 0, micro: number = 0, beta: boolean = false) {
validateInt(major, "major", 0, 2**16 - 1);
validateInt(minor, "minor", 0, 2**16 - 1);
validateInt(micro, "micro", 0, 2**16 - 1);
if (typeof beta !== "boolean") {
throw new TypeError(`Invalid "beta" type: ${typeof beta}`);
}

this.major = major;
this.minor = minor;
this.micro = micro;
this.beta = beta;
this.#string = [major, minor, micro].map(i => String.fromCharCode(i)) + String.fromCharCode(beta ? 0 : 1);
Object.freeze(this);
}

/**
* Check whether two versions are equal
* @param other The to-be compared version
* @returns Whether `this` and `other` are the same version
*/
equal(other: Version): boolean {
return other instanceof Version ? this.valueOf() === other.valueOf() : false;
}

/**
* Check whether this version is greater than the other
* @param other The to-be compared version
* @returns Whether the version of `this` is greater than `other`
*/
greater(other: Version): boolean {
return other instanceof Version ? this.valueOf() > other.valueOf() : false;
}

/**
* Check whether this version is lesser than the other
* @param other The to-be compared version
* @returns Whether the version of `this` is lesser than `other`
*/
lesser(other: Version): boolean {
return other instanceof Version ? this.valueOf() < other.valueOf() : false;
}

/**
* Check whether this version is greater than or equal to the other
* @param other The to-be compared version
* @returns Whether the version of `this` is greater than or equal to `other`
*/
greaterOrEqual(other: Version): boolean {
return other instanceof Version ? this.valueOf() >= other.valueOf() : false;
}

/**
* Check whether this version is lesser than or equal to the other
* @param other The to-be compared version
* @returns Whether the version of `this` is lesser than or equal to `other`
*/
lesserOrEqual(other: Version): boolean {
return other instanceof Version ? this.valueOf() <= other.valueOf() : false;
}

/**
* Construct a new instance from the passed version string
* @param version The to-be parsed version
* @returns A new {@link Version} instance
*/
static fromVersion(version: string): Version {
const match = MBS_VERSION_PATTERN.exec(version);
if (match === null) {
throw new Error(`Invalid "version": ${version}`);
}
return new Version(
Number(match[2]),
Number(match[3]),
Number(match[4]),
(match[5] !== undefined),
);
}

/**
* Construct a new instance from the passed BC version string
* @param version The to-be parsed BC version
* @returns A new {@link Version} instance
*/
static fromBCVersion(version: string): Version {
const match = GameVersionFormat.exec(version);
if (match === null) {
throw new Error(`Invalid BC version: "${version}"`);
}
return new Version(Number(match[1]), 0, 0, match[2] !== undefined);
}

/** Return a string representation of this instance. */
toString(): string {
return toStringTemplate(typeof this, this.toJSON());
}

/** Return a length-4 UTF16 string encoding the major, minor, micro & beta components of the version */
valueOf(): string {
return this.#string;
}

/** Return an object representation of this instance. */
toJSON() {
return {
major: this.major,
minor: this.minor,
micro: this.micro,
beta: this.beta,
};
}
}
Loading

0 comments on commit 36403b0

Please sign in to comment.