feat:移除了弹窗,服务器添加sls

This commit is contained in:
2026-09-08 22:39:45 +08:00
commit 6a295f9a7a
4082 changed files with 1322534 additions and 0 deletions

View File

@ -0,0 +1,255 @@
import { invoke } from '@tauri-apps/api/core'
import type { SeedMapDimension } from './biomes.ts'
import type { SeedMapFeature, SeedMapFeatureKind } from './features.ts'
import type { SeedMapOreHit, SeedMapOreKind } from './ores.ts'
export type SeedMapEdition = 'java' | 'java-large-biomes'
export type SeedMapVersionProfile = {
edition: SeedMapEdition
version: string
available: boolean
dimensions: SeedMapDimension[]
ores: boolean
note?: string
}
export type SeedMapTileRequest = {
epoch: number
seed: string
edition: SeedMapEdition
version: string
dimension: SeedMapDimension
x: number
z: number
scale: number
width: number
height: number
elevation?: number
terrain: boolean
contours: boolean
highlightBiomes?: number[]
}
export type SeedMapTile = {
epoch: number
width: number
height: number
bitmap: ImageBitmap
approximate: boolean
}
export type SeedMapFeatureQuery = {
seed: string
edition: SeedMapEdition
version: string
dimension: SeedMapDimension
minX: number
minZ: number
maxX: number
maxZ: number
featureMask?: number
}
export type SeedMapSpawnPoint = {
x: number
z: number
approximate: boolean
}
export type SeedMapLevelDat = {
seed: string
version?: string
}
export type SeedMapOreScanChunk = {
cx: number
cz: number
hits: SeedMapOreHit[]
}
/**
* Versions the bundled cubiomes engine understands, newest first. Releases
* since the 1.21 winter drop (26.x and the late 1.21.x patches) share the
* same world generation and all run on the newest bundled engine.
*/
export const SEED_MAP_FALLBACK_VERSIONS: readonly string[] = [
'26.2',
'26.1.2',
'26.1',
'1.21.9',
'1.21.6',
'1.21.5',
'1.21.4',
'1.21.3',
'1.21.1',
'1.20',
'1.19.4',
'1.19.2',
'1.18',
'1.17',
'1.16',
'1.15',
'1.14',
'1.13',
'1.12',
'1.11',
'1.10',
'1.9',
'1.8',
'1.7',
'1.6',
'1.5',
'1.4',
'1.3',
'1.2',
'1.1',
'1.0',
]
export const SEED_MAP_DEFAULT_VERSION = SEED_MAP_FALLBACK_VERSIONS[0]
export function fallbackSeedMapProfiles(): SeedMapVersionProfile[] {
const profiles: SeedMapVersionProfile[] = []
for (const edition of ['java', 'java-large-biomes'] as const) {
for (const version of SEED_MAP_FALLBACK_VERSIONS) {
if (edition === 'java-large-biomes' && compareToMinor(version, 1, 3) < 0) continue
const dimensions: SeedMapDimension[] = ['overworld']
if (compareToMinor(version, 1, 16) >= 0) dimensions.push('nether')
if (compareToMinor(version, 1, 9) >= 0) dimensions.push('end')
profiles.push({
edition,
version,
available: true,
dimensions,
ores: compareToMinor(version, 1, 18) >= 0,
})
}
}
return profiles
}
function compareToMinor(version: string, major: number, minor: number): number {
const [versionMajor = 0, versionMinor = 0] = version.split('.').map(Number)
if (versionMajor !== major) return versionMajor - major
return versionMinor - minor
}
export async function getSeedMapProfiles(): Promise<SeedMapVersionProfile[]> {
return await invoke<SeedMapVersionProfile[]>('plugin:seed-map|seed_map_profiles')
}
/**
* Tiles render natively; this cap only bounds how many blocking renders are
* in flight at once so pans stay responsive while tiles stream in.
*/
export function seedMapTileConcurrency(
scale: number,
hardwareConcurrency = typeof navigator === 'undefined' ? 4 : navigator.hardwareConcurrency || 4,
): number {
const cores = Math.max(1, Math.floor(hardwareConcurrency))
const limit = Math.max(2, Math.min(8, cores - 2))
return scale <= 1 ? Math.min(4, limit) : limit
}
export async function renderSeedMapTile(request: SeedMapTileRequest): Promise<SeedMapTile> {
const pixels = await invoke<ArrayBuffer>('plugin:seed-map|seed_map_render_tile', {
request: {
seed: request.seed,
edition: request.edition,
version: request.version,
dimension: request.dimension,
x: request.x,
z: request.z,
scale: request.scale,
width: request.width,
height: request.height,
elevation: request.elevation ?? 128,
terrain: request.terrain,
contours: request.contours,
highlightBiomes: request.highlightBiomes ?? null,
},
})
const expectedLength = request.width * request.height * 4
if (pixels.byteLength !== expectedLength) {
throw new Error('The seed map backend returned an unexpected tile size.')
}
const image = new ImageData(new Uint8ClampedArray(pixels), request.width, request.height)
const bitmap = await createImageBitmap(image, {
colorSpaceConversion: 'none',
premultiplyAlpha: 'none',
})
return {
epoch: request.epoch,
width: request.width,
height: request.height,
bitmap,
approximate: request.terrain || request.contours,
}
}
export async function findSeedMapFeatures(query: SeedMapFeatureQuery): Promise<SeedMapFeature[]> {
const features = await invoke<
{ kind: string; x: number; z: number; approximate: boolean; endShip?: boolean }[]
>('plugin:seed-map|seed_map_find_features', {
query: {
seed: query.seed,
edition: query.edition,
version: query.version,
dimension: query.dimension,
minX: query.minX,
minZ: query.minZ,
maxX: query.maxX,
maxZ: query.maxZ,
featureMask: query.featureMask ?? null,
},
})
return features.map((feature) => ({
kind: feature.kind as SeedMapFeatureKind,
x: feature.x,
z: feature.z,
approximate: feature.approximate,
endShip: feature.endShip,
}))
}
export async function getSeedMapSpawn(
seed: string,
edition: SeedMapEdition,
version: string,
): Promise<SeedMapSpawnPoint> {
return await invoke<SeedMapSpawnPoint>('plugin:seed-map|seed_map_spawn', {
seed,
edition,
version,
})
}
export async function getSeedMapBiomeAt(query: {
seed: string
edition: SeedMapEdition
version: string
dimension: SeedMapDimension
x: number
y: number
z: number
}): Promise<number> {
return await invoke<number>('plugin:seed-map|seed_map_biome_at', query)
}
export async function scanSeedMapOres(request: {
seed: string
version: string
dimension: SeedMapDimension
ores: SeedMapOreKind[]
chunks: number[]
}): Promise<SeedMapOreScanChunk[]> {
return await invoke<SeedMapOreScanChunk[]>('plugin:seed-map|seed_map_scan_ores', {
request,
})
}
export async function readSeedMapLevelDat(path: string): Promise<SeedMapLevelDat> {
return await invoke<SeedMapLevelDat>('plugin:seed-map|seed_map_read_level_dat', { path })
}

View File

@ -0,0 +1,197 @@
export type SeedMapDimension = 'overworld' | 'nether' | 'end'
export type SeedMapBiomeCategory =
| 'beach'
| 'cave'
| 'desert'
| 'forest'
| 'ice'
| 'jungle'
| 'mesa'
| 'mountains'
| 'mushroom'
| 'ocean'
| 'plains'
| 'river'
| 'savanna'
| 'swamp'
| 'taiga'
| 'nether'
| 'end'
export type SeedMapBiome = {
id: number
dimensions: readonly SeedMapDimension[]
category: SeedMapBiomeCategory
color: string
}
export type SeedMapBiomeGroup = {
category: SeedMapBiomeCategory
dimension: SeedMapDimension
biomes: SeedMapBiome[]
}
const OVERWORLD: readonly SeedMapDimension[] = ['overworld']
const NETHER: readonly SeedMapDimension[] = ['nether']
const END: readonly SeedMapDimension[] = ['end']
/**
* Java biome ids as used by cubiomes, grouped for the picker. Colors mirror
* the native renderer's cubiomes palette so the picker and map stay aligned.
*/
export const SEED_MAP_BIOMES: readonly SeedMapBiome[] = [
{ id: 16, dimensions: OVERWORLD, category: 'beach', color: '#FADE55' },
{ id: 26, dimensions: OVERWORLD, category: 'beach', color: '#FAF0C0' },
{ id: 25, dimensions: OVERWORLD, category: 'beach', color: '#A2A284' },
{ id: 183, dimensions: OVERWORLD, category: 'cave', color: '#031F29' },
{ id: 174, dimensions: OVERWORLD, category: 'cave', color: '#4E3012' },
{ id: 175, dimensions: OVERWORLD, category: 'cave', color: '#283C00' },
{ id: 187, dimensions: OVERWORLD, category: 'cave', color: '#C8C828' },
{ id: 2, dimensions: OVERWORLD, category: 'desert', color: '#FA9418' },
{ id: 27, dimensions: OVERWORLD, category: 'forest', color: '#307444' },
{ id: 185, dimensions: OVERWORLD, category: 'forest', color: '#FF91C8' },
{ id: 29, dimensions: OVERWORLD, category: 'forest', color: '#40511A' },
{ id: 132, dimensions: OVERWORLD, category: 'forest', color: '#2D8E49' },
{ id: 4, dimensions: OVERWORLD, category: 'forest', color: '#056621' },
{ id: 178, dimensions: OVERWORLD, category: 'forest', color: '#47726C' },
{ id: 155, dimensions: OVERWORLD, category: 'forest', color: '#589C6C' },
{ id: 186, dimensions: OVERWORLD, category: 'forest', color: '#696D95' },
{ id: 34, dimensions: OVERWORLD, category: 'forest', color: '#5B7352' },
{ id: 181, dimensions: OVERWORLD, category: 'ice', color: '#B0B3CE' },
{ id: 11, dimensions: OVERWORLD, category: 'ice', color: '#A0A0FF' },
{ id: 140, dimensions: OVERWORLD, category: 'ice', color: '#B4DCDC' },
{ id: 168, dimensions: OVERWORLD, category: 'jungle', color: '#849500' },
{ id: 21, dimensions: OVERWORLD, category: 'jungle', color: '#507B0A' },
{ id: 23, dimensions: OVERWORLD, category: 'jungle', color: '#60930F' },
{ id: 37, dimensions: OVERWORLD, category: 'mesa', color: '#D94515' },
{ id: 165, dimensions: OVERWORLD, category: 'mesa', color: '#FF6D3D' },
{ id: 38, dimensions: OVERWORLD, category: 'mesa', color: '#B09765' },
{ id: 180, dimensions: OVERWORLD, category: 'mountains', color: '#DCDCC8' },
{ id: 177, dimensions: OVERWORLD, category: 'mountains', color: '#60A445' },
{ id: 179, dimensions: OVERWORLD, category: 'mountains', color: '#C4C4C4' },
{ id: 182, dimensions: OVERWORLD, category: 'mountains', color: '#7B8F74' },
{ id: 131, dimensions: OVERWORLD, category: 'mountains', color: '#888888' },
{ id: 3, dimensions: OVERWORLD, category: 'mountains', color: '#606060' },
{ id: 14, dimensions: OVERWORLD, category: 'mushroom', color: '#FF00FF' },
{ id: 46, dimensions: OVERWORLD, category: 'ocean', color: '#202070' },
{ id: 49, dimensions: OVERWORLD, category: 'ocean', color: '#202038' },
{ id: 50, dimensions: OVERWORLD, category: 'ocean', color: '#404090' },
{ id: 48, dimensions: OVERWORLD, category: 'ocean', color: '#000040' },
{ id: 24, dimensions: OVERWORLD, category: 'ocean', color: '#000030' },
{ id: 10, dimensions: OVERWORLD, category: 'ocean', color: '#7070D6' },
{ id: 45, dimensions: OVERWORLD, category: 'ocean', color: '#000090' },
{ id: 0, dimensions: OVERWORLD, category: 'ocean', color: '#000070' },
{ id: 44, dimensions: OVERWORLD, category: 'ocean', color: '#0000AC' },
{ id: 1, dimensions: OVERWORLD, category: 'plains', color: '#8DB360' },
{ id: 12, dimensions: OVERWORLD, category: 'plains', color: '#FFFFFF' },
{ id: 129, dimensions: OVERWORLD, category: 'plains', color: '#B5DB88' },
{ id: 7, dimensions: OVERWORLD, category: 'river', color: '#0000FF' },
{ id: 35, dimensions: OVERWORLD, category: 'savanna', color: '#BDB25F' },
{ id: 36, dimensions: OVERWORLD, category: 'savanna', color: '#A79D64' },
{ id: 163, dimensions: OVERWORLD, category: 'savanna', color: '#E5DA87' },
{ id: 184, dimensions: OVERWORLD, category: 'swamp', color: '#2CCC8E' },
{ id: 6, dimensions: OVERWORLD, category: 'swamp', color: '#07F9B2' },
{ id: 32, dimensions: OVERWORLD, category: 'taiga', color: '#596651' },
{ id: 160, dimensions: OVERWORLD, category: 'taiga', color: '#818E79' },
{ id: 30, dimensions: OVERWORLD, category: 'taiga', color: '#31554A' },
{ id: 5, dimensions: OVERWORLD, category: 'taiga', color: '#0B6A5F' },
{ id: 8, dimensions: NETHER, category: 'nether', color: '#572526' },
{ id: 170, dimensions: NETHER, category: 'nether', color: '#4D3A2E' },
{ id: 171, dimensions: NETHER, category: 'nether', color: '#981A11' },
{ id: 172, dimensions: NETHER, category: 'nether', color: '#49907B' },
{ id: 173, dimensions: NETHER, category: 'nether', color: '#645F63' },
{ id: 9, dimensions: END, category: 'end', color: '#8080FF' },
{ id: 40, dimensions: END, category: 'end', color: '#4B4BAB' },
{ id: 41, dimensions: END, category: 'end', color: '#C9C959' },
{ id: 42, dimensions: END, category: 'end', color: '#B5B536' },
{ id: 43, dimensions: END, category: 'end', color: '#7070CC' },
]
/** Display names for the Java biome ids shown in the picker. */
export const SEED_MAP_BIOME_NAMES: Readonly<Record<number, string>> = {
0: 'Ocean',
1: 'Plains',
2: 'Desert',
3: 'Windswept Hills',
4: 'Forest',
5: 'Taiga',
6: 'Swamp',
7: 'River',
8: 'Nether Wastes',
9: 'The End',
10: 'Frozen Ocean',
11: 'Frozen River',
12: 'Snowy Plains',
14: 'Mushroom Fields',
16: 'Beach',
21: 'Jungle',
23: 'Sparse Jungle',
24: 'Deep Ocean',
25: 'Stony Shore',
26: 'Snowy Beach',
27: 'Birch Forest',
29: 'Dark Forest',
30: 'Snowy Taiga',
32: 'Old Growth Pine Taiga',
34: 'Windswept Forest',
35: 'Savanna',
36: 'Savanna Plateau',
37: 'Badlands',
38: 'Wooded Badlands',
40: 'Small End Islands',
41: 'End Midlands',
42: 'End Highlands',
43: 'End Barrens',
44: 'Warm Ocean',
45: 'Lukewarm Ocean',
46: 'Cold Ocean',
48: 'Deep Lukewarm Ocean',
49: 'Deep Cold Ocean',
50: 'Deep Frozen Ocean',
129: 'Sunflower Plains',
131: 'Windswept Gravelly Hills',
132: 'Flower Forest',
140: 'Ice Spikes',
155: 'Old Growth Birch Forest',
160: 'Old Growth Spruce Taiga',
163: 'Windswept Savanna',
165: 'Eroded Badlands',
168: 'Bamboo Jungle',
170: 'Soul Sand Valley',
171: 'Crimson Forest',
172: 'Warped Forest',
173: 'Basalt Deltas',
174: 'Dripstone Caves',
175: 'Lush Caves',
177: 'Meadow',
178: 'Grove',
179: 'Snowy Slopes',
180: 'Jagged Peaks',
181: 'Frozen Peaks',
182: 'Stony Peaks',
183: 'Deep Dark',
184: 'Mangrove Swamp',
185: 'Cherry Grove',
186: 'Pale Garden',
187: 'Sulfur Caves',
}
export function seedMapBiomeGroups(): SeedMapBiomeGroup[] {
const groups = new Map<SeedMapBiomeCategory, SeedMapBiomeGroup>()
for (const biome of SEED_MAP_BIOMES) {
const dimension = biome.dimensions[0] ?? 'overworld'
const group = groups.get(biome.category) ?? {
category: biome.category,
dimension,
biomes: [],
}
group.biomes.push(biome)
groups.set(biome.category, group)
}
return [...groups.values()]
}
export function seedMapBiomeSlug(name: string): string {
return name.toLocaleLowerCase().replaceAll(' ', '-')
}

View File

@ -0,0 +1,103 @@
import type { SeedMapDimension } from './biomes.ts'
export type SeedMapFeatureKind =
| 'village'
| 'outpost'
| 'shipwreck'
| 'monument'
| 'mansion'
| 'ancient-city'
| 'trail-ruins'
| 'trial-chambers'
| 'ruined-portal'
| 'stronghold'
| 'slime-chunk'
| 'desert-pyramid'
| 'jungle-temple'
| 'swamp-hut'
| 'igloo'
| 'ocean-ruin'
| 'buried-treasure'
| 'mineshaft'
| 'desert-well'
| 'geode'
| 'fortress'
| 'bastion'
| 'end-city'
| 'end-gateway'
export type SeedMapFeature = {
kind: SeedMapFeatureKind
x: number
z: number
approximate: boolean
endShip?: boolean
}
export type SeedMapFeatureDefinition = {
kind: SeedMapFeatureKind
mask: number
dimensions: readonly SeedMapDimension[]
maxScale: number
}
const OVERWORLD: readonly SeedMapDimension[] = ['overworld']
const NETHER: readonly SeedMapDimension[] = ['nether']
const END: readonly SeedMapDimension[] = ['end']
const ALL_DIMENSIONS: readonly SeedMapDimension[] = ['overworld', 'nether', 'end']
const OVERWORLD_AND_NETHER: readonly SeedMapDimension[] = ['overworld', 'nether']
/**
* Map layers in display order. `maxScale` bounds how far the map can zoom out
* before a dense layer stops being queried and drawn.
*/
export const SEED_MAP_FEATURES: readonly SeedMapFeatureDefinition[] = [
{ kind: 'slime-chunk', mask: 1 << 10, dimensions: ALL_DIMENSIONS, maxScale: 4 },
{ kind: 'village', mask: 1 << 0, dimensions: OVERWORLD, maxScale: 16 },
{ kind: 'outpost', mask: 1 << 1, dimensions: OVERWORLD, maxScale: 64 },
{ kind: 'shipwreck', mask: 1 << 2, dimensions: OVERWORLD, maxScale: 16 },
{ kind: 'monument', mask: 1 << 3, dimensions: OVERWORLD, maxScale: 16 },
{ kind: 'mansion', mask: 1 << 4, dimensions: OVERWORLD, maxScale: 64 },
{ kind: 'ancient-city', mask: 1 << 5, dimensions: OVERWORLD, maxScale: 16 },
{ kind: 'trail-ruins', mask: 1 << 6, dimensions: OVERWORLD, maxScale: 16 },
{ kind: 'trial-chambers', mask: 1 << 7, dimensions: OVERWORLD, maxScale: 4 },
{ kind: 'ruined-portal', mask: 1 << 8, dimensions: OVERWORLD_AND_NETHER, maxScale: 4 },
{ kind: 'stronghold', mask: 1 << 9, dimensions: OVERWORLD, maxScale: 64 },
{ kind: 'desert-pyramid', mask: 1 << 11, dimensions: OVERWORLD, maxScale: 16 },
{ kind: 'jungle-temple', mask: 1 << 12, dimensions: OVERWORLD, maxScale: 16 },
{ kind: 'swamp-hut', mask: 1 << 13, dimensions: OVERWORLD, maxScale: 16 },
{ kind: 'igloo', mask: 1 << 14, dimensions: OVERWORLD, maxScale: 16 },
{ kind: 'ocean-ruin', mask: 1 << 15, dimensions: OVERWORLD, maxScale: 4 },
{ kind: 'buried-treasure', mask: 1 << 16, dimensions: OVERWORLD, maxScale: 16 },
{ kind: 'mineshaft', mask: 1 << 17, dimensions: OVERWORLD, maxScale: 4 },
{ kind: 'desert-well', mask: 1 << 18, dimensions: OVERWORLD, maxScale: 16 },
{ kind: 'geode', mask: 1 << 19, dimensions: OVERWORLD, maxScale: 4 },
{ kind: 'fortress', mask: 1 << 20, dimensions: NETHER, maxScale: 64 },
{ kind: 'bastion', mask: 1 << 21, dimensions: NETHER, maxScale: 16 },
{ kind: 'end-city', mask: 1 << 22, dimensions: END, maxScale: 16 },
{ kind: 'end-gateway', mask: 1 << 23, dimensions: END, maxScale: 4 },
]
export function visibleFeatureDefinitions(
kinds: SeedMapFeatureKind[],
dimension: SeedMapDimension,
scale: number,
): SeedMapFeatureDefinition[] {
return SEED_MAP_FEATURES.filter(
(feature) =>
kinds.includes(feature.kind) &&
feature.dimensions.includes(dimension) &&
scale <= feature.maxScale,
)
}
export function featureMask(kinds: SeedMapFeatureKind[]): number {
return SEED_MAP_FEATURES.reduce(
(mask, feature) => (kinds.includes(feature.kind) ? mask | feature.mask : mask),
0,
)
}
export function featureKey(feature: Pick<SeedMapFeature, 'kind' | 'x' | 'z'>): string {
return `${feature.kind}:${feature.x}:${feature.z}`
}

View File

@ -0,0 +1,188 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import {
clearSeedMapHistory,
loadSeedMapHistory,
recordSeedMapHistory,
removeSeedMapHistoryEntry,
sanitizeSeedMapHistory,
SEED_MAP_HISTORY_LIMIT,
seedMapHistoryId,
updateSeedMapHistoryProgress,
} from './history.ts'
function memoryStorage(): Pick<Storage, 'getItem' | 'setItem'> {
const values = new Map<string, string>()
return {
getItem: (key) => values.get(key) ?? null,
setItem: (key, value) => void values.set(key, value),
}
}
test('recording keeps newest entries first and dedupes revisits', () => {
const storage = memoryStorage()
recordSeedMapHistory(
{ seed: '111', edition: 'java', gameVersion: '26.2', source: 'manual' },
1_000,
storage,
)
recordSeedMapHistory(
{ seed: '222', edition: 'java', gameVersion: '26.2', source: 'random' },
2_000,
storage,
)
const entries = recordSeedMapHistory(
{ seed: '111', edition: 'java', gameVersion: '1.21.3', source: 'manual' },
3_000,
storage,
)
assert.equal(entries.length, 2)
assert.equal(entries[0].seed, '111')
assert.equal(entries[0].gameVersion, '1.21.3')
assert.equal(entries[0].firstViewedAt, 1_000)
assert.equal(entries[0].lastViewedAt, 3_000)
assert.equal(entries[1].seed, '222')
})
test('instance attribution survives a later manual revisit', () => {
const storage = memoryStorage()
recordSeedMapHistory(
{
seed: '10292992',
edition: 'java',
gameVersion: '26.2',
source: 'instance',
instanceName: 'Fabric 26.2',
worldName: '生存世界',
},
1_000,
storage,
)
const entries = recordSeedMapHistory(
{ seed: '10292992', edition: 'java', gameVersion: '26.2', source: 'manual' },
2_000,
storage,
)
assert.equal(entries[0].source, 'instance')
assert.equal(entries[0].instanceName, 'Fabric 26.2')
assert.equal(entries[0].worldName, '生存世界')
assert.equal(entries[0].lastViewedAt, 2_000)
})
test('the same seed in different editions stays as separate entries', () => {
const storage = memoryStorage()
recordSeedMapHistory(
{ seed: '7', edition: 'java', gameVersion: '26.2', source: 'manual' },
1_000,
storage,
)
const entries = recordSeedMapHistory(
{ seed: '7', edition: 'java-large-biomes', gameVersion: '26.2', source: 'manual' },
2_000,
storage,
)
assert.equal(entries.length, 2)
assert.notEqual(entries[0].id, entries[1].id)
assert.equal(seedMapHistoryId('7', 'java'), 'java:7')
})
test('history is capped and blank seeds are ignored', () => {
const storage = memoryStorage()
for (let index = 0; index < SEED_MAP_HISTORY_LIMIT + 10; index++) {
recordSeedMapHistory(
{ seed: `seed-${index}`, edition: 'java', gameVersion: '26.2', source: 'manual' },
index,
storage,
)
}
recordSeedMapHistory(
{ seed: ' ', edition: 'java', gameVersion: '26.2', source: 'manual' },
99_999,
storage,
)
const entries = loadSeedMapHistory(storage)
assert.equal(entries.length, SEED_MAP_HISTORY_LIMIT)
assert.equal(entries[0].seed, `seed-${SEED_MAP_HISTORY_LIMIT + 9}`)
})
test('remove and clear update the persisted list', () => {
const storage = memoryStorage()
recordSeedMapHistory(
{ seed: 'a', edition: 'java', gameVersion: '26.2', source: 'manual' },
1_000,
storage,
)
recordSeedMapHistory(
{ seed: 'b', edition: 'java', gameVersion: '26.2', source: 'manual' },
2_000,
storage,
)
const afterRemove = removeSeedMapHistoryEntry(seedMapHistoryId('a', 'java'), storage)
assert.deepEqual(
afterRemove.map((entry) => entry.seed),
['b'],
)
assert.deepEqual(clearSeedMapHistory(storage), [])
assert.deepEqual(loadSeedMapHistory(storage), [])
})
test('exploration progress is stored per seed and survives revisits', () => {
const storage = memoryStorage()
recordSeedMapHistory(
{ seed: 'base', edition: 'java', gameVersion: '26.2', source: 'manual' },
1_000,
storage,
)
const id = seedMapHistoryId('base', 'java')
updateSeedMapHistoryProgress(id, ['village:0:0'], ['diamond:1:2:3'], storage)
let entries = loadSeedMapHistory(storage)
assert.deepEqual(entries[0].completedFeatures, ['village:0:0'])
assert.deepEqual(entries[0].completedOres, ['diamond:1:2:3'])
entries = recordSeedMapHistory(
{ seed: 'base', edition: 'java', gameVersion: '26.2', source: 'manual' },
2_000,
storage,
)
assert.deepEqual(entries[0].completedFeatures, ['village:0:0'])
entries = recordSeedMapHistory(
{
seed: 'base',
edition: 'java',
gameVersion: '26.2',
source: 'manual',
completedFeatures: ['village:0:0', 'monument:5:5'],
completedOres: [],
},
3_000,
storage,
)
assert.deepEqual(entries[0].completedFeatures, ['village:0:0', 'monument:5:5'])
assert.deepEqual(entries[0].completedOres, [])
assert.deepEqual(
updateSeedMapHistoryProgress(seedMapHistoryId('missing', 'java'), ['x'], [], storage).map(
(entry) => entry.seed,
),
['base'],
)
})
test('sanitizing drops malformed entries and duplicate ids', () => {
const entries = sanitizeSeedMapHistory([
{ seed: 'ok', edition: 'java', gameVersion: '26.2', source: 'share', lastViewedAt: 10 },
{ seed: 'ok', edition: 'java', gameVersion: '26.2', source: 'manual', lastViewedAt: 20 },
{ seed: '', edition: 'java' },
{ seed: 42 },
null,
'nonsense',
{ seed: 'weird-source', edition: 'java', gameVersion: '26.2', source: 'nope' },
])
assert.deepEqual(
entries.map((entry) => entry.seed),
['ok', 'weird-source'],
)
assert.equal(entries[0].source, 'share')
assert.equal(entries[1].source, 'manual')
})

View File

@ -0,0 +1,181 @@
import type { SeedMapEdition } from './backend.ts'
export type SeedMapHistorySource = 'manual' | 'random' | 'instance' | 'share'
export type SeedMapHistoryEntry = {
id: string
seed: string
edition: SeedMapEdition
gameVersion: string
source: SeedMapHistorySource
instanceName?: string
worldName?: string
firstViewedAt: number
lastViewedAt: number
completedFeatures: string[]
completedOres: string[]
}
export type SeedMapHistoryDraft = {
seed: string
edition: SeedMapEdition
gameVersion: string
source: SeedMapHistorySource
instanceName?: string
worldName?: string
completedFeatures?: string[]
completedOres?: string[]
}
type StorageLike = Pick<Storage, 'getItem' | 'setItem'>
export const SEED_MAP_HISTORY_KEY = 'axolotl.lab.seed-map.history.v1'
export const SEED_MAP_HISTORY_LIMIT = 60
const SOURCES: readonly SeedMapHistorySource[] = ['manual', 'random', 'instance', 'share']
export function seedMapHistoryId(seed: string, edition: SeedMapEdition): string {
return `${edition}:${seed}`
}
export function loadSeedMapHistory(
storage: StorageLike | null = getBrowserStorage(),
): SeedMapHistoryEntry[] {
if (!storage) return []
try {
const raw = storage.getItem(SEED_MAP_HISTORY_KEY)
if (!raw) return []
return sanitizeSeedMapHistory(JSON.parse(raw))
} catch {
return []
}
}
/**
* Records a viewed seed. Revisiting a known seed refreshes its timestamp and
* game version in place; richer attribution (an instance/world label) always
* wins over a plain manual revisit so the list keeps its most useful context.
*/
export function recordSeedMapHistory(
draft: SeedMapHistoryDraft,
now: number = Date.now(),
storage: StorageLike | null = getBrowserStorage(),
): SeedMapHistoryEntry[] {
const seed = draft.seed.trim().slice(0, 256)
if (!seed) return loadSeedMapHistory(storage)
const entries = loadSeedMapHistory(storage)
const id = seedMapHistoryId(seed, draft.edition)
const existing = entries.find((entry) => entry.id === id)
const keepExistingAttribution =
existing !== undefined && existing.instanceName !== undefined && draft.source !== 'instance'
const next: SeedMapHistoryEntry = {
id,
seed,
edition: draft.edition,
gameVersion: draft.gameVersion,
source: keepExistingAttribution ? existing.source : draft.source,
instanceName: keepExistingAttribution ? existing.instanceName : draft.instanceName,
worldName: keepExistingAttribution ? existing.worldName : draft.worldName,
firstViewedAt: existing?.firstViewedAt ?? now,
lastViewedAt: now,
completedFeatures: draft.completedFeatures ?? existing?.completedFeatures ?? [],
completedOres: draft.completedOres ?? existing?.completedOres ?? [],
}
const merged = [next, ...entries.filter((entry) => entry.id !== id)].slice(
0,
SEED_MAP_HISTORY_LIMIT,
)
persist(merged, storage)
return merged
}
/**
* Persists exploration progress for a known seed without touching its
* position in the list or its timestamps. Unknown ids are ignored.
*/
export function updateSeedMapHistoryProgress(
id: string,
completedFeatures: string[],
completedOres: string[],
storage: StorageLike | null = getBrowserStorage(),
): SeedMapHistoryEntry[] {
const entries = loadSeedMapHistory(storage)
const entry = entries.find((candidate) => candidate.id === id)
if (!entry) return entries
entry.completedFeatures = completedFeatures.slice(0, 2_000)
entry.completedOres = completedOres.slice(0, 10_000)
persist(entries, storage)
return entries
}
export function removeSeedMapHistoryEntry(
id: string,
storage: StorageLike | null = getBrowserStorage(),
): SeedMapHistoryEntry[] {
const entries = loadSeedMapHistory(storage).filter((entry) => entry.id !== id)
persist(entries, storage)
return entries
}
export function clearSeedMapHistory(
storage: StorageLike | null = getBrowserStorage(),
): SeedMapHistoryEntry[] {
persist([], storage)
return []
}
export function sanitizeSeedMapHistory(value: unknown): SeedMapHistoryEntry[] {
if (!Array.isArray(value)) return []
const seen = new Set<string>()
const entries: SeedMapHistoryEntry[] = []
for (const item of value) {
if (!item || typeof item !== 'object') continue
const source = item as Partial<SeedMapHistoryEntry>
if (typeof source.seed !== 'string' || !source.seed.trim()) continue
const edition: SeedMapEdition =
source.edition === 'java-large-biomes' ? 'java-large-biomes' : 'java'
const seed = source.seed.trim().slice(0, 256)
const id = seedMapHistoryId(seed, edition)
if (seen.has(id)) continue
seen.add(id)
const lastViewedAt = finiteTime(source.lastViewedAt)
entries.push({
id,
seed,
edition,
gameVersion: typeof source.gameVersion === 'string' ? source.gameVersion.slice(0, 32) : '',
source: SOURCES.includes(source.source as SeedMapHistorySource)
? (source.source as SeedMapHistorySource)
: 'manual',
instanceName:
typeof source.instanceName === 'string' ? source.instanceName.slice(0, 128) : undefined,
worldName: typeof source.worldName === 'string' ? source.worldName.slice(0, 128) : undefined,
firstViewedAt: finiteTime(source.firstViewedAt) || lastViewedAt,
lastViewedAt,
completedFeatures: sanitizeKeyList(source.completedFeatures, 2_000),
completedOres: sanitizeKeyList(source.completedOres, 10_000),
})
}
return entries.sort((a, b) => b.lastViewedAt - a.lastViewedAt).slice(0, SEED_MAP_HISTORY_LIMIT)
}
function persist(entries: SeedMapHistoryEntry[], storage: StorageLike | null): void {
try {
storage?.setItem(SEED_MAP_HISTORY_KEY, JSON.stringify(entries))
} catch {
// Ignore quota errors: history is a convenience and must never break the map.
}
}
function sanitizeKeyList(value: unknown, limit: number): string[] {
if (!Array.isArray(value)) return []
return value.filter((key): key is string => typeof key === 'string').slice(0, limit)
}
function finiteTime(value: unknown): number {
return typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : 0
}
function getBrowserStorage(): Storage | null {
return typeof window === 'undefined' ? null : window.localStorage
}

View File

@ -0,0 +1,8 @@
export * from './backend.ts'
export * from './biomes.ts'
export * from './features.ts'
export * from './history.ts'
export * from './ore-layer.ts'
export * from './ores.ts'
export * from './visuals.ts'
export * from './workspace.ts'

View File

