Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
173 changes: 173 additions & 0 deletions backend/src/world/world.service.terrain.spec.ts
Original file line number Diff line number Diff line change
@@ -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>(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])

Check failure on line 60 in backend/src/world/world.service.terrain.spec.ts

View workflow job for this annotation

GitHub Actions / backend (24.x)

Forbidden non-null assertion

Check failure on line 60 in backend/src/world/world.service.terrain.spec.ts

View workflow job for this annotation

GitHub Actions / backend (22.x)

Forbidden non-null assertion
expect(result![7]).toEqual([5, 10])

Check failure on line 61 in backend/src/world/world.service.terrain.spec.ts

View workflow job for this annotation

GitHub Actions / backend (24.x)

Forbidden non-null assertion

Check failure on line 61 in backend/src/world/world.service.terrain.spec.ts

View workflow job for this annotation

GitHub Actions / backend (22.x)

Forbidden non-null assertion
expect(result![896]).toEqual([5, 10])

Check failure on line 62 in backend/src/world/world.service.terrain.spec.ts

View workflow job for this annotation

GitHub Actions / backend (24.x)

Forbidden non-null assertion

Check failure on line 62 in backend/src/world/world.service.terrain.spec.ts

View workflow job for this annotation

GitHub Actions / backend (22.x)

Forbidden non-null assertion

// Check total count. 8x8 = 64 cells should be set.
expect(Object.keys(result!).length).toBe(64)

Check failure on line 65 in backend/src/world/world.service.terrain.spec.ts

View workflow job for this annotation

GitHub Actions / backend (24.x)

Forbidden non-null assertion

Check failure on line 65 in backend/src/world/world.service.terrain.spec.ts

View workflow job for this annotation

GitHub Actions / backend (22.x)

Forbidden non-null assertion
})

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])

Check failure on line 96 in backend/src/world/world.service.terrain.spec.ts

View workflow job for this annotation

GitHub Actions / backend (24.x)

Forbidden non-null assertion

Check failure on line 96 in backend/src/world/world.service.terrain.spec.ts

View workflow job for this annotation

GitHub Actions / backend (22.x)

Forbidden non-null assertion
expect(result![1291]).toEqual([2, 20])

Check failure on line 97 in backend/src/world/world.service.terrain.spec.ts

View workflow job for this annotation

GitHub Actions / backend (24.x)

Forbidden non-null assertion

Check failure on line 97 in backend/src/world/world.service.terrain.spec.ts

View workflow job for this annotation

GitHub Actions / backend (22.x)

Forbidden non-null assertion
expect(result![1418]).toEqual([3, 30])

Check failure on line 98 in backend/src/world/world.service.terrain.spec.ts

View workflow job for this annotation

GitHub Actions / backend (24.x)

Forbidden non-null assertion

Check failure on line 98 in backend/src/world/world.service.terrain.spec.ts

View workflow job for this annotation

GitHub Actions / backend (22.x)

Forbidden non-null assertion
expect(result![1419]).toEqual([4, 40])

Check failure on line 99 in backend/src/world/world.service.terrain.spec.ts

View workflow job for this annotation

GitHub Actions / backend (24.x)

Forbidden non-null assertion

Check failure on line 99 in backend/src/world/world.service.terrain.spec.ts

View workflow job for this annotation

GitHub Actions / backend (22.x)

Forbidden non-null assertion
})

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)

Check failure on line 120 in backend/src/world/world.service.terrain.spec.ts

View workflow job for this annotation

GitHub Actions / backend (24.x)

Forbidden non-null assertion

Check failure on line 120 in backend/src/world/world.service.terrain.spec.ts

View workflow job for this annotation

GitHub Actions / backend (22.x)

Forbidden non-null assertion
})

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])

Check failure on line 168 in backend/src/world/world.service.terrain.spec.ts

View workflow job for this annotation

GitHub Actions / backend (24.x)

Forbidden non-null assertion

Check failure on line 168 in backend/src/world/world.service.terrain.spec.ts

View workflow job for this annotation

GitHub Actions / backend (22.x)

Forbidden non-null assertion
expect(result![1]).toEqual([5, 20])
expect(result![128]).toEqual([5, 10])
expect(result![129]).toEqual([5, 10])
})
})
66 changes: 50 additions & 16 deletions backend/src/world/world.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,47 +87,81 @@
return props
}

async getTerrainPage(wid: number, pageX: number, pageZ: number) {
const cacheKey = `T-${wid}-${pageX}-${pageZ}`
let page: Partial<Record<number, [number, number]>> | undefined =
await this.cache.get(cacheKey)
if (page == null) {
page = {}
for (const elev of await this.db.elev.findMany({
select: {
node_x: true,
node_z: true,
radius: true,
textures: true,
heights: true
},
where: {
AND: [{wid}, {page_x: pageX}, {page_z: pageZ}]
}
})) {
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]
}
}
}
await this.cache.set(cacheKey, page)
}
return page
}

Check notice on line 166 in backend/src/world/world.service.ts

View check run for this annotation

codefactor.io / CodeFactor

backend/src/world/world.service.ts#L90-L166

Complex Method
}
32 changes: 4 additions & 28 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading