Update few more modules

This commit is contained in:
2026-03-25 23:19:03 +01:00
parent f27f6df525
commit 04b10efbfc
16 changed files with 568 additions and 233 deletions
+2 -2
View File
@@ -1,6 +1,5 @@
import { compact, escapeRegExp, repeat } from 'lodash';
import { createBinaryReader, readUint8, readUint32, readUint16 } from 'ag-sockets/dist/browser';
import { decodeString } from 'ag-sockets/dist/utf8';
import {
Entity, Region, Pony, EntityState, PonyOptions, MessageType, isNonIgnorableMessage, EntityPlayerState,
EntityOrPonyOptions, isPublicMessage, FakeEntity, Action, WorldMap, DoAction, UpdateType, DecodedUpdate,
@@ -36,6 +35,7 @@ import { compareFriends } from '../components/services/model';
import { canCollideWith } from '../common/collision';
import { hasDrawLight, hasLightSprite } from './draw';
import { setTile } from '../common/tileUtils';
import { decodeStringFromUint8Array } from '../common/binaryUtils';
function log(message: string) {
if (DEVELOPMENT && !TESTS) {
@@ -180,7 +180,7 @@ export function handleUpdateEntity(game: PonyTownGame, update: DecodedUpdate) {
export function handleUpdatePonies(game: PonyTownGame, ponies: PonyData[]) {
for (const [id, options = {}, name, info, playerState, nameBad] of ponies) {
const decodedName = name && decodeString(name) || undefined;
const decodedName = name && decodeStringFromUint8Array(name) || undefined;
const filteredName = filterEntityName(game, decodedName, nameBad);
const decodedInfo = info ? bitmask(info, PONY_INFO_KEY) : '';
const pony = createPonyEntity(game, id, options, filteredName, decodedInfo, EntityState.None);
+153 -1
View File
@@ -1,5 +1,7 @@
import { BinaryWriter, getWriterBuffer, createBinaryWriter, resizeWriter } from 'ag-sockets/dist/browser';
import { BinaryWriter, getWriterBuffer, createBinaryWriter, resizeWriter, readUint8Array, BinaryReader } from 'ag-sockets/dist/browser';
import { isDataViewError } from './utils';
import { encodeStringTo } from 'ag-sockets/dist/utf8';
import { getLength } from 'ag-sockets/dist/utils';
export function writeBinary(write: (writer: BinaryWriter) => void): Uint8Array {
const writer = createBinaryWriter();
@@ -19,3 +21,153 @@ export function writeBinary(write: (writer: BinaryWriter) => void): Uint8Array {
return getWriterBuffer(writer);
}
export function decodeString(value: DataView | null, offset: number, length: number): string | null {
if (value == null) return null;
let result = '';
const end = offset + length;
for (let i = offset; i < end;) {
const byte1 = value.getUint8(i++);
let code: number;
if ((byte1 & 0x80) === 0) {
code = byte1;
} else if ((byte1 & 0xe0) === 0xc0) {
const byte2 = continuationByte(value, i++, end);
code = ((byte1 & 0x1f) << 6) | byte2;
if (code < 0x80) {
throw Error('Invalid continuation byte');
}
} else if ((byte1 & 0xf0) === 0xe0) {
const byte2 = continuationByte(value, i++, end);
const byte3 = continuationByte(value, i++, end);
code = ((byte1 & 0x0f) << 12) | (byte2 << 6) | byte3;
if (code < 0x0800) {
throw Error('Invalid continuation byte');
}
if (code >= 0xd800 && code <= 0xdfff) {
throw Error(`Lone surrogate U+${code.toString(16).toUpperCase()} is not a scalar value`);
}
} else if ((byte1 & 0xf8) === 0xf0) {
const byte2 = continuationByte(value, i++, end);
const byte3 = continuationByte(value, i++, end);
const byte4 = continuationByte(value, i++, end);
code = ((byte1 & 0x0f) << 0x12) | (byte2 << 0x0c) | (byte3 << 0x06) | byte4;
if (code < 0x010000 || code > 0x10ffff) {
throw Error('Invalid continuation byte');
}
} else {
throw Error('Invalid UTF-8 detected');
}
if (code > 0xffff) {
code -= 0x10000;
result += String.fromCharCode(code >>> 10 & 0x3ff | 0xd800);
code = 0xdc00 | code & 0x3ff;
}
result += String.fromCharCode(code);
}
return result;
}
function continuationByte(buffer: DataView, index: number, end: number): number {
if (index >= end) throw Error('Invalid byte index');
const continuationByte = buffer.getUint8(index);
if ((continuationByte & 0xC0) === 0x80) {
return continuationByte & 0x3F;
} else {
throw Error('Invalid continuation byte');
}
}
export function encodeString(string?: string | null) {
const writer = createBinaryWriter(getStringLengthWithLength(string));
if (string != null) {
writeStringValue(writer, string);
}
return getWriterBuffer(writer);
}
export function getStringLengthWithLength(value?: string | null) {
if (value == null)
return 1;
const len = stringLengthInBytes(value);
return getLength(len) + len;
}
export function writeStringValue(writer: BinaryWriter, value: string) {
writer.offset = encodeStringTo(writer.view, writer.offset, value);
if (writer.offset > writer.view.byteLength) {
throw new Error('Exceeded DataView size');
}
}
function stringLengthInBytes(value: string): number {
let result = 0;
forEachCharacter(value, code => result += charLengthInBytes(code));
return result;
}
function forEachCharacter(value: string, callback: (code: number) => void) {
for (let i = 0; i < value.length; i++) {
const code = value.charCodeAt(i);
// high surrogate
if (code >= 0xd800 && code <= 0xdbff) {
if ((i + 1) < value.length) {
const extra = value.charCodeAt(i + 1);
// low surrogate
if ((extra & 0xfc00) === 0xdc00) {
i++;
callback(((code & 0x3ff) << 10) + (extra & 0x3ff) + 0x10000);
}
}
} else {
callback(code);
}
}
}
function charLengthInBytes(code: number): number {
if ((code & 0xffffff80) === 0) {
return 1;
} else if ((code & 0xfffff800) === 0) {
return 2;
} else if ((code & 0xffff0000) === 0) {
return 3;
} else {
return 4;
}
}
export function resizeWriterWithData(writer: BinaryWriter) {
const size = writer.view.byteLength;
const originalBuffer = new Uint8Array(writer.view.buffer);
const resizedBuffer = new Uint8Array(size * 2);
for (let i = 0; i < originalBuffer.byteLength; i++) {
resizedBuffer[i] = originalBuffer[i];
}
const resizedDataView = new DataView(resizedBuffer.buffer);
writer.view = resizedDataView;
}
export function decodeStringFromBuffer(reader: BinaryReader) {
return decodeStringFromUint8Array(readUint8Array(reader));
}
export function decodeStringFromUint8Array(value: Uint8Array | null) {
const view = value ? new DataView(value.buffer, value.byteOffset, value.byteLength) : null;
return decodeString(view, 0, view ? view.byteLength : 0);
}
+2 -2
View File
@@ -2,9 +2,9 @@ import {
BinaryWriter, BinaryReader, writeInt16, readInt16, createBinaryReader, readUint16, readLength,
readUint32, readUint8, readObject, readUint8Array
} from 'ag-sockets/dist/browser';
import { decodeString } from 'ag-sockets/dist/utf8';
import { DecodedUpdate, DecodedRegionUpdate, TileUpdate, UpdateFlags } from '../interfaces';
import { tileWidth, tileHeight, MAX_VELOCITY } from '../constants';
import { decodeStringFromUint8Array } from '../binaryUtils';
export function writeVelocity(writer: BinaryWriter, value: number) {
if (value >= MAX_VELOCITY || value <= -MAX_VELOCITY) {
@@ -140,7 +140,7 @@ export function readOneUpdate(reader: BinaryReader): DecodedUpdate | undefined {
}
if ((flags & UpdateFlags.Name) !== 0) {
update.name = decodeString(readUint8Array(reader)) || undefined;
update.name = decodeStringFromUint8Array(readUint8Array(reader)) || undefined;
update.filterName = (flags & UpdateFlags.NameBad) !== 0;
}
+2 -2
View File
@@ -1,6 +1,6 @@
import { max, compact } from 'lodash';
import { saveAs } from 'file-saver';
import { Psd, writePsd, Layer } from 'ag-psd';
import { Psd, writePsd, Layer, BlendMode } from 'ag-psd';
import { SpriteSet, PonyInfoNumber, PaletteSpriteSet, NoDraw } from '../../common/interfaces';
import { times, cloneDeep, setFlag, includes, toInt } from '../../common/utils';
import { createDefaultPony, syncLockedPonyInfoNumber, toPaletteNumber, mockPaletteManager } from '../../common/ponyInfo';
@@ -237,7 +237,7 @@ function createPsdPatternLayers(sheet: Sheet, rows: number, cols: number, layer:
canvas: drawPsdLayer(sheet, rows, cols, layer, i),
hidden: true,
clipping: true,
blendMode: 'multiply',
blendMode: 'multiply' as BlendMode,
})),
]);
}
+1 -1
View File
@@ -1,6 +1,5 @@
import { repeat } from 'lodash';
import { toByteArray } from 'base64-js';
import { encodeString } from 'ag-sockets/dist/utf8';
import { PonyOptions, EntityState, UpdateFlags } from '../common/interfaces';
import { ICharacter, IAccount, Character, queryCharacter } from './db';
import { isForbiddenName } from '../common/security';
@@ -23,6 +22,7 @@ import { isPonyFlying } from '../common/entityUtils';
import { createCharacterState, updateClientCharacter } from './playerUtils';
import { encodeExpression } from '../common/encoders/expressionEncoder';
import { QueryFilter } from 'mongoose';
import { encodeString } from '../common/binaryUtils';
export const defaultCharacterState: CharacterState = { x: 0, y: 0 };
+10 -11
View File
@@ -1,5 +1,4 @@
import { resizeWriter, writeUint8, BinaryWriter, writeUint32, writeUint16 } from 'ag-sockets';
import { encodeString } from 'ag-sockets/dist/utf8';
import { writeUint8, BinaryWriter, writeUint32, writeUint16 } from 'ag-sockets';
import {
Entity, Rect, EntityState, UpdateFlags, Action, EntityOrPonyOptions, UpdateType, TileType, canWalk, setAnimationToEntityState
} from '../common/interfaces';
@@ -16,6 +15,7 @@ import { PONY_TYPE } from '../common/constants';
import { grapesPurple, grapesGreen } from '../common/entities';
import { getTile } from '../common/tileUtils';
import { getRegion, getRegionGlobal } from '../common/region';
import { encodeString, resizeWriterWithData } from '../common/binaryUtils';
export function isEntityShadowed(entity: ServerEntity): entity is ServerEntityWithClient {
return entity.client !== undefined && entity.client.shadowed;
@@ -163,18 +163,17 @@ export function pushUpdateEntity(update: EntityUpdateBase) {
}
}
function resizePreserveWriter(error: unknown, writer: BinaryWriter, offset: number) {
function resizePreserveWriter(error: Error, writer: BinaryWriter, offset: number) {
if (isOverflowError(error)) {
const bytes = writer.bytes;
resizeWriter(writer);
writer.bytes.set(bytes);
resizeWriterWithData(writer);
writer.offset = offset;
// DEVELOPMENT && logger.debug(`resize writer to ${writer.bytes.byteLength} (${error.message})`);
// DEVELOPMENT && console.log(`resize writer to ${writer.view.byteLength} (${error.message})`);
} else {
throw error;
}
}
export function pushAddEntityToClient(client: IClient, entity: ServerEntity) {
const writer = client.updateQueue;
const offset = writer.offset;
@@ -185,7 +184,7 @@ export function pushAddEntityToClient(client: IClient, entity: ServerEntity) {
writeOneEntity(writer, entity, client);
break;
} catch (e) {
resizePreserveWriter(e, writer, offset);
resizePreserveWriter(e as Error, writer, offset);
}
}
}
@@ -201,7 +200,7 @@ export function pushUpdateEntityToClient(client: IClient, update: EntityUpdateBa
writeOneUpdate(writer, entity, flags, x, y, vx, vy, options, action, playerState);
break;
} catch (e) {
resizePreserveWriter(e, writer, offset);
resizePreserveWriter(e as Error, writer, offset);
}
}
}
@@ -216,7 +215,7 @@ export function pushRemoveEntityToClient(client: IClient, entity: ServerEntity)
writeUint32(writer, entity.id);
break;
} catch (e) {
resizePreserveWriter(e, writer, offset);
resizePreserveWriter(e as Error, writer, offset);
}
}
}
@@ -233,7 +232,7 @@ export function pushUpdateTileToClient(client: IClient, x: number, y: number, ty
writeUint8(writer, type);
break;
} catch (e) {
resizePreserveWriter(e, writer, offset);
resizePreserveWriter(e as Error, writer, offset);
}
}
}
+8 -8
View File
@@ -14,7 +14,7 @@ import * as express from 'express';
import { WebSocketServer } from '@encharm/cws';
import { compact, once } from 'lodash';
import { copySync, removeSync, ensureDirSync } from 'fs-extra';
import { createServerHost, createClientOptions, ServerOptions, ClientExtensions, Packet } from 'ag-sockets';
import { createServerHost, createClientOptions, ServerOptions, ClientExtensions } from 'ag-sockets';
import { config, port, server, args, version } from './config';
import { YEAR, WEEK } from '../common/constants';
import { rollbarCheckIgnore } from '../common/rollbar';
@@ -210,7 +210,7 @@ const sessionMiddlewares = once(() => [createSession(), passport.initialize(), p
const adminMiddlewares = once(() => [...sessionMiddlewares(), isAdmin(server)]);
const socketOptionsBase: ServerOptions = {
ws: { Server: WebSocketServer },
hash: STAMP,
hash: STAMP.toString(),
};
initLogRequest(stats.logRequest);
@@ -246,15 +246,15 @@ if (args.game) {
...socketOptionsBase,
verifyClient: () => !getSettings().isServerOffline && !liveSettings.shutdown,
forceBinary: true,
onSend: (packet: Packet) => {
sent += packet.binary ? packet.binary.byteLength : (packet.json ? packet.json.length : 0);
onSend: (id: number, name: string, packetSize: number, binary: boolean) => {
sent += packetSize;
sentPackets++;
stats.logSendStats(packet);
stats.logSendStats(id, name, binary, packetSize);
},
onRecv: (packet: Packet) => {
received += packet.binary ? packet.binary.byteLength : (packet.json ? packet.json.length : 0);
onRecv: (id: number, name: string, packetSize: number, binary: boolean) => {
received += packetSize;
receivedPackets++;
stats.logRecvStats(packet);
stats.logRecvStats(id, name, binary, packetSize);
},
};
+1 -1
View File
@@ -53,7 +53,7 @@ export function toFriendRemove(client: IClient): FriendStatusData {
}
export function toFriend(client: IClient): FriendStatusData {
if (client.isConnected) {
if (client.isConnected()) {
return toFriendOnline(client);
} else {
return toFriendOffline(client);
+6 -7
View File
@@ -3,7 +3,6 @@ import * as path from 'path';
import * as moment from 'moment';
import { compact } from 'lodash';
import { Request } from 'express';
import { Packet } from 'ag-sockets';
import { RequestStats, ServerStats } from '../common/adminInterfaces';
import { HOUR } from '../common/constants';
import { ByteSize } from './utils/byteSize';
@@ -92,13 +91,13 @@ export class StatsTracker {
logSpamming = () => {
this.dailySpamming++;
}
logRecvStats = (packet: Packet) => {
this.logSocketStats(this.recvStats, packet);
logRecvStats = (id: number, name: string, binary: boolean, size: number) => {
this.logSocketStats(this.recvStats, id, name, binary, size);
}
logSendStats = (packet: Packet) => {
this.logSocketStats(this.sendStats, packet);
logSendStats = (id: number, name: string, binary: boolean, size: number) => {
this.logSocketStats(this.sendStats, id, name, binary, size);
}
private logSocketStats(stats: (SocketStats | undefined)[], { id, name, binary, json }: Packet) {
private logSocketStats(stats: (SocketStats | undefined)[], id: number, name: string, binary: boolean, size: number) {
const entry = stats[id] || (stats[id] = {
id,
name,
@@ -119,7 +118,7 @@ export class StatsTracker {
entry.countStr++;
}
entry.size.addBytes(binary ? (binary.length || binary.byteLength) : (json ? json.length : 0));
entry.size.addBytes(size);
}
getStats(): RequestStats[] {
const result: RequestStats[] = [];
+2 -2
View File
@@ -95,7 +95,7 @@ export class World {
// this.mapPools.set('house', createPool(10, () => createHouseMap(this, true), resetHouseMap));
partyService.partyChanged.subscribe(client => {
if (client.isConnected && client.map.usage === MapUsage.Party) {
if (client.isConnected() && client.map.usage === MapUsage.Party) {
if (
client.party && client.party.leader === client && client.map.instance === client.accountId &&
!this.maps.some(m => m.id === client.map.id && m.instance === client.party!.id)
@@ -785,7 +785,7 @@ export class World {
client.disconnect(true);
} else {
setTimeout(() => {
if (client.isConnected) {
if (client.isConnected()) {
client.disconnect(true);
}
}, 200);
+1 -1
View File
@@ -1,6 +1,5 @@
import { stubClass, resetStubMethods } from '../lib';
import { NgZone } from '@angular/core';
import { encodeString } from 'ag-sockets/dist/utf8';
import { Subject } from 'rxjs';
import { expect } from 'chai';
import { stub, assert, SinonStub } from 'sinon';
@@ -25,6 +24,7 @@ import { createServerRegion } from '../../server/serverRegion';
import { ServerRegion } from '../../server/serverInterfaces';
import { getTile } from '../../common/tileUtils';
import { Weather } from '../../common/commonInterfaces';
import { encodeString } from '../../common/binaryUtils';
describe('ClientActions', () => {
let zone: NgZone;
@@ -3,13 +3,13 @@ import { expect } from 'chai';
import { encodeUpdateSimple, encodeRegionSimple } from '../../../common/encoders/updateEncoder';
import { EntityState, DecodedRegionUpdate, UpdateFlags, TileType } from '../../../common/interfaces';
import { entity, mockClient, serverEntity } from '../../mocks';
import { encodeString } from 'ag-sockets/dist/utf8';
import { emptyUpdate, decodeUpdate } from '../../../common/encoders/updateDecoder';
import { createServerRegion } from '../../../server/serverRegion';
import { IClient, ServerRegion } from '../../../server/serverInterfaces';
import { setEntityName } from '../../../server/entityUtils';
import { compressTiles } from '../../../common/compress';
import { REGION_SIZE } from '../../../common/constants';
import { encodeString } from '../../../common/binaryUtils';
describe('updateEncoder', () => {
describe('encodeUpdate() + decodeUpdate()', () => {
+1 -1
View File
@@ -1,6 +1,5 @@
import '../lib';
import { expect } from 'chai';
import { encodeString } from 'ag-sockets/dist/utf8';
import { createExtraOptions, updatePony, encryptInfo, createPony, getAndFixCharacterState } from '../../server/characterUtils';
import { account, character, serverEntity, entity, genObjectId } from '../mocks';
import { CharacterFlags, CharacterState, SupporterFlags, CharacterStateFlags } from '../../common/adminInterfaces';
@@ -13,6 +12,7 @@ import { CounterService } from '../../server/services/counter';
import { createCharacterState } from '../../server/playerUtils';
import { hasFlag } from '../../common/utils';
import { Types } from 'mongoose';
import { encodeString } from '../../common/binaryUtils';
describe('characterUtils', () => {
describe('createPony()', () => {
+1 -1
View File
@@ -3,7 +3,6 @@ import { expect } from 'chai';
import { stub, assert, spy, SinonSpy, SinonFakeTimers, useFakeTimers, SinonStub } from 'sinon';
import { range } from 'lodash';
import { getWriterBuffer } from 'ag-sockets';
import { encodeString } from 'ag-sockets/dist/utf8';
import {
ChatType, TileType, Action, PlayerAction, ModAction, Eye, Muzzle, SelectFlags, InfoFlags
} from '../../common/interfaces';
@@ -24,6 +23,7 @@ import { SupporterInvitesService } from '../../server/services/supporterInvites'
import { createCamera } from '../../common/camera';
import { FriendsService } from '../../server/services/friends';
import * as playerUtils from '../../server/playerUtils';
import { encodeString } from '../../common/binaryUtils';
describe('ServerActions', () => {
let accountService = stubFromInstance<AccountService>({