@ -0,0 +1,286 @@
import { ref } from 'vue'
import { scanSeedMapOres } from './backend.ts'
import type { SeedMapDimension } from './biomes.ts'
import {
preferSeedMapOreHit,
SEED_MAP_ORE_CACHE_LIMIT,
SEED_MAP_ORE_CACHE_TARGET,
SEED_MAP_ORE_MAX_SCALE,
type SeedMapOreChunk,
seedMapOreColumnKey,
type SeedMapOreHit,
type SeedMapOreKind,
seedMapOreScanBudget,
} from './ores.ts'
export type SeedMapOreViewport = {
minX: number
minZ: number
maxX: number
maxZ: number
}
export type SeedMapOreLayerContext = {
enabled: boolean
seed: string
version: string
dimension: SeedMapDimension
scale: number
selectedOres: SeedMapOreKind[]
yMin: number | null
yMax: number | null
bounds: SeedMapOreViewport
center: { x: number; z: number }
}
type OreChunkCache = Map<SeedMapOreKind, SeedMapOreHit[]>
const SCAN_BATCH_CHUNKS = 96
export function useSeedMapOreLayer(options: {
onUpdate: () => void
onError: (message: string) => void
}) {
const hits = ref<SeedMapOreHit[]>([])
const scanning = ref(false)
const progress = ref(0)
const scannedChunks = ref(0)
const totalChunks = ref(0)
const chunkCache = new Map<string, OreChunkCache>()
let context: SeedMapOreLayerContext | null = null
let contextKey = ''
let refreshTimer: ReturnType<typeof setTimeout> | undefined
let rebuildTimer: ReturnType<typeof setTimeout> | undefined
let refreshGeneration = 0
let lastRebuild = 0
function refresh(nextContext: SeedMapOreLayerContext, delay = 180): void {
context = cloneContext(nextContext)
refreshGeneration++
if (refreshTimer) clearTimeout(refreshTimer)
const nextKey = `${nextContext.seed}|${nextContext.version}|${nextContext.dimension}`
if (nextKey !== contextKey) {
contextKey = nextKey
chunkCache.clear()
}
rebuildVisibleHits()
if (!canScan(nextContext)) {
scanning.value = false
progress.value = 0
scannedChunks.value = 0
totalChunks.value = 0
return
}
const generation = refreshGeneration
refreshTimer = setTimeout(() => void scanVisibleChunks(generation), delay)
}
function refreshFilter(yMin: number | null, yMax: number | null): void {
if (!context) return
context.yMin = yMin
context.yMax = yMax
rebuildVisibleHits()
}
function clear(): void {
refreshGeneration++
if (refreshTimer) clearTimeout(refreshTimer)
if (rebuildTimer) clearTimeout(rebuildTimer)
chunkCache.clear()
hits.value = []
scanning.value = false
progress.value = 0
scannedChunks.value = 0
totalChunks.value = 0
options.onUpdate()
}
function dispose(): void {
clear()
}
async function scanVisibleChunks(generation: number): Promise<void> {
refreshTimer = undefined
if (generation !== refreshGeneration || !context || !canScan(context)) return
scanning.value = true
progress.value = 0
try {
const missing = missingChunks(context)
totalChunks.value = missing.length
scannedChunks.value = 0
for (
let offset = 0;
offset < missing.length && generation === refreshGeneration;
offset += SCAN_BATCH_CHUNKS
) {
const batch = missing.slice(offset, offset + SCAN_BATCH_CHUNKS)
const results = await scanSeedMapOres({
seed: context.seed,
version: context.version,
dimension: context.dimension,
ores: [...context.selectedOres],
chunks: batch.flatMap(({ cx, cz }) => [cx, cz]),
})
if (generation !== refreshGeneration) return
for (const cell of results) {
const cache = chunkCache.get(chunkKey(cell.cx, cell.cz)) ?? new Map()
const grouped = new Map<SeedMapOreKind, SeedMapOreHit[]>()
for (const hit of cell.hits) {
const list = grouped.get(hit.ore) ?? []
list.push(hit)
grouped.set(hit.ore, list)
}
for (const ore of context.selectedOres) {
cache.set(ore, grouped.get(ore) ?? [])
}
chunkCache.set(chunkKey(cell.cx, cell.cz), cache)
}
scannedChunks.value = Math.min(missing.length, offset + batch.length)
progress.value = Math.min(1, scannedChunks.value / Math.max(1, missing.length))
pruneCache(context)
scheduleRebuild()
}
} catch (error) {
if (generation === refreshGeneration) {
options.onError(error instanceof Error ? error.message : String(error))
}
} finally {
if (generation === refreshGeneration) {
scanning.value = false
progress.value = 1
rebuildVisibleHits()
}
}
}
function missingChunks(activeContext: SeedMapOreLayerContext): SeedMapOreChunk[] {
const chunks = chunksInViewport(activeContext.bounds, activeContext.center)
const budget = seedMapOreScanBudget(activeContext.selectedOres)
const missing: SeedMapOreChunk[] = []
for (const chunk of chunks) {
const cache = chunkCache.get(chunkKey(chunk.cx, chunk.cz))
if (activeContext.selectedOres.every((ore) => cache?.has(ore))) continue
missing.push(chunk)
if (missing.length >= budget) break
}
return missing
}
function rebuildVisibleHits(): void {
if (!context || !canDisplay(context)) {
hits.value = []
options.onUpdate()
return
}
const columns = new Map<string, SeedMapOreHit>()
const selected = new Set(context.selectedOres)
for (const chunk of chunksInViewport(context.bounds, context.center, false)) {
const cache = chunkCache.get(chunkKey(chunk.cx, chunk.cz))
if (!cache) continue
for (const [ore, oreHits] of cache) {
if (!selected.has(ore)) continue
for (const hit of oreHits) {
if (context.yMin !== null && hit.y < context.yMin) continue
if (context.yMax !== null && hit.y > context.yMax) continue
const key = seedMapOreColumnKey(hit)
columns.set(key, preferSeedMapOreHit(columns.get(key), hit))
}
}
}
hits.value = [...columns.values()].sort(
(a, b) =>
(a.x - context!.center.x) ** 2 +
(a.z - context!.center.z) ** 2 -
((b.x - context!.center.x) ** 2 + (b.z - context!.center.z) ** 2),
)
lastRebuild = performance.now()
options.onUpdate()
}
function scheduleRebuild(): void {
if (rebuildTimer) return
const delay = Math.max(0, 140 - (performance.now() - lastRebuild))
rebuildTimer = setTimeout(() => {
rebuildTimer = undefined
rebuildVisibleHits()
}, delay)
}
function pruneCache(activeContext: SeedMapOreLayerContext): void {
if (chunkCache.size <= SEED_MAP_ORE_CACHE_LIMIT) return
const centerX = Math.floor(activeContext.center.x / 16)
const centerZ = Math.floor(activeContext.center.z / 16)
const entries = [...chunkCache.keys()].map((key) => {
const [cx, cz] = key.split(',').map(Number)
return { key, distance: (cx - centerX) ** 2 + (cz - centerZ) ** 2 }
})
entries.sort((a, b) => b.distance - a.distance)
const removeCount = chunkCache.size - SEED_MAP_ORE_CACHE_TARGET
for (let index = 0; index < removeCount; index++) {
chunkCache.delete(entries[index].key)
}
}
return {
hits,
scanning,
progress,
scannedChunks,
totalChunks,
refresh,
refreshFilter,
clear,
dispose,
}
}
function canDisplay(context: SeedMapOreLayerContext): boolean {
return (
context.enabled &&
context.dimension !== 'end' &&
context.scale <= SEED_MAP_ORE_MAX_SCALE &&
context.selectedOres.length > 0
)
}
function canScan(context: SeedMapOreLayerContext): boolean {
return canDisplay(context) && context.seed.trim().length > 0
}
function chunksInViewport(
bounds: SeedMapOreViewport,
center: { x: number; z: number },
sort = true,
): SeedMapOreChunk[] {
const chunks: SeedMapOreChunk[] = []
for (let cx = Math.floor(bounds.minX / 16); cx <= Math.floor(bounds.maxX / 16); cx++) {
for (let cz = Math.floor(bounds.minZ / 16); cz <= Math.floor(bounds.maxZ / 16); cz++) {
chunks.push({ cx, cz })
}
}
if (sort) {
const centerX = center.x / 16
const centerZ = center.z / 16
chunks.sort(
(a, b) =>
(a.cx - centerX) ** 2 +
(a.cz - centerZ) ** 2 -
((b.cx - centerX) ** 2 + (b.cz - centerZ) ** 2),
)
}
return chunks
}
function chunkKey(cx: number, cz: number): string {
return `${cx},${cz}`
}
function cloneContext(context: SeedMapOreLayerContext): SeedMapOreLayerContext {
return {
...context,
selectedOres: [...context.selectedOres],
bounds: { ...context.bounds },
center: { ...context.center },
}
}

View File

@ -0,0 +1,203 @@
import type { SeedMapDimension } from './biomes.ts'
export type SeedMapDisplayMode = 'structures' | 'ores'
export type SeedMapOreKind =
| 'diamond'
| 'iron'
| 'iron_vein'
| 'copper'
| 'copper_vein'
| 'gold'
| 'redstone'
| 'lapis'
| 'coal'
| 'netherite'
export type SeedMapOreDefinition = {
kind: SeedMapOreKind
dimension: Exclude<SeedMapDimension, 'end'>
yMin: number
yMax: number
bandRows: number
image: string
texture: string
deepslateTexture?: string
}
export type SeedMapOreHit = {
ore: SeedMapOreKind
x: number
y: number
z: number
verified: boolean
yMin: number
yMax: number
precision: number
}
export type SeedMapOreChunk = {
cx: number
cz: number
}
const ORE_ASSET_ROOT = '/seed-map-assets/ores'
export const SEED_MAP_ORE_MAX_SCALE = 0.25
export const SEED_MAP_ORE_CACHE_LIMIT = 6_000
export const SEED_MAP_ORE_CACHE_TARGET = 5_400
/**
* Ore prediction replays the vanilla 1.18+ population RNG natively, so the
* offered kinds match the scattered-ore features of those versions. The
* `bandRows` value reflects each distribution's Y span and scales the scan
* budget so wide bands do not overwhelm a single pass.
*/
export const SEED_MAP_ORES: readonly SeedMapOreDefinition[] = [
{
kind: 'diamond',
dimension: 'overworld',
yMin: -144,
yMax: 16,
bandRows: 160,
image: `${ORE_ASSET_ROOT}/diamond-ore.png`,
texture: `${ORE_ASSET_ROOT}/ore-flat/diamond_ore.png`,
deepslateTexture: `${ORE_ASSET_ROOT}/ore-flat/deepslate_diamond_ore.png`,
},
{
kind: 'iron',
dimension: 'overworld',
yMin: -64,
yMax: 319,
bandRows: 384,
image: `${ORE_ASSET_ROOT}/iron-ore.png`,
texture: `${ORE_ASSET_ROOT}/ore-flat/iron_ore.png`,
deepslateTexture: `${ORE_ASSET_ROOT}/ore-flat/deepslate_iron_ore.png`,
},
{
kind: 'iron_vein',
dimension: 'overworld',
yMin: -60,
yMax: -8,
bandRows: 128,
image: `${ORE_ASSET_ROOT}/block-of-raw-iron.png`,
texture: `${ORE_ASSET_ROOT}/ore-flat/iron_ore.png`,
deepslateTexture: `${ORE_ASSET_ROOT}/ore-flat/deepslate_iron_ore.png`,
},
{
kind: 'copper',
dimension: 'overworld',
yMin: -16,
yMax: 112,
bandRows: 144,
image: `${ORE_ASSET_ROOT}/copper-ore.png`,
texture: `${ORE_ASSET_ROOT}/ore-flat/copper_ore.png`,
deepslateTexture: `${ORE_ASSET_ROOT}/ore-flat/deepslate_copper_ore.png`,
},
{
kind: 'copper_vein',
dimension: 'overworld',
yMin: 0,
yMax: 50,
bandRows: 128,
image: `${ORE_ASSET_ROOT}/block-of-raw-copper.png`,
texture: `${ORE_ASSET_ROOT}/ore-flat/copper_ore.png`,
deepslateTexture: `${ORE_ASSET_ROOT}/ore-flat/deepslate_copper_ore.png`,
},
{
kind: 'gold',
dimension: 'overworld',
yMin: -64,
yMax: 32,
bandRows: 104,
image: `${ORE_ASSET_ROOT}/gold-ore.png`,
texture: `${ORE_ASSET_ROOT}/ore-flat/gold_ore.png`,
deepslateTexture: `${ORE_ASSET_ROOT}/ore-flat/deepslate_gold_ore.png`,
},
{
kind: 'redstone',
dimension: 'overworld',
yMin: -96,
yMax: 15,
bandRows: 112,
image: `${ORE_ASSET_ROOT}/redstone-ore.png`,
texture: `${ORE_ASSET_ROOT}/ore-flat/redstone_ore.png`,
deepslateTexture: `${ORE_ASSET_ROOT}/ore-flat/deepslate_redstone_ore.png`,
},
{
kind: 'lapis',
dimension: 'overworld',
yMin: -64,
yMax: 64,
bandRows: 136,
image: `${ORE_ASSET_ROOT}/lapis-lazuli-ore.png`,
texture: `${ORE_ASSET_ROOT}/ore-flat/lapis_ore.png`,
deepslateTexture: `${ORE_ASSET_ROOT}/ore-flat/deepslate_lapis_ore.png`,
},
{
kind: 'coal',
dimension: 'overworld',
yMin: 0,
yMax: 319,
bandRows: 328,
image: `${ORE_ASSET_ROOT}/coal-ore.png`,
texture: `${ORE_ASSET_ROOT}/ore-flat/coal_ore.png`,
deepslateTexture: `${ORE_ASSET_ROOT}/ore-flat/deepslate_coal_ore.png`,
},
{
kind: 'netherite',
dimension: 'nether',
yMin: 8,
yMax: 119,
bandRows: 128,
image: `${ORE_ASSET_ROOT}/ancient-debris.png`,
texture: `${ORE_ASSET_ROOT}/ore-flat/ancient_debris_side.png`,
},
]
const ORE_BY_KIND = new Map(SEED_MAP_ORES.map((ore) => [ore.kind, ore]))
export function seedMapOreDefinition(kind: SeedMapOreKind): SeedMapOreDefinition {
const definition = ORE_BY_KIND.get(kind)
if (!definition) throw new Error(`Unknown seed-map ore: ${kind}`)
return definition
}
export function seedMapOresForDimension(dimension: SeedMapDimension): SeedMapOreDefinition[] {
if (dimension === 'end') return []
return SEED_MAP_ORES.filter((ore) => ore.dimension === dimension)
}
export function seedMapOreScanBudget(kinds: readonly SeedMapOreKind[]): number {
const maxBandRows = Math.max(1, ...kinds.map((kind) => seedMapOreDefinition(kind).bandRows))
return Math.max(60, Math.round(33_600 / maxBandRows))
}
export function seedMapOreYRange(kinds: readonly SeedMapOreKind[]): [number, number] {
if (kinds.length === 0) return [-64, 319]
return [
Math.min(...kinds.map((kind) => seedMapOreDefinition(kind).yMin)),
Math.max(...kinds.map((kind) => seedMapOreDefinition(kind).yMax)),
]
}
export function seedMapOreKey(hit: SeedMapOreHit): string {
return `${hit.ore}:${hit.x}:${hit.y}:${hit.z}`
}
export function seedMapOreColumnKey(hit: Pick<SeedMapOreHit, 'ore' | 'x' | 'z'>): string {
return `${hit.ore}:${hit.x}:${hit.z}`
}
export function preferSeedMapOreHit(
current: SeedMapOreHit | undefined,
candidate: SeedMapOreHit,
): SeedMapOreHit {
if (!current) return candidate
if (candidate.verified !== current.verified) return candidate.verified ? candidate : current
if (candidate.precision !== current.precision)
return candidate.precision > current.precision ? candidate : current
const currentRange = current.yMax - current.yMin
const candidateRange = candidate.yMax - candidate.yMin
return candidateRange < currentRange ? candidate : current
}

View File

@ -0,0 +1,103 @@
import {
CompassIcon,
DatabaseIcon,
GridIcon,
HomeIcon,
LandmarkIcon,
LayersIcon,
PickaxeIcon,
PinIcon,
TagCategoryFlagIcon,
WorldIcon,
} from '@modrinth/assets'
import type { Component } from 'vue'
import type { SeedMapFeatureKind } from './features.ts'
export const SEED_MAP_STRUCTURE_ASSET_ROOT = '/seed-map-assets/structures'
export const SEED_MAP_END_CITY_IMAGE_SOURCES = {
ship: `${SEED_MAP_STRUCTURE_ASSET_ROOT}/end_city_ship.webp`,
noShip: `${SEED_MAP_STRUCTURE_ASSET_ROOT}/end_city_no_ship.webp`,
} as const
export const SEED_MAP_FEATURE_ICONS: Record<SeedMapFeatureKind, Component> = {
village: HomeIcon,
outpost: TagCategoryFlagIcon,
shipwreck: CompassIcon,
monument: LandmarkIcon,
mansion: HomeIcon,
'ancient-city': DatabaseIcon,
'trail-ruins': LayersIcon,
'trial-chambers': LandmarkIcon,
'ruined-portal': WorldIcon,
stronghold: CompassIcon,
'slime-chunk': GridIcon,
'desert-pyramid': LandmarkIcon,
'jungle-temple': LandmarkIcon,
'swamp-hut': HomeIcon,
igloo: HomeIcon,
'ocean-ruin': LayersIcon,
'buried-treasure': PinIcon,
mineshaft: PickaxeIcon,
'desert-well': CompassIcon,
geode: DatabaseIcon,
fortress: LandmarkIcon,
bastion: HomeIcon,
'end-city': HomeIcon,
'end-gateway': WorldIcon,
}
export const SEED_MAP_FEATURE_IMAGE_SOURCES: Partial<Record<SeedMapFeatureKind, string>> = {
village: `${SEED_MAP_STRUCTURE_ASSET_ROOT}/village.webp`,
outpost: `${SEED_MAP_STRUCTURE_ASSET_ROOT}/pillager_outpost.webp`,
shipwreck: `${SEED_MAP_STRUCTURE_ASSET_ROOT}/shipwreck.webp`,
monument: `${SEED_MAP_STRUCTURE_ASSET_ROOT}/ocean_monument.webp`,
mansion: `${SEED_MAP_STRUCTURE_ASSET_ROOT}/woodland_mansion.webp`,
'ancient-city': `${SEED_MAP_STRUCTURE_ASSET_ROOT}/ancient_city.webp`,
'trail-ruins': `${SEED_MAP_STRUCTURE_ASSET_ROOT}/trail_ruin.webp`,
'trial-chambers': `${SEED_MAP_STRUCTURE_ASSET_ROOT}/trial_chambers.webp`,
'ruined-portal': `${SEED_MAP_STRUCTURE_ASSET_ROOT}/ruined_portal.webp`,
stronghold: `${SEED_MAP_STRUCTURE_ASSET_ROOT}/stronghold.webp`,
'slime-chunk': `${SEED_MAP_STRUCTURE_ASSET_ROOT}/slime_chunks.webp`,
'desert-pyramid': `${SEED_MAP_STRUCTURE_ASSET_ROOT}/desert_pyramid.webp`,
'jungle-temple': `${SEED_MAP_STRUCTURE_ASSET_ROOT}/jungle_temple.webp`,
'swamp-hut': `${SEED_MAP_STRUCTURE_ASSET_ROOT}/swamp_hut.webp`,
igloo: `${SEED_MAP_STRUCTURE_ASSET_ROOT}/igloo.webp`,
'ocean-ruin': `${SEED_MAP_STRUCTURE_ASSET_ROOT}/ocean_ruin.webp`,
'buried-treasure': `${SEED_MAP_STRUCTURE_ASSET_ROOT}/buried_treasure.webp`,
mineshaft: `${SEED_MAP_STRUCTURE_ASSET_ROOT}/mineshaft.webp`,
'desert-well': `${SEED_MAP_STRUCTURE_ASSET_ROOT}/desert_well.webp`,
geode: `${SEED_MAP_STRUCTURE_ASSET_ROOT}/amethyst_geode.webp`,
fortress: `${SEED_MAP_STRUCTURE_ASSET_ROOT}/nether_fortress.webp`,
bastion: `${SEED_MAP_STRUCTURE_ASSET_ROOT}/bastion_remnant.webp`,
'end-city': `${SEED_MAP_STRUCTURE_ASSET_ROOT}/end_city.webp`,
'end-gateway': `${SEED_MAP_STRUCTURE_ASSET_ROOT}/end_gateway.webp`,
}
export const SEED_MAP_FEATURE_COLORS: Record<SeedMapFeatureKind, string> = {
village: '#D58B35',
outpost: '#C94B52',
shipwreck: '#3B9CC6',
monument: '#34A99A',
mansion: '#8C67C5',
'ancient-city': '#6677D9',
'trail-ruins': '#C96F3C',
'trial-chambers': '#4DAE72',
'ruined-portal': '#C95B92',
stronghold: '#8793A4',
'slime-chunk': '#83B735',
'desert-pyramid': '#D0A23E',
'jungle-temple': '#4B9A56',
'swamp-hut': '#6A8D58',
igloo: '#80B6CA',
'ocean-ruin': '#4F9EAB',
'buried-treasure': '#E0B84E',
mineshaft: '#9B7657',
'desert-well': '#D6AF62',
geode: '#9969C7',
fortress: '#A94A51',
bastion: '#7E6658',
'end-city': '#A57BC5',
'end-gateway': '#70559F',
}

View File

@ -0,0 +1,205 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import {
fallbackSeedMapProfiles,
SEED_MAP_DEFAULT_VERSION,
seedMapTileConcurrency,
} from './backend.ts'
import { SEED_MAP_BIOME_NAMES, SEED_MAP_BIOMES, seedMapBiomeGroups } from './biomes.ts'
import { featureMask, SEED_MAP_FEATURES, visibleFeatureDefinitions } from './features.ts'
import {
applyShareQuery,
createDefaultSeedMapWorkspace,
isCurrentSeedMapEpoch,
sanitizeSeedMapWorkspace,
SEED_MAP_MIN_ZOOM,
} from './workspace.ts'
test('seed map starts near spawn with a lightweight layer set', () => {
const workspace = createDefaultSeedMapWorkspace()
assert.equal(workspace.zoom, 1)
assert.equal(workspace.terrainEstimation, true)
assert.equal(workspace.gameVersion, SEED_MAP_DEFAULT_VERSION)
assert.equal(workspace.elevation, 128)
assert.deepEqual(workspace.visibleFeatures, [
'village',
'ruined-portal',
'stronghold',
'fortress',
'bastion',
'end-city',
])
assert.equal(workspace.displayMode, 'structures')
assert.deepEqual(workspace.selectedOres, ['diamond'])
})
test('seed map workspace recovers from invalid persisted values', () => {
const state = sanitizeSeedMapWorkspace({
seed: ' mushroom island ',
edition: 'java',
zoom: 99,
center: { x: Number.NaN, z: 72 },
visibleFeatures: ['village', 'invalid'],
})
assert.equal(state.seed, 'mushroom island')
assert.equal(state.zoom, 4)
assert.deepEqual(state.center, { x: 0, z: 72 })
assert.deepEqual(state.visibleFeatures, ['village'])
assert.equal(state.showSpawn, true)
assert.deepEqual(state.highlightedBiomes, [])
})
test('seed map uses fixed surface and underground elevation slices', () => {
assert.equal(sanitizeSeedMapWorkspace({ elevation: 62 }).elevation, 128)
assert.equal(sanitizeSeedMapWorkspace({ elevation: 128 }).elevation, 128)
assert.equal(sanitizeSeedMapWorkspace({ elevation: 16 }).elevation, 16)
assert.equal(sanitizeSeedMapWorkspace({ elevation: -32 }).elevation, -32)
})
test('seed map keeps every offered ore kind and drops unknown ones', () => {
const state = sanitizeSeedMapWorkspace({
...createDefaultSeedMapWorkspace(),
selectedOres: ['diamond', 'iron_vein', 'copper_vein', 'coal', 'bogus'],
})
assert.deepEqual(state.selectedOres, ['diamond', 'iron_vein', 'copper_vein', 'coal'])
})
test('seed map supports a closer maximum zoom', () => {
const close = sanitizeSeedMapWorkspace({ zoom: SEED_MAP_MIN_ZOOM })
const tooClose = sanitizeSeedMapWorkspace({ zoom: SEED_MAP_MIN_ZOOM - 10 })
assert.equal(close.zoom, SEED_MAP_MIN_ZOOM)
assert.equal(tooClose.zoom, SEED_MAP_MIN_ZOOM)
})
test('seed map exposes every categorized cubiomes biome exactly once', () => {
const groups = seedMapBiomeGroups()
assert.equal(SEED_MAP_BIOMES.length, 65)
assert.equal(groups.length, 17)
assert.equal(groups.flatMap((group) => group.biomes).length, 65)
assert.equal(new Set(SEED_MAP_BIOMES.map((biome) => biome.id)).size, 65)
assert.deepEqual(
groups.filter((group) => group.dimension === 'nether').map((group) => group.category),
['nether'],
)
assert.deepEqual(
groups.filter((group) => group.dimension === 'end').map((group) => group.category),
['end'],
)
assert.deepEqual(
SEED_MAP_BIOMES.filter((biome) => !SEED_MAP_BIOME_NAMES[biome.id]),
[],
)
})
test('seed map migrates legacy biome highlighting and terrain defaults', () => {
const state = sanitizeSeedMapWorkspace({
version: 1,
highlightBiomeEnabled: true,
highlightedBiome: 21,
terrainEstimation: false,
})
assert.deepEqual(state.highlightedBiomes, [21])
assert.equal(state.highlightBiomeEnabled, true)
assert.equal(state.terrainEstimation, true)
assert.equal(state.version, 3)
})
test('seed map share queries preserve useful map state', () => {
const workspace = createDefaultSeedMapWorkspace()
const shared = applyShareQuery(
{
seed: '1234',
edition: 'java-large-biomes',
version: '1.21.1',
dimension: 'nether',
mode: 'ores',
ores: 'netherite',
yMin: '12',
yMax: '24',
x: '-500',
z: '312',
zoom: '1.25',
},
workspace,
)
assert.equal(shared.seed, '1234')
assert.equal(shared.edition, 'java-large-biomes')
assert.equal(shared.gameVersion, '1.21.1')
assert.equal(shared.dimension, 'nether')
assert.equal(shared.displayMode, 'ores')
assert.deepEqual(shared.selectedOres, ['netherite'])
assert.equal(shared.oreYMin, 12)
assert.equal(shared.oreYMax, 24)
assert.equal(shared.zoom, 1.25)
assert.deepEqual(shared.center, { x: -500, z: 312 })
})
test('fallback profiles gate dimensions, ores, and large biomes by version', () => {
const profiles = fallbackSeedMapProfiles()
const modern = profiles.find(
(profile) => profile.edition === 'java' && profile.version === '1.21.3',
)
assert.deepEqual(modern?.dimensions, ['overworld', 'nether', 'end'])
assert.equal(modern?.ores, true)
const legacy = profiles.find((profile) => profile.edition === 'java' && profile.version === '1.9')
assert.deepEqual(legacy?.dimensions, ['overworld', 'end'])
assert.equal(legacy?.ores, false)
const oldest = profiles.find((profile) => profile.edition === 'java' && profile.version === '1.8')
assert.deepEqual(oldest?.dimensions, ['overworld'])
assert.ok(
!profiles.some(
(profile) => profile.edition === 'java-large-biomes' && profile.version === '1.0',
),
)
})
test('feature masks match the selected map layers', () => {
const mask = featureMask(['village', 'stronghold', 'slime-chunk'])
const expected = SEED_MAP_FEATURES.filter((feature) =>
['village', 'stronghold', 'slime-chunk'].includes(feature.kind),
).reduce((value, feature) => value | feature.mask, 0)
assert.equal(mask, expected)
})
test('seed map ignores a response from an older tile request epoch', () => {
assert.equal(isCurrentSeedMapEpoch(8, 8), true)
assert.equal(isCurrentSeedMapEpoch(7, 8), false)
})
test('seed map tile concurrency reserves capacity for the interface', () => {
assert.equal(seedMapTileConcurrency(4, 4), 2)
assert.equal(seedMapTileConcurrency(4, 8), 6)
assert.equal(seedMapTileConcurrency(4, 16), 8)
assert.equal(seedMapTileConcurrency(1, 16), 4)
})
test('dense feature layers are omitted until the map is close enough', () => {
const close = visibleFeatureDefinitions(['slime-chunk', 'village', 'stronghold'], 'overworld', 4)
const medium = visibleFeatureDefinitions(
['slime-chunk', 'village', 'stronghold'],
'overworld',
64,
)
const far = visibleFeatureDefinitions(['slime-chunk', 'village', 'stronghold'], 'overworld', 256)
assert.deepEqual(
close.map((feature) => feature.kind),
['slime-chunk', 'village', 'stronghold'],
)
assert.deepEqual(
medium.map((feature) => feature.kind),
['stronghold'],
)
assert.deepEqual(far, [])
})
test('slime chunks are the first layer in every dimension', () => {
for (const dimension of ['overworld', 'nether', 'end'] as const) {
const visible = visibleFeatureDefinitions(
['village', 'fortress', 'end-city', 'slime-chunk'],
dimension,
4,
)
assert.equal(visible[0]?.kind, 'slime-chunk')
}
})

View File

