Initial commit: Client Doc docs Server Tools

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
ud18010
2026-07-10 10:24:29 +08:00
co-authored by Cursor
commit 7e35d8da31
3374 changed files with 680813 additions and 0 deletions
@@ -0,0 +1,66 @@
export default function fixCmapTable(arrayBuffer) {
const font = new DataView(arrayBuffer);
const tableCount = font.getUint16(4);
let cmapOffset = 0;
let cmapLength = 0;
let cmapCheckSumOffset = 0;
let cmapCheckSum = 0;
for (let i = 0; i < tableCount; i++) {
const tag = font.getUint32(12 + i * 16);
if (tag === 0x636D6170) {
cmapCheckSumOffset = 12 + i * 16 + 4;
cmapCheckSum = font.getUint32(cmapCheckSumOffset);
cmapOffset = font.getUint32(12 + i * 16 + 8);
cmapLength = font.getUint32(12 + i * 16 + 12);
GameGlobal.manager.Logger.pluginLog(`[font]cmapCheckSubOffset [${cmapCheckSumOffset}], cmapCheckSum [${cmapCheckSum}], cmapOffset [${cmapOffset}], cmapLength [${cmapLength}]`);
}
}
if (cmapOffset === 0) {
GameGlobal.manager.Logger.pluginError('[font]not found cmap');
return false;
}
const cmap = new DataView(arrayBuffer, cmapOffset, cmapLength);
const numTables = cmap.getUint16(2);
let subtableOffset = 4;
let targetSubtableOffset = 0;
for (let i = 0; i < numTables; i++) {
const platformId = cmap.getUint16(subtableOffset);
const encodingId = cmap.getUint16(subtableOffset + 2);
if (platformId === 0 && encodingId === 5) {
if (i === (numTables - 1)) {
targetSubtableOffset = subtableOffset;
GameGlobal.manager.Logger.pluginLog(`[font]targetSubtableOffset ${targetSubtableOffset}`);
}
break;
}
subtableOffset += 8;
}
if (targetSubtableOffset > 0) {
const newCmapView = new DataView(arrayBuffer, cmapOffset, cmapLength - 8);
newCmapView.setUint16(2, numTables - 1);
let sum = 0;
const lengthInUint32 = (newCmapView.byteLength + 3) / 4;
for (let i = 0; i < lengthInUint32; i++) {
sum += newCmapView.getUint32(i);
}
font.setUint32(cmapCheckSumOffset, sum);
return true;
}
GameGlobal.manager.Logger.pluginLog('[font]not found cmap subtable');
return false;
}
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: bb3a71ed8ecb9ac4486138760082906e
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,99 @@
import moduleHelper from '../module-helper';
import { formatJsonStr } from '../utils';
import fixCmapTable from './fix-cmap';
import readMetrics from './read-metrics';
import splitTTCToBufferOnlySC from './split-sc';
const { platform } = wx.getSystemInfoSync();
const tempCacheObj = {};
let fontDataCache;
let getFontPromise;
let isReadFromCache = false;
const isIOS = platform === 'ios';
const isAndroid = platform === 'android';
function handleGetFontData(config, forceLoad = false) {
const canGetWxCommonFont = !!GameGlobal.manager?.font?.getCommonFont;
if (!config && !canGetWxCommonFont) {
return Promise.reject('invalid usage');
}
if (!getFontPromise || forceLoad) {
getFontPromise = new Promise((resolve, reject) => {
if (!canGetWxCommonFont && !!config) {
const xhr = new GameGlobal.unityNamespace.UnityLoader.UnityCache.XMLHttpRequest();
xhr.open('GET', config.fallbackUrl, true);
xhr.responseType = 'arraybuffer';
xhr.onload = () => {
if ((xhr.status === 200 || xhr.status === 0) && xhr.response) {
const notoFontData = xhr.response;
fontDataCache = notoFontData;
isReadFromCache = xhr.isReadFromCache;
resolve();
}
};
xhr.onerror = reject;
xhr.send();
return;
}
GameGlobal.manager.font.getCommonFont({
success(fontData) {
if (isIOS) {
fixCmapTable(fontData);
}
if (isAndroid) {
const tempData = splitTTCToBufferOnlySC(fontData);
if (tempData) {
fontData = tempData;
}
}
fontDataCache = fontData;
resolve();
},
fail: reject,
});
});
}
return getFontPromise;
}
function WXGetFontRawData(conf, callbackId) {
const config = formatJsonStr(conf);
const loadFromRemote = !GameGlobal.manager?.font?.getCommonFont;
GameGlobal.manager.TimeLogger.timeStart('WXGetFontRawData');
handleGetFontData(config).then(() => {
if (fontDataCache) {
GameGlobal.manager.font.reportGetFontCost(GameGlobal.manager.TimeLogger.timeEnd('WXGetFontRawData'), { loadFromRemote, isReadFromCache, preloadWXFont: GameGlobal.unityNamespace.preloadWXFont });
const { ascent, descent, lineGap, unitsPerEm } = readMetrics(fontDataCache) || {};
tempCacheObj[callbackId] = fontDataCache;
moduleHelper.send('GetFontRawDataCallback', JSON.stringify({ callbackId, type: 'success', res: JSON.stringify({ byteLength: fontDataCache.byteLength, ascent, descent, lineGap, unitsPerEm }) }));
GameGlobal.manager.Logger.pluginLog(`[font] load font from ${loadFromRemote ? `network, url=${config.fallbackUrl}` : 'local'}`);
fontDataCache = null;
}
else {
GameGlobal.manager.Logger.pluginError('[font] load font error: empty content');
}
})
.catch((err) => {
GameGlobal.manager.Logger.pluginError('[font] load font error: ', err);
});
}
function WXShareFontBuffer(buffer, offset, callbackId) {
if (typeof tempCacheObj[callbackId] === 'string') {
GameGlobal.manager.Logger.pluginError('[font]内存写入异常');
}
buffer.set(new Uint8Array(tempCacheObj[callbackId]), offset);
delete tempCacheObj[callbackId];
}
export function preloadWxCommonFont() {
if (!!GameGlobal.unityNamespace.preloadWXFont && !!GameGlobal.manager?.font?.getCommonFont) {
handleGetFontData();
}
}
export default {
WXGetFontRawData,
WXShareFontBuffer,
};
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 0ae59ac9cad5d0341ae29f64c79fb367
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,41 @@
import { toBytesInt32 } from './util';
export default function readMetrics(arrayBuffer) {
const font = new DataView(arrayBuffer);
const tableCount = font.getUint16(4);
const ppem = 1;
let ascent = 0;
let descent = 0;
let lineGap = 0;
let unitsPerEm;
for (let i = 0; i < tableCount; i++) {
const tag = font.getUint32(12 + i * 16);
const tagStr = toBytesInt32(tag);
if (tagStr === 'hhea') {
const offset = font.getUint32(12 + i * 16 + 8);
const length = font.getUint32(12 + i * 16 + 12);
const hhea = new DataView(arrayBuffer, offset, length);
ascent = hhea.getInt16(4);
descent = hhea.getInt16(6);
lineGap = hhea.getInt16(8);
}
else if (tagStr === 'head') {
const offset = font.getUint32(12 + i * 16 + 8);
const length = font.getUint32(12 + i * 16 + 12);
const head = new DataView(arrayBuffer, offset, length);
unitsPerEm = head.getUint16(18);
}
}
if (!ascent || !descent || !unitsPerEm) {
return undefined;
}
return {
ascent: ascent * ppem / unitsPerEm,
descent: descent * ppem / unitsPerEm,
lineGap: lineGap * ppem / unitsPerEm,
unitsPerEm,
};
}
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: ba0614c832510aa43b86dd9a8356abc9
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,140 @@
import { ceil4, toBytesInt32, decodeUnicode } from './util';
function extractTTF(ttcView, tableHeaderOffset) {
const subFontTableCount = ttcView.getUint16(tableHeaderOffset + 0x04);
const subFontHeaderLength = 0x0C + subFontTableCount * 0x10;
let tableLength = 0;
for (let j = 0; j < subFontTableCount; j++) {
const length = ttcView.getUint32(tableHeaderOffset + 0x0C + 0x0C + j * 0x10);
tableLength += ceil4(length);
}
const totalLength = subFontHeaderLength + tableLength;
const newBuf = new ArrayBuffer(totalLength);
const newBufUint = new Uint8Array(newBuf);
const newBufData = new DataView(newBuf);
newBufUint.set(new Uint8Array(ttcView.buffer, tableHeaderOffset, subFontHeaderLength), 0);
let currentOffset = subFontHeaderLength;
for (let j = 0; j < subFontTableCount; j++) {
const offset = ttcView.getUint32(tableHeaderOffset + 0x0C + 0x08 + j * 0x10);
const length = ttcView.getUint32(tableHeaderOffset + 0x0C + 0x0C + j * 0x10);
newBufData.setUint32(0x0C + 0x08 + j * 0x10, currentOffset);
newBufUint.set(new Uint8Array(ttcView.buffer, offset, length), currentOffset);
currentOffset += ceil4(length);
}
return newBufData;
}
function parseTableToDataView(fontDataView, tableName, startOffset = 0) {
const font = fontDataView;
const tableCount = font.getUint16(startOffset + 4);
for (let i = 0; i < tableCount; i++) {
const tag = font.getUint32(startOffset + 12 + i * 16);
const tagStr = toBytesInt32(tag);
if (tagStr === tableName) {
const offset = font.getUint32(startOffset + 12 + i * 16 + 8);
const length = font.getUint32(startOffset + 12 + i * 16 + 12);
return new DataView(fontDataView.buffer, offset, length);
}
}
GameGlobal.manager.Logger.pluginError(`\tTable#${tableName} not found in DataView`);
return undefined;
}
function parseNameTable(fontDataView, startOffset = 0) {
const nameTable = parseTableToDataView(fontDataView, 'name', startOffset);
if (!nameTable) {
return undefined;
}
const result = {};
result.data = nameTable;
result.format = nameTable.getUint16(0);
result.count = nameTable.getUint16(2);
result.stringOffset = nameTable.getUint16(4);
const nameRecords = [];
for (let i = 0; i < result.count; i++) {
const offset = 6 + i * 12;
nameRecords.push({
platformID: nameTable.getUint16(offset),
platformSpecificID: nameTable.getUint16(offset + 2),
languageID: nameTable.getUint16(offset + 4),
nameID: nameTable.getUint16(offset + 6),
length: nameTable.getUint16(offset + 8),
offset: nameTable.getUint16(offset + 10),
});
}
result.nameRecords = nameRecords;
return result;
}
function parseFamilyName(fontDataView, startOffset = 0) {
const nameTable = parseNameTable(fontDataView, startOffset);
if (!nameTable) {
return undefined;
}
if (nameTable.nameRecords) {
for (const record of nameTable.nameRecords) {
const { nameID } = record;
if (nameID === 1) {
const { offset } = record;
const byteLength = record.length;
return decodeUnicode(fontDataView.buffer, (nameTable.data?.byteOffset || 0) + (nameTable.stringOffset || 0) + offset, byteLength);
}
}
}
return undefined;
}
export default function splitTTCToBufferOnlySC(arrayBuffer) {
const ttc = new DataView(arrayBuffer);
const tag = ttc.getUint32(0);
if (toBytesInt32(tag) !== 'ttcf') {
GameGlobal.manager.Logger.pluginError('input not a valid ttc file');
return undefined;
}
const ttfCount = ttc.getInt32(8);
let fontSCHeaderOffset = undefined;
const reg = /S\0?C/;
for (let i = 0; i < ttfCount; i++) {
const tableHeaderOffset = ttc.getUint32(0x0C + i * 4);
const familyName = parseFamilyName(ttc, tableHeaderOffset);
if (typeof familyName === 'string' && reg.test(familyName)) {
fontSCHeaderOffset = tableHeaderOffset;
break;
}
}
if (!fontSCHeaderOffset) {
GameGlobal.manager.Logger.pluginError('SC Font not found in TTC File.');
return undefined;
}
return extractTTF(ttc, fontSCHeaderOffset).buffer;
}
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 2f94e57070564964cb628dc0d1d597a5
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,17 @@
export function toBytesInt32(num) {
let ascii = '';
for (let i = 3; i >= 0; i--) {
ascii += String.fromCharCode((num >> (8 * i)) & 255);
}
return ascii;
}
;
export function ceil4(n) {
return (n + 3) & ~3;
}
export function decodeUnicode(buffer, byteOffset, byteLength) {
const dataview = new Uint8Array(buffer, byteOffset, byteLength);
const ret = String.fromCharCode.apply(null, Array.from(dataview));
return ret;
}
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 905521993cef08345ac22af6b193656b
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant: