From 2077b4c8edc354227560c2282234a33ae51e7db5 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sat, 24 Jan 2026 14:56:23 +0000 Subject: [PATCH] =?UTF-8?q?=E2=9A=A1=20Optimize=20terrain=20page=20generat?= =?UTF-8?q?ion=20loops?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Implement fast path for parsing single-value strings - Optimize loops for flat/uniform terrain nodes - Hoist loop invariants - Add regression tests --- .../src/world/world.service.terrain.spec.ts | 173 ++++++++++++++++++ backend/src/world/world.service.ts | 66 +++++-- package-lock.json | 32 +--- 3 files changed, 227 insertions(+), 44 deletions(-) create mode 100644 backend/src/world/world.service.terrain.spec.ts diff --git a/backend/src/world/world.service.terrain.spec.ts b/backend/src/world/world.service.terrain.spec.ts new file mode 100644 index 0000000..7e73993 --- /dev/null +++ b/backend/src/world/world.service.terrain.spec.ts @@ -0,0 +1,173 @@ +import {Test, TestingModule} from '@nestjs/testing' +import {WorldService} from './world.service' +import {DbService} from '../db/db.service' +import {CACHE_MANAGER} from '@nestjs/cache-manager' + +const mockDbService = { + elev: { + findMany: vi.fn() + }, + prop: { + findMany: vi.fn() + }, + world: { + findMany: vi.fn(), + findFirst: vi.fn() + } +} + +const mockCache = { + get: vi.fn(), + set: vi.fn() +} + +describe('WorldService Terrain Logic', () => { + let service: WorldService + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + WorldService, + {provide: DbService, useValue: mockDbService}, + {provide: CACHE_MANAGER, useValue: mockCache} + ] + }).compile() + + service = module.get(WorldService) + vi.clearAllMocks() + }) + + it('should process a flat node correctly', async () => { + mockCache.get.mockResolvedValue(null) + mockDbService.elev.findMany.mockResolvedValue([ + { + node_x: 0, + node_z: 0, + radius: 4, // width 8 + textures: '5', + heights: '10' + } + ]) + + const result = await service.getTerrainPage(1, 0, 0) + + // Check a few cells + // node_x=0, node_z=0. Cell index = row + j + 0 + 0. + // row = i * 128. + // i=0, j=0 => cell 0. + // i=0, j=7 => cell 7. + // i=7, j=0 => cell 7*128 = 896. + expect(result![0]).toEqual([5, 10]) + expect(result![7]).toEqual([5, 10]) + expect(result![896]).toEqual([5, 10]) + + // Check total count. 8x8 = 64 cells should be set. + expect(Object.keys(result!).length).toBe(64) + }) + + it('should process a detailed node correctly', async () => { + mockCache.get.mockResolvedValue(null) + // Create a 2x2 node (radius 1) + // Width = 2. + // Textures: "1 2 3 4" -> + // i=0, j=0 (idx 0) -> 1 + // i=0, j=1 (idx 1) -> 2 + // i=1, j=0 (idx 2) -> 3 + // i=1, j=1 (idx 3) -> 4 + + mockDbService.elev.findMany.mockResolvedValue([ + { + node_x: 10, + node_z: 10, + radius: 1, + textures: '1 2 3 4', + heights: '10 20 30 40' + } + ]) + + const result = await service.getTerrainPage(1, 0, 0) + + // Offsets: node_x=10, node_z=10 -> zOffset = 1280. + // i=0, row=0. cell = 0 + 0 + 10 + 1280 = 1290. val: 1, 10 + // i=0, row=0. cell = 0 + 1 + 10 + 1280 = 1291. val: 2, 20 + // i=1, row=128. cell = 128 + 0 + 10 + 1280 = 1418. val: 3, 30 + // i=1, row=128. cell = 128 + 1 + 10 + 1280 = 1419. val: 4, 40 + + expect(result![1290]).toEqual([1, 10]) + expect(result![1291]).toEqual([2, 20]) + expect(result![1418]).toEqual([3, 30]) + expect(result![1419]).toEqual([4, 40]) + }) + + it('should ignore empty cells (0,0)', async () => { + mockCache.get.mockResolvedValue(null) + mockDbService.elev.findMany.mockResolvedValue([ + { + node_x: 0, + node_z: 0, + radius: 1, // width 2 + textures: '0 1 0 1', + heights: '0 1 0 1' + } + ]) + + const result = await service.getTerrainPage(1, 0, 0) + // idx 0: t=0, h=0 -> skip + // idx 1: t=1, h=1 -> set + // idx 2: t=0, h=0 -> skip + // idx 3: t=1, h=1 -> set + + expect(Object.keys(result!).length).toBe(2) + }) + + it('should handle partial arrays by repeating the first element (legacy behavior)', async () => { + mockCache.get.mockResolvedValue(null) + mockDbService.elev.findMany.mockResolvedValue([ + { + node_x: 0, + node_z: 0, + radius: 1, // width 2 + textures: '5', // length 1 + heights: '10' // length 1 + } + ]) + + // Even though arrays are length 1, if it wasn't caught by the "flat" optimization (e.g. if one was length 1 and other length 2?), + // the logic falls back to checking length. + // But here textures='5' -> length 1. + // Wait, if textures='5', heights='10', it hits the flat path. + + // Let's try mixed case: textures='5', heights='10 20'. + // This won't hit the flat path (both must be len 1). + // Logic: idx < tLen ? textures[idx] : textures[0] + + mockDbService.elev.findMany.mockResolvedValue([ + { + node_x: 0, + node_z: 0, + radius: 1, // width 2 + textures: '5', + heights: '10 20' + } + ]) + + const result = await service.getTerrainPage(1, 0, 0) + + // i=0, j=0. idx=0. t=5, h=10. + // i=0, j=1. idx=1. t=5 (fallback), h=20. + // i=1, j=0. idx=2. t=5, h=10 (fallback? no, heights has only 2 elements? Wait). + // heights='10 20' -> [10, 20]. Len 2. + // idx 2. 2 < 2 is False. So heights[0] -> 10. + // idx 3. 3 < 2 is False. So heights[0] -> 10. + + // cell 0: [5, 10] + // cell 1: [5, 20] + // cell 128: [5, 10] + // cell 129: [5, 10] + + expect(result![0]).toEqual([5, 10]) + expect(result![1]).toEqual([5, 20]) + expect(result![128]).toEqual([5, 10]) + expect(result![129]).toEqual([5, 10]) + }) +}) diff --git a/backend/src/world/world.service.ts b/backend/src/world/world.service.ts index 5a3478b..e556f01 100644 --- a/backend/src/world/world.service.ts +++ b/backend/src/world/world.service.ts @@ -106,23 +106,57 @@ export class WorldService { } })) { const width = elev.radius * 2 - const textures = (elev.textures ?? '') - .split(' ') - .map((n: string) => parseInt(n)) - const heights = (elev.heights ?? '') - .split(' ') - .map((n: string) => parseInt(n)) - for (let i = 0; i < width; i++) { - const row = i * 128 - for (let j = 0; j < width; j++) { - const idx = width * i + j - const texture = idx < textures.length ? textures[idx] : textures[0] - const height = idx < heights.length ? heights[idx] : heights[0] - if (texture === 0 && height === 0) { - continue + let textures: number[] + if (!elev.textures || elev.textures.length === 0) { + textures = [0] + } else if (elev.textures.indexOf(' ') === -1) { + textures = [parseInt(elev.textures)] + } else { + textures = elev.textures.split(' ').map((n: string) => parseInt(n)) + } + let heights: number[] + if (!elev.heights || elev.heights.length === 0) { + heights = [0] + } else if (elev.heights.indexOf(' ') === -1) { + heights = [parseInt(elev.heights)] + } else { + heights = elev.heights.split(' ').map((n: string) => parseInt(n)) + } + + const zOffset = elev.node_z * 128 + const nodeX = elev.node_x + + if (textures.length === 1 && heights.length === 1) { + const t = textures[0] + const h = heights[0] + if (t === 0 && h === 0) { + continue + } + for (let i = 0; i < width; i++) { + const row = i * 128 + for (let j = 0; j < width; j++) { + const cell = row + j + nodeX + zOffset + page[cell] = [t, h] + } + } + } else { + const tLen = textures.length + const hLen = heights.length + const t0 = textures[0] + const h0 = heights[0] + for (let i = 0; i < width; i++) { + const row = i * 128 + const baseIdx = width * i + for (let j = 0; j < width; j++) { + const idx = baseIdx + j + const texture = idx < tLen ? textures[idx] : t0 + const height = idx < hLen ? heights[idx] : h0 + if (texture === 0 && height === 0) { + continue + } + const cell = row + j + nodeX + zOffset + page[cell] = [texture, height] } - const cell = row + j + elev.node_x + elev.node_z * 128 - page[cell] = [texture, height] } } } diff --git a/package-lock.json b/package-lock.json index f441575..c2ea146 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1390,7 +1390,7 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -1433,7 +1433,7 @@ "version": "7.28.5", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/types": "^7.28.5" @@ -1483,7 +1483,7 @@ "version": "7.28.5", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", @@ -5685,20 +5685,6 @@ "url": "https://dotenvx.com" } }, - "node_modules/@prisma/config/node_modules/magicast": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", - "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@babel/parser": "^7.25.4", - "@babel/types": "^7.25.4", - "source-map-js": "^1.2.0" - } - }, "node_modules/@prisma/debug": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.2.0.tgz", @@ -6720,7 +6706,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": ">=10" } @@ -6738,7 +6723,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": ">=10" } @@ -6756,7 +6740,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=10" } @@ -6774,7 +6757,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=10" } @@ -6792,7 +6774,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=10" } @@ -6810,7 +6791,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=10" } @@ -6828,7 +6808,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=10" } @@ -6846,7 +6825,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">=10" } @@ -6864,7 +6842,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">=10" } @@ -6882,7 +6859,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">=10" } @@ -15939,7 +15915,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, + "devOptional": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0"