diff --git a/README.md b/README.md index 42bf331..d2d4451 100644 --- a/README.md +++ b/README.md @@ -276,6 +276,13 @@ Tools are accessible at `/tools/` (only available in dev mode or when ## Changelog +#### Pony.Town v0.55.2 +- Optimized server performance +- Fixed various animation issues +- Fixed issues with kiss animation while turning head +- Added additional server performance stats to the admin panel +- Changed the button on admin panel that removes old events from 10min to 1day + ### Pony.Town v0.55.0 - Added Kiss action - Added being able to extinguish torches by sneezing diff --git a/package.json b/package.json index a065e0a..291873a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pony-town", - "version": "0.55.0", + "version": "0.55.2", "description": "A game of ponies building a town", "main": "pony-town.js", "scripts": { diff --git a/src/ts/client/buttonActions.ts b/src/ts/client/buttonActions.ts index 6495f6e..add0169 100644 --- a/src/ts/client/buttonActions.ts +++ b/src/ts/client/buttonActions.ts @@ -39,7 +39,7 @@ const ICON_SIZE = 16; const headX = -26; const headY = -30; const headlessBoopFrame = { ...cloneDeep(boop.frames[7]), head: 0 }; -const headlessBoop: BodyAnimation = { name: '', loop: false, fps: 1, frames: [headlessBoopFrame] }; +const headlessBoop: BodyAnimation = { name: '', loop: false, fps: 1, frames: [headlessBoopFrame], disableHeadTurnFrames: 0 }; function createPony(coatColor: string, wings = false, horn = false) { const info = createDefaultPony(); diff --git a/src/ts/client/credits.ts b/src/ts/client/credits.ts index a8c9481..d86ff3a 100644 --- a/src/ts/client/credits.ts +++ b/src/ts/client/credits.ts @@ -77,7 +77,7 @@ export const CONTRIBUTORS: Contributors[] = [ { name: 'Deeraw', links: ['https://www.deviantart.com/deerdraw', 'https://twitter.com/TheOnlyDeeraw'] }, { name: 'Aviivix' }, { name: 'SnowFl8keAnge1' }, - { name: '3aHo3a', links: ['http://twitter.com/imnuclearimwild'] }, + { name: '3aHo3a', links: ['https://twitter.com/imnuclearimwild'] }, ], }, ]; diff --git a/src/ts/client/playerActions.ts b/src/ts/client/playerActions.ts index edb6b86..f5c4f60 100644 --- a/src/ts/client/playerActions.ts +++ b/src/ts/client/playerActions.ts @@ -139,7 +139,21 @@ export function boopAction(game: PonyTownGame) { } export function turnHeadAction(game: PonyTownGame) { - if (game.player && game.send(server => server.action(Action.TurnHead))) { + if (!game.player) { + return; + } + + if (game.player.animator.state) { + const disabledFrames = game.player.animator.state.animation.disableHeadTurnFrames; + if (disabledFrames > 0) { + const disabledTime = disabledFrames / game.player.animator.state.animation.fps; + if (disabledTime >= game.player.animator.time) { + return; + } + } + } + + if (game.send(server => server.action(Action.TurnHead))) { game.player.state = game.player.state ^ EntityState.HeadTurned; game.headTurnedOverride = hasFlag(game.player.state, EntityState.HeadTurned); game.onActionsUpdate.next(); diff --git a/src/ts/client/ponyAnimations.ts b/src/ts/client/ponyAnimations.ts index a861194..9fe8170 100644 --- a/src/ts/client/ponyAnimations.ts +++ b/src/ts/client/ponyAnimations.ts @@ -20,7 +20,7 @@ export function createBodyFrame([ } export function createBodyAnimation( - name: string, fps: number, loop: boolean, frames: number[][], shadowOffsets?: number[][] + name: string, fps: number, loop: boolean, frames: number[][], shadowOffsets?: number[][], disableHeadTurnFrames?: number ): Readonly { if (shadowOffsets && shadowOffsets.length !== frames.length) { throw new Error(`Incorrect frame count for shadowOffsets for ${name}, animation frames ${frames.length}, shadow frames ${shadowOffsets.length}`); @@ -28,7 +28,7 @@ export function createBodyAnimation( const shadow = shadowOffsets && shadowOffsets.map(([frame, offset]) => ({ frame, offset })); - return { name, loop, fps, frames: frames.map(createBodyFrame), shadow }; + return { name, loop, fps, frames: frames.map(createBodyFrame), shadow, disableHeadTurnFrames: disableHeadTurnFrames || 0 }; } export const stand = createBodyAnimation('stand', 24, true, [ @@ -468,7 +468,7 @@ export const kissBody = createBodyAnimation('kiss-body', 24, false, [ ...repeat(2, [5, 1, 1, 0, 26, 26, 19, 19, -10, -6, 1, 0, 1, 1, 1, 1, 1, -1, 1, -1]), [5, 1, 1, 0, 28, 28, 18, 18, -9, -5, 1, 0, 1, 1, 1, 1, 1, -1, 1, -1], ...repeat(2, [2, 1, 1, 0, 1, 1, 1, 1, -1, 0, -1, 1, 1, 0, 1, 0, 1, 0, 1]) -]); +], undefined, 12); export const kissLiftHoofBody = createBodyAnimation('kiss-lift-hoof-body', 24, false, [ ...repeat(3, [2, 1, 0, 0, 1, 1, 1, 1, -1, 0, 0, 1, 1, 0, 1, 0, 1, 0, 1]), @@ -479,7 +479,7 @@ export const kissLiftHoofBody = createBodyAnimation('kiss-lift-hoof-body', 24, f ...repeat(2, [5, 1, 1, 0, 8, 26, 19, 19, -10, -6, 1, 0, 1, 2, 1, 1, 1, -1, 1, -1]), [5, 1, 1, 0, 28, 28, 18, 18, -9, -5, 1, 0, 1, 1, 1, 1, 1, -1, 1, -1], ...repeat(2, [2, 1, 1, 0, 1, 1, 1, 1, -1, 0, -1, 1, 1, 0, 1, 0, 1, 0, 1]) -]); +], undefined, 12); export const kissFlyBody = createBodyAnimation('kiss-fly-body', 16, false, [ [1, 1, 3, 0, 8, 10, 6, 5, 0, -16], @@ -514,7 +514,7 @@ export const kissFlyBody = createBodyAnimation('kiss-fly-body', 16, false, [ [1, 1, 12, 0, 9, 10, 4, 4, 0, -17, -1, 0, 0, 0, 0, -1], [1, 1, 3, 0, 8, 10, 5, 4, 0, -16, 0, 0, 0, 0, 0, -1], [1, 1, 4, 0, 8, 10, 6, 5, 0, -15] -]); +], undefined, 12); export const kissFlyBugBody = createBodyAnimation('kiss-fly-bug-body', 24, false, [ [1, 1, 3, 0, 8, 10, 6, 5, 0, -16], @@ -557,7 +557,7 @@ export const kissFlyBugBody = createBodyAnimation('kiss-fly-bug-body', 24, false [1, 1, 4, 0, 9, 10, 4, 4, 0, -17, -1, 0, 0, 0, 0, -1], [1, 1, 5, 0, 8, 10, 5, 4, 0, -17, 0, 0, 0, 0, 0, -1], [1, 1, 4, 0, 8, 10, 6, 5, 0, -17] -]); +], undefined, 12); export const kissLieBody = createBodyAnimation('kiss-lie-body', 24, false, [ ...repeat(3, [14, 1, 0, 2, 38, 38, 26, 26]), @@ -569,7 +569,7 @@ export const kissLieBody = createBodyAnimation('kiss-lie-body', 24, false, [ [13, 1, 0, 2, 38, 38, 26, 26, 0, 0, -1], ...repeat(2, [14, 1, 0, 2, 38, 38, 26, 26, 0, 0, -1]), ...repeat(2, [14, 1, 0, 2, 38, 38, 26, 26]) -], [...repeat(72, [3, 3])]); +], [...repeat(72, [3, 3])], 70); export const kissSitBody = createBodyAnimation('kiss-sit-body', 24, false, [ ...repeat(3, [8, 1, 2, 2, 34, 34, 25, 25]), @@ -579,7 +579,7 @@ export const kissSitBody = createBodyAnimation('kiss-sit-body', 24, false, [ ...repeat(60, [9, 1, 2, 2, 39, 39, 26, 26, -1, -1, -2, 0, 0, 1, 0, 1, 1, 1, 1, 1]), ...repeat(2, [9, 1, 2, 2, 34, 34, 26, 26, 0, -1, -2, 0, 0, 1, 0, 1, 0, 1, 0, 1]), ...repeat(2, [9, 1, 2, 2, 34, 34, 26, 26, 0, 0, -1]) -], [...repeat(72, [0, 6])]); +], [...repeat(72, [0, 6])], 68); export const kissSwimBody = createBodyAnimation('kiss-swim-body', 8, false, [ ...repeat(2, [1, 1, 0, 0, 8, 10, 6, 5, 0, 14]), @@ -596,7 +596,7 @@ export const kissSwimBody = createBodyAnimation('kiss-swim-body', 8, false, [ ...repeat(2, [1, 1, 0, 0, 9, 10, 4, 4, -1, 12, -1, 0, 0, 0, 0, -1]), [1, 1, 0, 0, 8, 10, 4, 4, 0, 13, -1, 0, 0, 0, 0, -1], [1, 1, 0, 0, 8, 10, 5, 5, 0, 13] -]); +], undefined, 12); export const kissToTrot = createBodyAnimation('kiss-to-trot-body', 24, false, [ [2, 1, 0, 0, 8, 4, 19, 4, -1, 0, -1, 1, 0, 0, 0, 0, 1, -1], @@ -626,6 +626,7 @@ export function mergeAnimations(name: string, fps: number, loop: boolean, animat name, fps, loop, + disableHeadTurnFrames: Math.min(...animations.map(a => a.disableHeadTurnFrames)), frames: flatten(animations.map(a => a.frames)), shadow: flatten(animations.map(a => a.shadow || a.frames.map(() => ({ frame: 0, offset: 0 })))), }; diff --git a/src/ts/client/ponyStates.ts b/src/ts/client/ponyStates.ts index f4a5170..b5ff9a0 100644 --- a/src/ts/client/ponyStates.ts +++ b/src/ts/client/ponyStates.ts @@ -127,20 +127,19 @@ transition(standing, kissing, { exitAfter: 0 }); transition(kissingHoof, standing); transition(standing, kissingHoof, { exitAfter: 0 }); transition(kissingFlying, hovering, { enterTime: 1.1 / 10 }); -transition(hovering, kissingFlying, { exitAfter: 0 }); -transition(kissingLying, lying); -transition(lying, kissingLying, { exitAfter: 0 }); -transition(kissingSitting, sitting); -transition(sitting, kissingSitting, { exitAfter: 0 }); +transition(hovering, kissingFlying, { exitAfter: 0, onlyDirectTo: kissingFlying }); transition(kissing, kissingToTrotting, { exitAfter: 0, onlyDirectTo: trotting }); transition(kissingToTrotting, trotting); +transition(kissingToTrotting, standing); transition(kissingSitting, sittingToTrotting, { exitAfter: 0, onlyDirectTo: trotting }); -transition(sittingToTrotting, standing); +transition(kissingSitting, sitting); +transition(sitting, kissingSitting, { exitAfter: 0, onlyDirectTo: kissingSitting }); transition(kissingLying, lyingToTrotting, { exitAfter: 0, onlyDirectTo: trotting }); -transition(lyingToTrotting, standing); +transition(kissingLying, lying); +transition(lying, kissingLying, { exitAfter: 0, onlyDirectTo: kissingLying }); transition(anyState, trotting, { exitAfter: 0, keepTime: true }); transition(anyState, flying, { exitAfter: 0, keepTime: true }); diff --git a/src/ts/common/adminInterfaces.ts b/src/ts/common/adminInterfaces.ts index ce12b7e..ada287e 100644 --- a/src/ts/common/adminInterfaces.ts +++ b/src/ts/common/adminInterfaces.ts @@ -132,6 +132,8 @@ export interface InternalApi extends InternalCommonApi { cancelUpdate(): Promise; shutdownServer(value: boolean): Promise; getTimings(): Promise; + setTimingEnabled(isEnabled: boolean): Promise; + getWorldPerfStats(): Promise; teleportTo(adminAccountId: string, targetAccountId: string): Promise; } @@ -820,6 +822,35 @@ export interface TimingEntry { name?: string; } +export interface WorldPerfStats { + lastUpdateTime: number; + maxUpdateTime: number; + minUpdateTime: number; + avgUpdateTime: number; + isSamplingEnabled: boolean; + clients: number; + movingEntities: number; + regionsCount: number; + mapsCount: number; + clientQueue: number; + controllersCount: number; + clientsWithAdds: number; + clientsWithUpdates: number; + clientsWithSays: number; + totalSays: number; + sent: number; + received: number; + sentPackets: number; + receivedPackets: number; +} + +export function defaultWorldPerfStats() { + return { lastUpdateTime: 0, maxUpdateTime: 0, minUpdateTime: 0, avgUpdateTime: 0, isSamplingEnabled: false, + clients: 0, movingEntities: 0, regionsCount: 0, mapsCount: 0, clientQueue: 0, controllersCount: 0, + clientsWithAdds: 0, clientsWithUpdates: 0, clientsWithSays: 0, totalSays: 0, sent: 0, received: 0, + sentPackets: 0, receivedPackets: 0 }; +} + export type ModelTypes = 'accounts' | 'auths' | 'origins' | 'ponies' | 'accountAuths' | 'accountOrigins' | 'accountPonies'; @@ -919,5 +950,7 @@ export interface IAdminServerActions { clearSessions(accountId: string): Promise; // other getTimings(serverId: string): Promise; + setTimingEnabled(serverId: string, isEnabled: boolean): Promise; + getWorldPerfStats(serverId: string): Promise; teleportTo(accountId: string): Promise; } diff --git a/src/ts/common/collision.ts b/src/ts/common/collision.ts index 02cea72..e933864 100644 --- a/src/ts/common/collision.ts +++ b/src/ts/common/collision.ts @@ -5,16 +5,6 @@ import { getRegionGlobal, isInWaterAt } from './worldMap'; import { tileWidth, tileHeight, PONY_TYPE, REGION_SIZE, REGION_WIDTH, REGION_HEIGHT } from './constants'; import { isInTheAir, isFlying } from './entityUtils'; -let isCollidingCount = 0; -let isCollidingObjectCount = 0; - -export function getCollisionStats() { - const stats = { isCollidingCount, isCollidingObjectCount }; - isCollidingCount = 0; - isCollidingObjectCount = 0; - return stats; -} - export function isOutsideMap(x: number, y: number, map: IMap): boolean { return x < 0 || y < 0 || x >= map.width || y >= map.height; } diff --git a/src/ts/common/interfaces.ts b/src/ts/common/interfaces.ts index 18d5a98..27afe88 100644 --- a/src/ts/common/interfaces.ts +++ b/src/ts/common/interfaces.ts @@ -1378,6 +1378,7 @@ export interface BodyAnimation { fps: number; frames: BodyAnimationFrame[]; shadow?: BodyShadow[]; + disableHeadTurnFrames: number; } export interface HeadAnimationFrame { @@ -1853,3 +1854,17 @@ export const enum UpdateFlags { SwitchRegion = 2048, // max 32768 } + +export let counterNow: () => number; + +if (typeof window !== 'undefined') { + counterNow = performance.now; +} else { + const hrtime = process.hrtime; + const getNanoSeconds = () => { + const hr = hrtime(); + return hr[0] * 1e9 + hr[1]; + }; + const nodeLoadTime = getNanoSeconds() - process.uptime() * 1e9; + counterNow = () => (getNanoSeconds() - nodeLoadTime) / 1e6; +} diff --git a/src/ts/common/sheets.ts b/src/ts/common/sheets.ts index e8d1bda..5f8d0d3 100644 --- a/src/ts/common/sheets.ts +++ b/src/ts/common/sheets.ts @@ -839,6 +839,7 @@ function state( name: '', loop: false, fps: 24, + disableHeadTurnFrames: 0, frames: times(frames, i => ({ ...createBodyFrame([]), head: (head || ones)[i], diff --git a/src/ts/components/admin/admin-events/admin-events.pug b/src/ts/components/admin/admin-events/admin-events.pug index a6a6148..a63e817 100644 --- a/src/ts/components/admin/admin-events/admin-events.pug +++ b/src/ts/components/admin/admin-events/admin-events.pug @@ -12,9 +12,9 @@ ng-template(#duplicatesPopover) button.btn.btn-sm.btn-default.ml-1((click)="cleanupDeleted()" title="Cleanup deleted events") fa-icon.mr-1([icon]="syncIcon" size="lg") | clear - button.btn.btn-sm.btn-default.ml-1((click)="removeEvents(1000 * 60 * 10)" title="Delete all older than 10 minutes") + button.btn.btn-sm.btn-default.ml-1((click)="removeEvents(1000 * 60 * 60 * 24)" title="Delete all older than 1 day") fa-icon.mr-1([icon]="clockIcon" size="lg") - | 10min + | 1day button.btn.btn-sm.btn-default.ml-1((click)="removeEvents(1000 * 3)" title="Delete all events") fa-icon.mr-1([icon]="trashIcon" size="lg") | all diff --git a/src/ts/components/admin/admin-reports/admin-reports-perf/admin-reports-perf.pug b/src/ts/components/admin/admin-reports/admin-reports-perf/admin-reports-perf.pug index c1b9469..0e5a558 100644 --- a/src/ts/components/admin/admin-reports/admin-reports-perf/admin-reports-perf.pug +++ b/src/ts/components/admin/admin-reports/admin-reports-perf/admin-reports-perf.pug @@ -1,18 +1,22 @@ div(style="position: relative;") div(style="position: absolute; top: -50px; right: 0;") b.p-2 {{server}} - .btn-group(*ngIf="server") + .btn-group(*ngIf="loaded") button.btn.btn-default((click)="resetZoom()") | Reset zoom button.btn.btn-default((click)="fitZoom()") | Fit zoom button.btn.btn-default((click)="fullFrameZoom()") | Full frame - button.btn.btn-default.ml-1(*ngIf="server" (click)="load(server)") + button.btn.btn-default.ml-1(*ngIf="loaded" (click)="load(server)") | Refresh + button.btn.btn-default.ml-1(*ngIf="loaded" (click)="disable()") + | Disable + button.btn.btn-default.ml-1(*ngIf="!loaded && server" (click)="enable()") + | Enable .btn-group.dropdown.ml-1(dropdown) button.btn.btn-default.dropdown-toggle(dropdownToggle) - | Load + | Select .dropdown-menu.dropdown-menu-right(*dropdownMenu) button.dropdown-item(*ngFor="let s of servers" (click)="load(s)") | {{s}} @@ -20,11 +24,88 @@ div(style="position: relative;") div(#container style="position: relative; overflow: hidden;") canvas(#canvas (mousemove)="mouseMove($event)" (wheel)="wheel($event)" (agDrag)="drag($event)") div(#tooltip style="display: none; position: absolute; left: 0; top: 0; padding: 2px 6px; background: #111; border-radius: 2px; font-size: 12px; white-space: pre; pointer-events: none;") - + + p .float-right.text-muted - div total samples: {{timings.length}} + div samples: {{timings.length}} + + br + .float-right.text-muted + div sampling: {{worldPerfStats.isSamplingEnabled}} + + br + .float-right.text-muted + div avg update: {{worldPerfStats.avgUpdateTime.toFixed(2)}} - table.table.table-sm(style="font-size: small; width: 1000px;") + br + .float-right.text-muted + div last update: {{worldPerfStats.lastUpdateTime.toFixed(2)}} + + br + .float-right.text-muted + div max update: {{worldPerfStats.maxUpdateTime.toFixed(2)}} + + br + .float-right.text-muted + div min update: {{worldPerfStats.minUpdateTime.toFixed(2)}} + + br + .float-right.text-muted + div moving entities: {{worldPerfStats.movingEntities}} + + br + .float-right.text-muted + div controllers: {{worldPerfStats.controllersCount}} + + br + .float-right.text-muted + div regions: {{worldPerfStats.regionsCount}} + + br + .float-right.text-muted + div maps: {{worldPerfStats.mapsCount}} + + br + .float-right.text-muted + div clients: {{worldPerfStats.clients}} + + br + .float-right.text-muted + div clients with adds: {{worldPerfStats.clientsWithAdds}} + + br + .float-right.text-muted + div clients with updates: {{worldPerfStats.clientsWithUpdates}} + + br + .float-right.text-muted + div clients with says: {{worldPerfStats.clientsWithSays}} + + br + .float-right.text-muted + div total says: {{worldPerfStats.totalSays}} + + br + .float-right.text-muted + div client queue: {{worldPerfStats.clientQueue}} + + br + .float-right.text-muted + div client sent: {{(worldPerfStats.sent / 1024).toFixed(2) + 'kb'}} + + br + .float-right.text-muted + div client received: {{(worldPerfStats.received / 1024).toFixed(2) + 'kb'}} + + br + .float-right.text-muted + div client sent packets: {{worldPerfStats.sentPackets}} + + br + .float-right.text-muted + div client received packets: {{worldPerfStats.receivedPackets}} + + table.table.table-sm(style="position: relative; bottom : 410px; font-size: small; width: 1000px;") thead tr th.text-right Self Time diff --git a/src/ts/components/admin/admin-reports/admin-reports-perf/admin-reports-perf.ts b/src/ts/components/admin/admin-reports/admin-reports-perf/admin-reports-perf.ts index d06c842..d156af1 100644 --- a/src/ts/components/admin/admin-reports/admin-reports-perf/admin-reports-perf.ts +++ b/src/ts/components/admin/admin-reports/admin-reports-perf/admin-reports-perf.ts @@ -1,6 +1,6 @@ import { Component, ViewChild, ElementRef } from '@angular/core'; import { AdminModel } from '../../../services/adminModel'; -import { TimingEntry, TimingEntryType } from '../../../../common/adminInterfaces'; +import { TimingEntry, TimingEntryType, WorldPerfStats, defaultWorldPerfStats } from '../../../../common/adminInterfaces'; import { findById, pointInRect, clamp } from '../../../../common/utils'; import { SERVER_FPS } from '../../../../common/constants'; import { AgDragEvent } from '../../../shared/directives/agDrag'; @@ -39,6 +39,7 @@ export class AdminReportsPerf { endTime = 0; listing: ListingEntry[] = []; timings: TimingEntry[] = []; + worldPerfStats = defaultWorldPerfStats(); private tooltips: Tooltip[] = []; private startTimeFrom = 0; private endTimeFrom = 0; @@ -53,10 +54,23 @@ export class AdminReportsPerf { } }, 100); } + + setInterval(this.update, frameTime, this); } get servers() { return this.model.state.gameServers.map(s => s.id); } + disable() { + this.loaded = false; + this.timings = []; + this.listing = []; + this.model.setTimingEnabled(this.server, false); + this.redraw(); + } + async enable() { + this.loaded = true; + await this.load(this.server); + } async load(server: string) { this.server = server; this.timings = []; @@ -72,6 +86,17 @@ export class AdminReportsPerf { this.redraw(); } + async update(self: AdminReportsPerf) { + if (self.server) { + const result = await self.model.getWorldPerfStats(self.server); + if (result) { + self.worldPerfStats = result as WorldPerfStats; + } + } + else { + self.worldPerfStats = defaultWorldPerfStats(); + } + } mouseMove(e: MouseEvent) { const rect = this.container.nativeElement.getBoundingClientRect() as DOMRect; const x = e.pageX - rect.left; @@ -117,6 +142,10 @@ export class AdminReportsPerf { } } resetZoom() { + if (!this.timings.length) { + return; + } + const firstTime = this.timings[0].time; const lastTime = this.timings[this.timings.length - 1].time; this.startTime = firstTime - timePadding; @@ -125,6 +154,10 @@ export class AdminReportsPerf { this.lastZoom = 0; } fitZoom() { + if (!this.timings.length) { + return; + } + const firstTime = this.timings[0].time; const lastTime = this.timings[this.timings.length - 1].time; const totalTime = lastTime - firstTime; @@ -134,6 +167,10 @@ export class AdminReportsPerf { this.lastZoom = 1; } fullFrameZoom() { + if (!this.timings.length) { + return; + } + const firstTime = this.timings[0].time; const lastTime = firstTime + frameTime; this.startTime = firstTime - 2; @@ -245,6 +282,10 @@ export class AdminReportsPerf { } } recalcListing() { + if (!this.timings.length) { + return; + } + interface Entry extends TimingEntry { excludedTime: number; } diff --git a/src/ts/components/services/adminModel.ts b/src/ts/components/services/adminModel.ts index f6e20da..0ea9ea1 100644 --- a/src/ts/components/services/adminModel.ts +++ b/src/ts/components/services/adminModel.ts @@ -519,6 +519,14 @@ export class AdminModel { return this.server.getTimings(server) .catch(this.handleError); } + setTimingEnabled(server: string, isEnabled: boolean) { + return this.server.setTimingEnabled(server, isEnabled) + .catch(this.handleError); + } + getWorldPerfStats(server: string) { + return this.server.getWorldPerfStats(server) + .catch(this.handleError); + } teleportTo(accountId: string) { return this.server.teleportTo(accountId) .catch(this.handleError); diff --git a/src/ts/components/tools/tools-animation/tools-animation.ts b/src/ts/components/tools/tools-animation/tools-animation.ts index 737b359..801e7ea 100644 --- a/src/ts/components/tools/tools-animation/tools-animation.ts +++ b/src/ts/components/tools/tools-animation/tools-animation.ts @@ -685,6 +685,7 @@ function toBodyAnimation({ name, loop, fps, frames }: BodyAnimation, full: boole loop, fps, shadow, + disableHeadTurnFrames: 0, frames: flatMap(frames, f => repeat(full ? f.duration : 1, { body: f.body, head: f.head, @@ -706,7 +707,7 @@ function toBodyAnimation({ name, loop, fps, frames }: BodyAnimation, full: boole backLegY: switchFarClose ? f.backFarLegY : f.backLegY, backFarLegX: switchFarClose ? f.backLegX : f.backFarLegX, backFarLegY: switchFarClose ? f.backLegY : f.backFarLegY, - })), + })) }; } diff --git a/src/ts/server/adminServerActions.ts b/src/ts/server/adminServerActions.ts index 906361d..9876658 100644 --- a/src/ts/server/adminServerActions.ts +++ b/src/ts/server/adminServerActions.ts @@ -567,6 +567,16 @@ export class AdminServerActions implements IAdminServerActions, SocketServer { return server.api.getTimings(); } @Method({ promise: true }) + async setTimingEnabled(serverId: string, isEnabled: boolean) { + const server = getServer(serverId); + server.api.setTimingEnabled(isEnabled); + } + @Method({ promise: true }) + async getWorldPerfStats(serverId: string) { + const server = getServer(serverId); + return server.api.getWorldPerfStats(); + } + @Method({ promise: true }) async teleportTo(accountId: string) { const adminAccountId = this.account._id.toString(); await forAllGameServers(server => server.api.teleportTo(adminAccountId, accountId)); diff --git a/src/ts/server/api/internal.ts b/src/ts/server/api/internal.ts index 150c3a3..8cdbee8 100644 --- a/src/ts/server/api/internal.ts +++ b/src/ts/server/api/internal.ts @@ -15,10 +15,11 @@ import { createReloadSettings } from './internal-common'; import { UserError } from '../userError'; import { liveSettings } from '../liveSettings'; import { formatDuration, invalidEnum } from '../../common/utils'; -import { timingEntries } from '../timing'; +import { timingEntries, setTimingEnabled } from '../timing'; import { toPairs, groupBy } from 'lodash'; import { getSizeOfMap } from '../serverMap'; import { teleportTo } from '../playerUtils'; +import { getWorldPerfStats } from '../worldPerfStats'; export const createAccountChanged = (world: World, tokens: TokenService, findAccount: FindAccountSafe) => @@ -269,6 +270,8 @@ export function createInternalApi( shutdownServer: createShutdownServer(world, live), accountHidden: createHiddenStats(hiding), getTimings: async () => timingEntries(), + setTimingEnabled: async (isEnabled: boolean) => setTimingEnabled(isEnabled), + getWorldPerfStats: async () => getWorldPerfStats(), teleportTo: createTeleportTo(world), }; } diff --git a/src/ts/server/playerUtils.ts b/src/ts/server/playerUtils.ts index 51ab199..35441f1 100644 --- a/src/ts/server/playerUtils.ts +++ b/src/ts/server/playerUtils.ts @@ -31,7 +31,8 @@ import { import { replaceEmojis } from '../client/emoji'; import { expression, parseExpression } from '../common/expressionUtils'; import { - canBoopOrKiss, isPonySitting, isPonyStanding, getBoopRect, canStand, isPonyFlying, setPonyState, canSit, canLie, getkissRect, getSneezeRect + canBoopOrKiss, isPonySitting, isPonyStanding, getBoopRect, canStand, isPonyFlying, setPonyState, canSit, + canLie, getkissRect, getSneezeRect } from '../common/entityUtils'; import { withBorder } from '../common/rect'; import { isOnlineFriend } from './services/friends'; @@ -407,8 +408,10 @@ export function useHeldItem(client: IClient) { } } -export function canPerformAction(client: IClient) { - const now = Date.now(); +export function canPerformAction(client: IClient, now?: number) { + if (!now) { + now = Date.now(); + } return client.lastExpressionAction < now && client.lastBoopOrKissAction < now; } @@ -420,9 +423,7 @@ export function updateEntityPlayerState(client: IClient, entity: ServerEntity) { // actions export function turnHead(client: IClient) { - if (canPerformAction(client)) { - updateEntityState(client.pony, client.pony.state ^ EntityState.HeadTurned); - } + updateEntityState(client.pony, client.pony.state ^ EntityState.HeadTurned); } const purpleGrapeTypes = entities.grapesPurple.map(x => x.type); @@ -471,7 +472,7 @@ function boopEntity(client: IClient, rect: Rect, isOnlyBooping: boolean) { } export function boop(client: IClient, now: number) { - if (canPerformAction(client) && canBoopOrKiss(client.pony)) { + if (canPerformAction(client, now) && canBoopOrKiss(client.pony)) { cancelEntityExpression(client.pony); sendAction(client.pony, Action.Boop); boopEntity(client, getBoopRect(client.pony), false); @@ -480,20 +481,21 @@ export function boop(client: IClient, now: number) { } export function kiss(client: IClient, now: number) { - if (canPerformAction(client) && canBoopOrKiss(client.pony)) { + if (canPerformAction(client, now) && canBoopOrKiss(client.pony)) { cancelEntityExpression(client.pony); sendAction(client.pony, Action.Kiss); boopEntity(client, getkissRect(client.pony), false); - client.lastBoopOrKissAction = now + 3350; + client.lastBoopOrKissAction = now + 3400; } } export function sneeze(client: IClient) { - if (canPerformAction(client)) { + const now = Date.now(); + if (canPerformAction(client, now)) { cancelEntityExpression(client.pony); sendAction(client.pony, Action.Sneeze); boopEntity(client, getSneezeRect(client.pony), true); - client.lastExpressionAction = Date.now() + 750; + client.lastExpressionAction = now + 750; } } @@ -557,10 +559,11 @@ export function fly(client: IClient) { } export function expressionAction(client: IClient, action: Action) { - if (canPerformAction(client) && isExpressionAction(action)) { + const now = Date.now(); + if (canPerformAction(client, now) && isExpressionAction(action)) { cancelEntityExpression(client.pony); sendAction(client.pony, action); - client.lastExpressionAction = Date.now() + 750; + client.lastExpressionAction = now + 750; } } @@ -836,7 +839,6 @@ export function switchTool(client: IClient, reverse: boolean) { const newIndex = reverse ? (index === -1 ? tools.length - 1 : index - 1) : ((index + 1) % tools.length); const tool = tools[newIndex]; holdItem(client.pony, tool.type); - console.log('isMobile ' + client.isMobile); const text = (client.isMobile && tool.textMobile) ? tool.textMobile : tool.text; saySystem(client, text); } diff --git a/src/ts/server/serverActions.ts b/src/ts/server/serverActions.ts index 07ebc1b..d30ed88 100644 --- a/src/ts/server/serverActions.ts +++ b/src/ts/server/serverActions.ts @@ -320,6 +320,7 @@ export class ServerActions implements IServerActions, SocketServer { let totalEditableEntities = 0; + // TODO: optimize for (const region of this.map.regions) { for (const entity of region.entities) { if (hasFlag(entity.state, EntityState.Editable)) { diff --git a/src/ts/server/timing.ts b/src/ts/server/timing.ts index 56ffe57..5289c4a 100644 --- a/src/ts/server/timing.ts +++ b/src/ts/server/timing.ts @@ -1,37 +1,41 @@ import { TimingEntry, TimingEntryType } from '../common/adminInterfaces'; +import { MINUTE } from '../common/constants'; +import { counterNow } from '../common/interfaces'; -const ENABLED = true; -const ENTRIES_LIMIT = 50000; +const ENTRIES_LIMIT = 20000; -const entries: TimingEntry[] = []; +let entries: TimingEntry[] = []; let entriesCount = 0; +let isProfilingEnabled = false; +let lastFetchTime = Date.now(); -let now: () => number; - -if (typeof window !== 'undefined') { - now = performance.now; -} else { - const hrtime = process.hrtime; - const getNanoSeconds = () => { - const hr = hrtime(); - return hr[0] * 1e9 + hr[1]; - }; - const nodeLoadTime = getNanoSeconds() - process.uptime() * 1e9; - now = () => (getNanoSeconds() - nodeLoadTime) / 1e6; +export function getTimingEnabled() { + return isProfilingEnabled; } -if (ENABLED) { - for (let i = 0; i < ENTRIES_LIMIT; i++) { - entries.push({ type: 0, time: 0, name: undefined }); +export function setTimingEnabled(isEnabled: boolean) { + if (isProfilingEnabled === isEnabled) { + return; + } + + isProfilingEnabled = isEnabled; + + if (isEnabled) { + for (let i = 0; i < ENTRIES_LIMIT; i++) { + entries.push({ type: 0, time: 0, name: undefined }); + } + } + else { + entries = []; } } export function timingStart(name: string) { - if (ENABLED) { + if (isProfilingEnabled) { if (entriesCount < ENTRIES_LIMIT) { const entry = entries[entriesCount]; entry.type = TimingEntryType.Start; - entry.time = now(); + entry.time = counterNow(); entry.name = name; entriesCount++; } else { @@ -41,11 +45,11 @@ export function timingStart(name: string) { } export function timingEnd() { - if (ENABLED) { + if (isProfilingEnabled) { if (entriesCount < ENTRIES_LIMIT) { const entry = entries[entriesCount]; entry.type = TimingEntryType.End; - entry.time = now(); + entry.time = counterNow(); entry.name = undefined; entriesCount++; } else { @@ -55,11 +59,22 @@ export function timingEnd() { } export function timingReset() { - if (ENABLED) { - entriesCount = 0; - } + entriesCount = 0; } export function timingEntries() { + lastFetchTime = Date.now(); + if (!isProfilingEnabled) { + setTimingEnabled(true); + return []; + } return entries.slice(0, entriesCount); } + +export function timingUpdate() { + if (isProfilingEnabled) { + if (Date.now() - lastFetchTime > MINUTE) { + setTimingEnabled(false); + } + } +} diff --git a/src/ts/server/world.ts b/src/ts/server/world.ts index e1d5255..30cd572 100644 --- a/src/ts/server/world.ts +++ b/src/ts/server/world.ts @@ -1,7 +1,7 @@ import { getWriterBuffer } from 'ag-sockets'; import { remove, compact } from 'lodash'; import { - TileType, WorldState, Season, Holiday, WorldStateFlags, LeaveReason, NotificationFlags, UpdateFlags, Action, + TileType, WorldState, Season, Holiday, WorldStateFlags, LeaveReason, NotificationFlags, UpdateFlags, Action, counterNow, } from '../common/interfaces'; import { removeItem, distance, clamp, randomPoint, includes, fromNow } from '../common/utils'; import { HOUR_LENGTH, DAY_LENGTH } from '../common/timeUtils'; @@ -31,12 +31,12 @@ import { import { roundPosition, roundPositionXMidPixel, roundPositionYMidPixel } from '../common/positionUtils'; import { logger } from './logger'; import { updateCamera, centerCameraOn } from '../common/camera'; -import { timingStart, timingEnd } from './timing'; +import { timingStart, timingEnd, timingUpdate } from './timing'; import { getRegionGlobal, getTile } from '../common/worldMap'; import { getEntityTypeName } from '../common/entities'; import { toFriendOnline, toFriendOffline, FriendsService } from './services/friends'; // import { Pool, createPool } from './pool'; -import { isStaticCollision, getCollisionStats, fixCollision, updatePosition } from '../common/collision'; +import { isStaticCollision, fixCollision, updatePosition } from '../common/collision'; import { HidingService } from './services/hiding'; import { generateRegionCollider } from '../common/region'; import { updateTileIndices } from '../client/tileUtils'; @@ -44,6 +44,7 @@ import { removeEntityFromRegion } from './serverRegion'; import { createIslandMap } from './maps/islandMap'; import { createHouseMap } from './maps/houseMap'; import { updateMainMapSeason } from './maps/mainMap'; +import { updateWorldPerfStats } from './worldPerfStats'; interface MapSwitch { map: ServerMap; @@ -287,8 +288,16 @@ export class World { } } update(delta: number, now: number) { + const statsStart = counterNow(); + let movingEntities = 0; + let regionsCount = 0; + let mapsCount = 0; + let controllersCount = 0; + const started = Date.now(); + timingUpdate(); + timingStart('world.update()'); resetEncodeUpdate(); @@ -315,7 +324,10 @@ export class World { timingStart('update positions'); for (const map of this.maps) { + ++mapsCount; for (const region of map.regions) { + ++regionsCount; + // TODO: update only moving entities, separate list of movingEntities for (const entity of region.movables) { // TODO: make sure timestamp is initialized if entity is moving @@ -323,6 +335,7 @@ export class World { if (delta > 0) { if (entity.vx !== 0 || entity.vy !== 0) { + ++movingEntities; updatePosition(entity, delta, map); } @@ -349,6 +362,7 @@ export class World { for (const map of this.maps) { for (const controller of map.controllers) { + ++controllersCount; controller.update(deltaSeconds, nowSeconds); } } @@ -408,9 +422,7 @@ export class World { totalSays += saysQueue.length; setupTiming(client); - timingStart('client.update()'); client.update(unsubscribes, subscribes, updateBuffer, regionUpdates, saysQueue); - timingEnd(); clearTiming(client); resetClientUpdates(client); @@ -426,17 +438,16 @@ export class World { } timingEnd(); - const { isCollidingCount, isCollidingObjectCount } = getCollisionStats(); - - timingStart(`adds [${clientsWithAdds}]\n` + - `updates [${clientsWithUpdates}]\n` + - `says [${totalSays} / ${clientsWithSays}]\n` + - `sockets [${this.socketStatsText()}]\n` + - `collisions [${isCollidingObjectCount} / ${isCollidingCount}]`); + timingStart(`cleanupOfflineClients`); this.cleanupOfflineClients(); timingEnd(); timingEnd(); + + const { sent, received, sentPackets, receivedPackets } = this.socketStats.stats(); + updateWorldPerfStats(statsStart, this.clients.length, movingEntities, regionsCount, mapsCount, this.joinQueue.length, + controllersCount, clientsWithAdds, clientsWithUpdates, clientsWithSays, totalSays, + sent, received, sentPackets, receivedPackets); } sparseUpdate(now: number) { timingStart('world.sparseUpdate()'); @@ -536,11 +547,6 @@ export class World { timingEnd(); } - private socketStatsText() { - const { sent, received, sentPackets, receivedPackets } = this.socketStats.stats(); - return `sent: ${(sent / 1024).toFixed(2)} kb (${sentPackets}), ` + - `recv: ${(received / 1024).toFixed(2)} kb (${receivedPackets})`; - } updatesStats() { timingStart('updatesStats()'); diff --git a/src/ts/server/worldPerfStats.ts b/src/ts/server/worldPerfStats.ts new file mode 100644 index 0000000..bc6008a --- /dev/null +++ b/src/ts/server/worldPerfStats.ts @@ -0,0 +1,66 @@ +import { defaultWorldPerfStats } from '../common/adminInterfaces'; +import { counterNow } from '../common/interfaces'; +import { getTimingEnabled } from './timing'; + +const FRAME_HISTORY_SIZE = 25; + +let stats = defaultWorldPerfStats(); +const frameHistory: number[] = []; +frameHistory.length = FRAME_HISTORY_SIZE; +frameHistory.fill(-1); +let frameHistoryPtr = 0; + +function addFrameToHistory(time: number) { + frameHistory[frameHistoryPtr++] = time; + if (frameHistoryPtr === FRAME_HISTORY_SIZE) { + frameHistoryPtr = 0; + } +} + +export function updateWorldPerfStats( + frameStart: number, clients: number, movingEntities: number, regionsCount: number, mapsCount: number, clientQueue: number, + controllersCount: number, clientsWithAdds: number, clientsWithUpdates: number, clientsWithSays: number, totalSays: number, + sent: number, received: number, sentPackets: number, receivedPackets: number) { + const time = counterNow() - frameStart; + stats.lastUpdateTime = time; + addFrameToHistory(time); + stats.isSamplingEnabled = getTimingEnabled(); + stats.clients = clients; + stats.movingEntities = movingEntities; + stats.regionsCount = regionsCount; + stats.mapsCount = mapsCount; + stats.clientQueue = clientQueue; + stats.controllersCount = controllersCount; + stats.clientsWithAdds = clientsWithAdds; + stats.clientsWithUpdates = clientsWithUpdates; + stats.clientsWithSays = clientsWithSays; + stats.totalSays = totalSays; + stats.sent = sent; + stats.received = received; + stats.sentPackets = sentPackets; + stats.receivedPackets = receivedPackets; + + let avg = 0, nonZero = 0; + stats.maxUpdateTime = Number.MIN_VALUE; + stats.minUpdateTime = Number.MAX_VALUE; + + for (const f of frameHistory) { + if (f < 0) { + continue; + } + if (f < stats.minUpdateTime) { + stats.minUpdateTime = f; + } + if (f > stats.maxUpdateTime) { + stats.maxUpdateTime = f; + } + avg += f; + ++nonZero; + } + + stats.avgUpdateTime = avg / nonZero; +} + +export function getWorldPerfStats() { + return stats; +} diff --git a/src/ts/tests/generated/sprites.spec.ts b/src/ts/tests/generated/sprites.spec.ts index 84f57f5..42d554b 100644 --- a/src/ts/tests/generated/sprites.spec.ts +++ b/src/ts/tests/generated/sprites.spec.ts @@ -20,7 +20,7 @@ const sets: [keyof typeof sprites, number, number, (number[] | null)[]][] = [ ['hornsBehind', -1, 1, [null, null, null, null, [3, 5], [3, 7], [3, 9, 9], [3, 9, 13], [3, 9, 13], [3, 7], [3, 11, 5, 5], [3, 9, 5, 5], [3, 9, 5], [3, 7], []]], ['ears', -1, 1, [[3, 7], [3, 9], [3, 9], [3, 5], [3, 7, 7], [3, 7, 9]]], ['earsFar', -1, 1, [[3, 7], [3, 9], [3, 9], [3, 5], [3, 7, 7], [3, 7, 9]]], - ['frontLegHooves', 1, 39, [null, [3], [3], [5], [5], [5], [3]]], + ['frontLegHooves', 1, 40, [null, [3], [3], [5], [5], [5], [3]]], ['backLegHooves', 1, 27, [null, [3], [3], [5], [3]]], ['wings', 1, 13, [null, [3, 5, 9], [5], [3, 5, 9], [3]]], ['tails', 0, 3, [null, [3, 11, 13, 7, 5, 5], [3, 13, 9, 7, 5, 5], [3, 11, 9, 7, 5, 5], [3, 13, 11, 7, 5, 5], [3, 9, 13, 7, 5, 5], [3, 11, 13, 7, 5, 5, 5], [5, 13, 11, 7, 7], [3, 11, 13, 5, 7, 5], [3, 11, 11, 7, 5, 5, 11], [3, 11, 13, 5], [3, 11, 11, 7, 5, 5], [3, 9, 9, 7, 5], [3, 5, 9, 5], [5, 11, 9, 7, 7], [5, 7], [3, 5, 7], [5, 11, 9, 7, 7], [7, 13, 11, 9, 9], [5, 7], [3, 11, 13, 7, 5, 5], [3, 9, 11, 7, 5, 5], [3, 11, 13, 7, 5, 5], [3, 9, 11, 7, 5, 5], [3, 13, 7], [3, 5, 13, 13, 11, 11], [3, 9, 13, 13, 13, 11], [3, 7, 5, 13, 5], [5, 13, 9, 9, 7, 7], [5, 13, 9, 9, 7, 7], [3, 5, 7]]], @@ -29,13 +29,13 @@ const sets: [keyof typeof sprites, number, number, (number[] | null)[]][] = [ ['facialHairBehind', -1, 1, [null, [3], [3, 7], null, [3, 7, 9], [3, 5], [3, 7, 9], [3, 7, 17], [3, 17, 7], [3, 13, 7, 7], [3, 13, 7, 9], [3, 13, 7, 9], [3, 13, 7, 9], [5, 13, 9, 13], [5, 13, 9, 13], [3, 17, 7, 11]]], ['headAccessoriesBehind', -1, 1, [null, [3, 5], [3, 9, 9, 9], [3, 5, 7, 9, 7], [3, 5], [3, 7], [3, 7, 13], [3, 11, 11], [3, 11, 13], [3, 11, 7, 9, 9], [3, 9, 13], [5, 11, 13], [3, 7], [3, 9, 13], [3, 9], [3, 9], [3, 7], [3, 7], [3, 7, 5, 5], [3, 7, 11]]], ['neckAccessories', 1, 16, [null, [3, 5], [3, 5, 9, 5], [3, 5, 7, 13, 7], [3, 5], [3, 7, 7, 5, 5], [3, 5, 7], [3, 5], [3], [3], [3, 5, 9, 5], [3, 5, 9], [3, 9], [3], [3]]], - ['frontLegAccessories', 1, 39, [null, [3, 13, 13, 13, 13]]], + ['frontLegAccessories', 1, 40, [null, [3, 13, 13, 13, 13]]], ['backLegAccessories', 1, 27, [null, [3, 13, 13, 13, 13]]], ['chestAccessories', 1, 16, [null, [3, 5, 9], [3, 7, 5, 5, 7, 5, 7, 5, 7, 11, 9, 5, 11], [5, 7, 11], [5, 7]]], ['backAccessories', 1, 16, [null, [3], [3, 7, 13, 5], [3, 7, 13, 5], [3, 9, 13, 5], [3, 3]]], ['waistAccessories', 1, 17, [null, [9], [9], [9]]], - ['earAccessories', -1, 1, [null, [3], [3], [3], [3], [5], [3, 11], [3], [3], [3], [3, 7, 7, 7], [3, 11, 11, 11], [3]]], - ['earAccessoriesBehind', -1, 1, [null, null, null, null, null, null, null, null, [3], null, [3, 7, 7, 7], [3, 11, 11, 11], []]], + ['earAccessories', -1, 1, [null, [3], [3], [3], [3], [5], [3, 11], [3], [3], [3], [3, 7, 7, 7], [3, 11, 11, 11], [3], []]], + ['earAccessoriesBehind', -1, 1, [null, null, null, null, null, null, null, null, [3], null, [3, 7, 7, 7], [3, 11, 11, 11], [], []]], ['extraAccessories', -1, 1, [[11], [5], [5], [7], [9], [13], [5], [11], [9], [11, 11], [9], [9, 9], [7], [9], null, null, [13], [13]]], ['extraAccessoriesBehind', -1, 1, [[11], [5], null, [7], [9], null, null, null, null, null, null, null, [7], [9], [5], [5], [13], []]], ]; diff --git a/src/ts/tests/server/playerUtils.spec.ts b/src/ts/tests/server/playerUtils.spec.ts index aa090ad..543ed57 100644 --- a/src/ts/tests/server/playerUtils.spec.ts +++ b/src/ts/tests/server/playerUtils.spec.ts @@ -114,6 +114,7 @@ describe('playerUtils', () => { account, character, ip: '', + isMobile: false, map, isSwitchingMap: false, pony, @@ -132,7 +133,7 @@ describe('playerUtils', () => { safeX: 10, safeY: 20, lastPacket: 123, - lastBoopAction: 0, + lastBoopOrKissAction: 0, lastExpressionAction: 0, lastX: 10, lastY: 20, @@ -591,7 +592,7 @@ describe('playerUtils', () => { boop(client, 100); - expect(client.lastBoopOrKissAction).equal(100 + 500); + expect(client.lastBoopOrKissAction).equal(100 + 850); }); it('executes boop on found entity', () => { @@ -615,6 +616,7 @@ describe('playerUtils', () => { boop(client, 0); assert.notCalled(stubBoop); + expect(client.pony.region!.entityUpdates.length).eql(0); }); it('does nothing if cannot perform action', () => { @@ -622,7 +624,7 @@ describe('playerUtils', () => { boop(client, 0); - expect(client.pony.region!.entityUpdates).eql([]); + expect(client.pony.region!.entityUpdates.length).eql(0); }); it('does nothing if moving', () => { @@ -630,7 +632,7 @@ describe('playerUtils', () => { boop(client, 0); - expect(client.pony.region!.entityUpdates).eql([]); + expect(client.pony.region!.entityUpdates.length).eql(0); }); }); @@ -663,6 +665,8 @@ describe('playerUtils', () => { it('updates entity flag to standing', () => { client.pony.state = EntityState.PonySitting; + client.lastBoopOrKissAction = 0; + client.lastExpressionAction = 0; stand(client); @@ -882,6 +886,8 @@ describe('playerUtils', () => { }); it('sends given action', () => { + client.lastExpressionAction = 0; + client.lastBoopOrKissAction = 0; expressionAction(client, Action.Yawn); expect(client.pony.region!.entityUpdates).eql([ @@ -896,7 +902,7 @@ describe('playerUtils', () => { }); it('does nothing if cannot perform action', () => { - client.lastExpressionAction = Date.now() + 1000; + client.lastExpressionAction = Date.now() + 2500; expressionAction(client, Action.Yawn); @@ -913,6 +919,7 @@ describe('playerUtils', () => { it('updates last expression action', () => { client.lastExpressionAction = 0; + client.lastBoopOrKissAction = 0; expressionAction(client, Action.Yawn);