Show local co-op icon in game card

This commit is contained in:
redphx
2025-01-02 21:39:27 +07:00
parent 9862f794cf
commit 90f89a0244
14 changed files with 287 additions and 14 deletions

View File

@@ -0,0 +1,7 @@
<svg xmlns='http://www.w3.org/2000/svg' width='1em' height='1em' viewBox='0 0 32 32' fill-rule='evenodd' stroke-linecap='round' stroke-linejoin='round'>
<g>
<path d='M24.272 11.165h-3.294l-3.14 3.564c-.391.391-.922.611-1.476.611a2.1 2.1 0 0 1-2.087-2.088 2.09 2.09 0 0 1 .031-.362l1.22-6.274a3.89 3.89 0 0 1 3.81-3.206h6.57c1.834 0 3.439 1.573 3.833 3.295l1.205 6.185a2.09 2.09 0 0 1 .031.362 2.1 2.1 0 0 1-2.087 2.088c-.554 0-1.085-.22-1.476-.611l-3.14-3.564' fill='none' stroke='#fff' stroke-width='2'/>
<circle cx='22.625' cy='5.874' r='.879'/><path d='M11.022 24.415H7.728l-3.14 3.564c-.391.391-.922.611-1.476.611a2.1 2.1 0 0 1-2.087-2.088 2.09 2.09 0 0 1 .031-.362l1.22-6.274a3.89 3.89 0 0 1 3.81-3.206h6.57c1.834 0 3.439 1.573 3.833 3.295l1.205 6.185a2.09 2.09 0 0 1 .031.362 2.1 2.1 0 0 1-2.087 2.088c-.554 0-1.085-.22-1.476-.611l-3.14-3.564' fill='none' stroke='#fff' stroke-width='2'/>
<circle cx='9.375' cy='19.124' r='.879'/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 981 B

View File

@@ -12,6 +12,7 @@ export const enum StorageKey {
GH_PAGES_COMMIT_HASH = 'BetterXcloud.GhPages.CommitHash',
LIST_CUSTOM_TOUCH_LAYOUTS = 'BetterXcloud.GhPages.CustomTouchLayouts',
LIST_FORCE_NATIVE_MKB = 'BetterXcloud.GhPages.ForceNativeMkb',
LIST_LOCAL_CO_OP = 'BetterXcloud.GhPages.LocalCoOp',
}

View File

@@ -50,4 +50,50 @@ export class PatcherUtils {
return str;
}
private static isVarCharacter(char: string) {
const code = char.charCodeAt(0);
// Check for uppercase letters (A-Z)
const isUppercase = code >= 65 && code <= 90;
// Check for lowercase letters (a-z)
const isLowercase = code >= 97 && code <= 122;
// Check for digits (0-9)
const isDigit = code >= 48 && code <= 57;
// Check for special characters '_' and '$'
const isSpecial = char === '_' || char === '$';
return isUppercase || isLowercase || isDigit || isSpecial;
}
static getVariableNameBefore(str: string, index: number) {
if (index < 0) {
return null;
}
const end = index;
let start = end - 1;
while (PatcherUtils.isVarCharacter(str[start])) {
start -= 1;
}
return str.substring(start + 1, end);
}
static getVariableNameAfter(str: string, index: number) {
if (index < 0) {
return null;
}
const start = index;
let end = start + 1;
while (PatcherUtils.isVarCharacter(str[end])) {
end += 1;
}
return str.substring(start, end);
}
}

View File

@@ -7,6 +7,7 @@ import { BxEvent } from "@/utils/bx-event";
import codeControllerCustomization from "./patches/controller-customization.js" with { type: "text" };
import codePollGamepad from "./patches/poll-gamepad.js" with { type: "text" };
import codeExposeStreamSession from "./patches/expose-stream-session.js" with { type: "text" };
import codeGameCardIcons from "./patches/game-card-icons.js" with { type: "text" };
import codeLocalCoOpEnable from "./patches/local-co-op-enable.js" with { type: "text" };
import codeRemotePlayKeepAlive from "./patches/remote-play-keep-alive.js" with { type: "text" };
import codeVibrationAdjust from "./patches/vibration-adjust.js" with { type: "text" };
@@ -1003,6 +1004,56 @@ ${subsVar} = subs;
str = PatcherUtils.insertAt(str, index, newCode);
return str;
},
exposeReactCreateComponent(str: string) {
let index = str.indexOf('.prototype.isReactComponent={}');
index > -1 && (index = PatcherUtils.indexOf(str, '.createElement=', index));
if (index < 0) {
return false;
}
const newCode = 'window.BX_EXPOSED.reactCreateElement=';
str = PatcherUtils.insertAt(str, index - 1, newCode);
return str;
},
// 27.0.6-hotfix.1, 73704.js
gameCardCustomIcons(str: string) {
let initialIndex = str.indexOf('const{supportedInputIcons:');
if (initialIndex < 0) {
return false;
}
const returnIndex = PatcherUtils.lastIndexOf(str, 'return ', str.indexOf('SupportedInputsBadge'));
if (returnIndex < 0) {
return false;
}
// Find function's parameter
const arrowIndex = PatcherUtils.lastIndexOf(str, '=>{', initialIndex, 300);
if (arrowIndex < 0) {
return false;
}
const paramVar = PatcherUtils.getVariableNameBefore(str, arrowIndex);
// Find supportedInputIcons and title var names
const supportedInputIconsVar = PatcherUtils.getVariableNameAfter(str, PatcherUtils.indexOf(str, 'supportedInputIcons:', initialIndex, 100, true));
if (!paramVar || !supportedInputIconsVar) {
return false;
}
const newCode = renderString(codeGameCardIcons, {
param: paramVar,
supportedInputIcons: supportedInputIconsVar,
});
str = PatcherUtils.insertAt(str, returnIndex, newCode);
return str;
},
};
let PATCH_ORDERS = PatcherUtils.filterPatches([
@@ -1012,6 +1063,9 @@ let PATCH_ORDERS = PatcherUtils.filterPatches([
'disableAbsoluteMouse',
] : []),
'exposeReactCreateComponent',
'gameCardCustomIcons',
'modifyPreloadedState',
'optimizeGameSlugGenerator',

View File

@@ -0,0 +1,9 @@
declare const $supportedInputIcons$: Array<any>;
declare const $param$: { productId: string };
const supportedInputIcons = $supportedInputIcons$;
const { productId } = $param$;
if (window.BX_EXPOSED.localCoOpManager.isSupported(productId)) {
supportedInputIcons.push(() => window.BX_EXPOSED.createReactLocalCoOpIcon());
}

View File

@@ -31,6 +31,12 @@ type ScriptEvents = {
data: any;
};
};
'list.localCoOp.updated': {
data: {
data: any;
};
};
};
type StreamEvents = {

View File

@@ -13,6 +13,7 @@ import { NativeMkbMode, TouchControllerMode } from "@/enums/pref-values";
import { Patcher, type PatchPage } from "@/modules/patcher/patcher";
import { BxEventBus } from "./bx-event-bus";
import { FeatureGates } from "./feature-gates";
import { LocalCoOpManager } from "./local-co-op-manager";
export enum SupportedInputType {
CONTROLLER = 'Controller',
@@ -230,4 +231,25 @@ export const BxExposed = {
BxLogger.info('beforePageLoad', page);
Patcher.patchPage(page);
} : () => {},
localCoOpManager: LocalCoOpManager.getInstance(),
reactCreateElement: function(...args: any[]) {},
createReactLocalCoOpIcon: isFullVersion() ? (): any => {
const reactCE = window.BX_EXPOSED.reactCreateElement;
// local-co-op.svg
return reactCE(
'svg',
{ xmlns: 'http://www.w3.org/2000/svg', width: '1em', height: '1em', viewBox: '0 0 32 32', 'fill-rule': 'evenodd', 'stroke-linecap': 'round', 'stroke-linejoin': 'round' },
reactCE(
'g',
null,
reactCE('path', { d: 'M24.272 11.165h-3.294l-3.14 3.564c-.391.391-.922.611-1.476.611a2.1 2.1 0 0 1-2.087-2.088 2.09 2.09 0 0 1 .031-.362l1.22-6.274a3.89 3.89 0 0 1 3.81-3.206h6.57c1.834 0 3.439 1.573 3.833 3.295l1.205 6.185a2.09 2.09 0 0 1 .031.362 2.1 2.1 0 0 1-2.087 2.088c-.554 0-1.085-.22-1.476-.611l-3.14-3.564', fill: 'none', stroke: '#fff', 'stroke-width': '2' }),
reactCE('circle', { cx: '22.625', cy: '5.874', r: '.879' }),
reactCE('path', { d: 'M11.022 24.415H7.728l-3.14 3.564c-.391.391-.922.611-1.476.611a2.1 2.1 0 0 1-2.087-2.088 2.09 2.09 0 0 1 .031-.362l1.22-6.274a3.89 3.89 0 0 1 3.81-3.206h6.57c1.834 0 3.439 1.573 3.833 3.295l1.205 6.185a2.09 2.09 0 0 1 .031.362 2.1 2.1 0 0 1-2.087 2.088c-.554 0-1.085-.22-1.476-.611l-3.14-3.564', fill: 'none', stroke: '#fff', 'stroke-width': '2' }),
reactCE('circle', { cx: '9.375', cy: '19.124', r: '.879' })
),
);
} : () => {},
};

View File

@@ -83,4 +83,24 @@ export class GhPagesUtils {
const customList = JSON.parse(window.localStorage.getItem(key) || '[]');
return customList;
}
static getLocalCoOpList() {
const supportedSchema = 1;
const key = StorageKey.LIST_LOCAL_CO_OP;
NATIVE_FETCH(GhPagesUtils.getUrl('local-co-op/ids.json'))
.then(response => response.json())
.then(json => {
if (json.$schemaVersion === supportedSchema) {
window.localStorage.setItem(key, JSON.stringify(json));
BxEventBus.Script.emit('list.localCoOp.updated', { data: json });
} else {
window.localStorage.removeItem(key);
BxEventBus.Script.emit('list.localCoOp.updated', { data: { data: {} } });
}
});
const customList = JSON.parse(window.localStorage.getItem(key) || '[]');
return customList;
}
}

View File

@@ -0,0 +1,21 @@
import { BxEventBus } from "./bx-event-bus";
import { GhPagesUtils } from "./gh-pages";
export class LocalCoOpManager {
private static instance: LocalCoOpManager;
public static getInstance = () => LocalCoOpManager.instance ?? (LocalCoOpManager.instance = new LocalCoOpManager());
private supportedIds: string[] = [];
constructor() {
BxEventBus.Script.once('list.localCoOp.updated', e => {
this.supportedIds = Object.keys(e.data.data);
console.log('supportedIds', this.supportedIds);
});
GhPagesUtils.getLocalCoOpList();
}
isSupported(productId: string) {
return this.supportedIds.includes(productId);
}
}

View File

@@ -436,7 +436,7 @@ export class GlobalSettingsStorage extends BaseSettingsStorage {
if (!setting.unsupported) {
(setting as any).multipleOptions = GhPagesUtils.getNativeMkbCustomList(true);
BxEventBus.Script.on('list.forcedNativeMkb.updated', payload => {
BxEventBus.Script.once('list.forcedNativeMkb.updated', payload => {
(setting as any).multipleOptions = payload.data.data;
});
}