@ -0,0 +1,299 @@
import { SEED_MAP_DEFAULT_VERSION, type SeedMapEdition } from './backend.ts'
import { SEED_MAP_BIOMES, type SeedMapDimension } from './biomes.ts'
import { SEED_MAP_FEATURES, type SeedMapFeatureKind } from './features.ts'
import { SEED_MAP_ORES, type SeedMapDisplayMode, type SeedMapOreKind } from './ores.ts'
export type SeedMapMarker = {
id: string
name: string
x: number
z: number
color: string
}
export type SeedMapWorkspace = {
version: 3
seed: string
edition: SeedMapEdition
gameVersion: string
dimension: SeedMapDimension
displayMode: SeedMapDisplayMode
center: { x: number; z: number }
zoom: number
elevation: number
showGrid: boolean
showChunkCoordinates: boolean
showSpawn: boolean
terrainEstimation: boolean
contourLines: boolean
highlightBiomeEnabled: boolean
highlightedBiomes: number[]
visibleFeatures: SeedMapFeatureKind[]
selectedOres: SeedMapOreKind[]
oreYMin: number | null
oreYMax: number | null
markers: SeedMapMarker[]
completedFeatures: string[]
completedOres: string[]
}
type StorageLike = Pick<Storage, 'getItem' | 'setItem'>
export const SEED_MAP_STORAGE_KEY = 'axolotl.lab.seed-map.v1'
export const SEED_MAP_SCALES = [1, 4, 16, 64, 256] as const
export const SEED_MAP_MIN_ZOOM = -2
/** Fixed vertical slices exposed by the seed-map elevation selector. */
export const SEED_MAP_ELEVATIONS = [128, 16, -32] as const
export function createDefaultSeedMapWorkspace(): SeedMapWorkspace {
return {
version: 3,
seed: '10292992',
edition: 'java',
gameVersion: SEED_MAP_DEFAULT_VERSION,
dimension: 'overworld',
displayMode: 'structures',
center: { x: 0, z: 0 },
zoom: 1,
elevation: SEED_MAP_ELEVATIONS[0],
showGrid: false,
showChunkCoordinates: false,
showSpawn: true,
terrainEstimation: true,
contourLines: false,
highlightBiomeEnabled: false,
highlightedBiomes: [],
visibleFeatures: ['village', 'ruined-portal', 'stronghold', 'fortress', 'bastion', 'end-city'],
selectedOres: ['diamond'],
oreYMin: null,
oreYMax: null,
markers: [],
completedFeatures: [],
completedOres: [],
}
}
export function loadSeedMapWorkspace(
storage: StorageLike | null = getBrowserStorage(),
): SeedMapWorkspace {
const fallback = createDefaultSeedMapWorkspace()
if (!storage) return fallback
try {
const raw = storage.getItem(SEED_MAP_STORAGE_KEY)
return raw ? sanitizeSeedMapWorkspace(JSON.parse(raw), fallback) : fallback
} catch {
return fallback
}
}
export function saveSeedMapWorkspace(
workspace: SeedMapWorkspace,
storage: StorageLike | null = getBrowserStorage(),
): void {
if (storage)
storage.setItem(SEED_MAP_STORAGE_KEY, JSON.stringify(sanitizeSeedMapWorkspace(workspace)))
}
export function sanitizeSeedMapWorkspace(
value: unknown,
fallback = createDefaultSeedMapWorkspace(),
): SeedMapWorkspace {
if (!value || typeof value !== 'object') return fallback
const source = value as Partial<Omit<SeedMapWorkspace, 'version'>> & {
version?: unknown
highlightedBiome?: unknown
}
const center =
source.center && typeof source.center === 'object' ? source.center : fallback.center
const zoom = finiteNumber(source.zoom, fallback.zoom)
const availableBiomeIds = new Set(SEED_MAP_BIOMES.map((biome) => biome.id))
const highlightedBiomes = (
Array.isArray(source.highlightedBiomes)
? source.highlightedBiomes
: typeof source.highlightedBiome === 'number'
? [source.highlightedBiome]
: []
)
.filter(
(biome, index, values): biome is number =>
typeof biome === 'number' &&
Number.isInteger(biome) &&
availableBiomeIds.has(biome) &&
values.indexOf(biome) === index,
)
.slice(0, SEED_MAP_BIOMES.length)
const selectedOres = sanitizeOreKinds(source.selectedOres)
const oreYMin = nullableInteger(source.oreYMin, -144, 319)
const oreYMax = nullableInteger(source.oreYMax, -144, 319)
return {
version: 3,
seed:
typeof source.seed === 'string' && source.seed.trim()
? source.seed.trim().slice(0, 256)
: fallback.seed,
edition: source.edition === 'java-large-biomes' ? source.edition : 'java',
gameVersion: typeof source.gameVersion === 'string' ? source.gameVersion : fallback.gameVersion,
dimension:
source.dimension === 'nether' || source.dimension === 'end' ? source.dimension : 'overworld',
displayMode: source.displayMode === 'ores' ? 'ores' : 'structures',
center: {
x: finiteNumber(center.x, fallback.center.x),
z: finiteNumber(center.z, fallback.center.z),
},
zoom:
Math.round(Math.min(Math.max(zoom, SEED_MAP_MIN_ZOOM), SEED_MAP_SCALES.length - 1) * 1_000) /
1_000,
elevation: sanitizeElevation(source.elevation, fallback.elevation),
showGrid: source.showGrid === true,
showChunkCoordinates: source.showChunkCoordinates === true,
showSpawn: source.showSpawn !== false,
terrainEstimation: source.version === 1 ? true : source.terrainEstimation !== false,
contourLines: source.contourLines === true,
highlightBiomeEnabled: source.highlightBiomeEnabled === true && highlightedBiomes.length > 0,
highlightedBiomes,
visibleFeatures: sanitizeFeatureKinds(source.visibleFeatures),
selectedOres: selectedOres.length > 0 ? selectedOres : [...fallback.selectedOres],
oreYMin: oreYMin !== null && oreYMax !== null && oreYMin > oreYMax ? oreYMax : oreYMin,
oreYMax: oreYMin !== null && oreYMax !== null && oreYMax < oreYMin ? oreYMin : oreYMax,
markers: sanitizeMarkers(source.markers),
completedFeatures: Array.isArray(source.completedFeatures)
? source.completedFeatures
.filter((key): key is string => typeof key === 'string')
.slice(0, 2_000)
: [],
completedOres: Array.isArray(source.completedOres)
? source.completedOres
.filter((key): key is string => typeof key === 'string')
.slice(0, 10_000)
: [],
}
}
export function createShareQuery(workspace: SeedMapWorkspace): Record<string, string> {
return {
seed: workspace.seed,
edition: workspace.edition,
version: workspace.gameVersion,
dimension: workspace.dimension,
mode: workspace.displayMode,
ores: workspace.selectedOres.join(','),
yMin: workspace.oreYMin === null ? '' : String(workspace.oreYMin),
yMax: workspace.oreYMax === null ? '' : String(workspace.oreYMax),
x: String(Math.round(workspace.center.x)),
z: String(Math.round(workspace.center.z)),
zoom: String(workspace.zoom),
}
}
export function isCurrentSeedMapEpoch(responseEpoch: number, currentEpoch: number): boolean {
return responseEpoch === currentEpoch
}
export function applyShareQuery(
query: Record<string, unknown>,
workspace = createDefaultSeedMapWorkspace(),
): SeedMapWorkspace {
return sanitizeSeedMapWorkspace({
...workspace,
seed: typeof query.seed === 'string' ? query.seed : workspace.seed,
edition: query.edition,
gameVersion: typeof query.version === 'string' ? query.version : workspace.gameVersion,
dimension: query.dimension,
displayMode: query.mode,
selectedOres:
typeof query.ores === 'string' && query.ores.length > 0
? query.ores.split(',')
: workspace.selectedOres,
oreYMin: parseNullableQueryNumber(query.yMin, workspace.oreYMin),
oreYMax: parseNullableQueryNumber(query.yMax, workspace.oreYMax),
center: {
x: parseQueryNumber(query.x, workspace.center.x),
z: parseQueryNumber(query.z, workspace.center.z),
},
zoom: parseQueryNumber(query.zoom, workspace.zoom),
})
}
function finiteNumber(value: unknown, fallback: number): number {
return typeof value === 'number' && Number.isFinite(value) ? value : fallback
}
function sanitizeElevation(value: unknown, fallback: number): number {
const parsed = Math.round(finiteNumber(value, Number.NaN))
if (SEED_MAP_ELEVATIONS.some((elevation) => elevation === parsed)) return parsed
const normalizedFallback = Math.round(finiteNumber(fallback, SEED_MAP_ELEVATIONS[0]))
return SEED_MAP_ELEVATIONS.some((elevation) => elevation === normalizedFallback)
? normalizedFallback
: SEED_MAP_ELEVATIONS[0]
}
function parseQueryNumber(value: unknown, fallback: number): number {
if (typeof value !== 'string') return fallback
const parsed = Number(value)
return Number.isFinite(parsed) ? parsed : fallback
}
function parseNullableQueryNumber(value: unknown, fallback: number | null): number | null {
if (value === '') return null
if (typeof value !== 'string' && typeof value !== 'number') return fallback
const parsed = Number(value)
return Number.isFinite(parsed) ? parsed : fallback
}
function nullableInteger(value: unknown, min: number, max: number): number | null {
if (value === null || value === undefined || value === '') return null
const parsed = Number(value)
if (!Number.isFinite(parsed)) return null
return Math.min(max, Math.max(min, Math.round(parsed)))
}
function getBrowserStorage(): Storage | null {
return typeof window === 'undefined' ? null : window.localStorage
}
function sanitizeFeatureKinds(value: unknown): SeedMapFeatureKind[] {
if (!Array.isArray(value)) return createDefaultSeedMapWorkspace().visibleFeatures
const available = new Set(SEED_MAP_FEATURES.map((feature) => feature.kind))
return value.filter(
(kind): kind is SeedMapFeatureKind =>
typeof kind === 'string' && available.has(kind as SeedMapFeatureKind),
)
}
function sanitizeOreKinds(value: unknown): SeedMapOreKind[] {
if (!Array.isArray(value)) return []
const available = new Set(SEED_MAP_ORES.map((ore) => ore.kind))
return value
.filter(
(ore, index, values): ore is SeedMapOreKind =>
typeof ore === 'string' &&
available.has(ore as SeedMapOreKind) &&
values.indexOf(ore) === index,
)
.slice(0, SEED_MAP_ORES.length)
}
function sanitizeMarkers(value: unknown): SeedMapMarker[] {
if (!Array.isArray(value)) return []
return value
.flatMap((marker) => {
if (!marker || typeof marker !== 'object') return []
const source = marker as Partial<SeedMapMarker>
if (typeof source.id !== 'string' || typeof source.name !== 'string') return []
if (!Number.isFinite(source.x) || !Number.isFinite(source.z)) return []
return [
{
id: source.id.slice(0, 128),
name: source.name.slice(0, 128),
x: Math.round(source.x as number),
z: Math.round(source.z as number),
color:
typeof source.color === 'string' && /^#[0-9A-Fa-f]{6}$/.test(source.color)
? source.color
: '#22C55E',
},
]
})
.slice(0, 1_000)
}