Add Ground Zero terrain pipeline and playable assets
This commit is contained in:
+200
-69
@@ -24,6 +24,9 @@ Core commitments:
|
|||||||
- [ ] No fragmented DLC model.
|
- [ ] No fragmented DLC model.
|
||||||
- [ ] Build the smallest strong survival foundation first.
|
- [ ] Build the smallest strong survival foundation first.
|
||||||
- [ ] Keep every system compatible with long-term persistence and generations.
|
- [ ] Keep every system compatible with long-term persistence and generations.
|
||||||
|
- [ ] Build toward an Earth-scale world made from real-world terrain tiles.
|
||||||
|
- [ ] Keep travel paced by believable real-world movement, vehicles, terrain, and character condition.
|
||||||
|
- [ ] Treat terrain, bathymetry, biomes, resources, rivers, and mountains as data-driven long-term infrastructure, not one-off maps.
|
||||||
|
|
||||||
## Current Known Project Location
|
## Current Known Project Location
|
||||||
|
|
||||||
@@ -95,65 +98,6 @@ Use these markers as the project progresses:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# Version .01 / 0.01 - Foundation Baseline
|
|
||||||
|
|
||||||
Status: in progress.
|
|
||||||
|
|
||||||
Purpose: establish the project in GitHub, prove Windows builds from a clean checkout, and create the first C++ gameplay foundation for survival, inventory, crafting, building, persistence, admin testing, and wildlife.
|
|
||||||
|
|
||||||
Completed in version .01:
|
|
||||||
|
|
||||||
- [x] Created GitHub repository `pacificao/AgrarianGameBuild`.
|
|
||||||
- [x] Imported the Unreal project into Git with Git LFS.
|
|
||||||
- [x] Added Unreal-safe `.gitignore` and `.gitattributes`.
|
|
||||||
- [x] Added Windows build and cleanup helper scripts.
|
|
||||||
- [x] Removed stale `VisualStudioTools` plugin reference.
|
|
||||||
- [x] Confirmed Windows build succeeds through `Scripts/BuildEditor-Windows.bat`.
|
|
||||||
- [x] Added replicated survival component.
|
|
||||||
- [x] Added replicated inventory component.
|
|
||||||
- [x] Added interaction interface and server-authoritative interaction path.
|
|
||||||
- [x] Added resource node actor.
|
|
||||||
- [x] Added campfire actor.
|
|
||||||
- [x] Added primitive shelter actor.
|
|
||||||
- [x] Added world time/weather game state.
|
|
||||||
- [x] Added crafting component.
|
|
||||||
- [x] Added item definition and recipe data asset classes.
|
|
||||||
- [x] Added building placement component.
|
|
||||||
- [x] Added save game structures and persistence subsystem scaffold.
|
|
||||||
- [x] Added persistent actor component and placed actor save/restore foundation.
|
|
||||||
- [x] Added admin/dev console commands.
|
|
||||||
- [x] Added replicated wildlife base actor.
|
|
||||||
- [x] Built `AgrarianGameEditor Win64 Development` successfully on Windows-Builder.
|
|
||||||
- [x] Added a Codex headless Windows build lane through `/home/nathan/bin/agrarian-build-editor`.
|
|
||||||
- [x] Added a project-local Unreal Python execution wrapper for headless editor asset updates.
|
|
||||||
- [x] Installed VS 2022 Build Tools MSVC `14.44.35207` for Unreal 5.7 compatibility.
|
|
||||||
- [x] Fixed `AAgrarianDebugHUD` compile issue caused by `const` HUD helper methods calling non-const `AHUD::DrawText`.
|
|
||||||
- [x] Confirmed the project loads through Unreal Editor command mode and the default test map passes map check.
|
|
||||||
- [x] Created `IA_Interact` input action.
|
|
||||||
- [x] Bound `IA_Interact` to `E` and `Gamepad_FaceButton_Left`.
|
|
||||||
- [x] Assigned `IA_Interact` to the character Blueprint's `InteractAction`.
|
|
||||||
- [x] Created item definition assets for wood, stone, fiber, food, meat, hide, and primitive structure parts.
|
|
||||||
- [x] Created recipe data assets for campfire, primitive shelter, basic tool, and bandage.
|
|
||||||
- [x] Created item definition assets for craft result items: campfire, primitive shelter, basic tool, and bandage.
|
|
||||||
- [x] Created Blueprint child actors for wood resource, campfire, primitive shelter, and first wildlife species.
|
|
||||||
- [x] Placed and verified wood resource, campfire, primitive shelter, and rabbit wildlife Blueprints in the test map.
|
|
||||||
|
|
||||||
Open version .01 tasks:
|
|
||||||
|
|
||||||
- [x] Confirm the project opens cleanly in Unreal Editor after the latest wildlife commit.
|
|
||||||
- [x] Create `IA_Interact` input action.
|
|
||||||
- [x] Bind `IA_Interact` to `E` and a gamepad button.
|
|
||||||
- [x] Assign `IA_Interact` to the character Blueprint's `InteractAction`.
|
|
||||||
- [x] Create item definition assets for wood, stone, fiber, food, meat, hide, and primitive structure parts.
|
|
||||||
- [x] Create recipe data assets for campfire, primitive shelter, basic tool, and bandage.
|
|
||||||
- [x] Create item definition assets for craft result items: campfire, primitive shelter, basic tool, and bandage.
|
|
||||||
- [x] Create Blueprint child actors for wood resource, campfire, primitive shelter, and first wildlife species.
|
|
||||||
- [x] Place resource nodes, campfire, shelter, and wildlife in the test map.
|
|
||||||
- [~] Add a simple HUD/debug display for survival and inventory.
|
|
||||||
- [ ] Test gather -> inventory -> craft -> place shelter -> save/load loop.
|
|
||||||
- [ ] Test wildlife damage/death/harvest loop.
|
|
||||||
- [ ] Decide whether to keep the current template variants or remove unused starter variants.
|
|
||||||
|
|
||||||
# Phase 0 - Project Foundation And Guardrails
|
# Phase 0 - Project Foundation And Guardrails
|
||||||
|
|
||||||
Goal: Prepare the project so all future development is controlled, recoverable, documented, and aligned with the long-term vision.
|
Goal: Prepare the project so all future development is controlled, recoverable, documented, and aligned with the long-term vision.
|
||||||
@@ -208,6 +152,9 @@ Current tooling decisions:
|
|||||||
- [ ] Create technical design document.
|
- [ ] Create technical design document.
|
||||||
- [ ] Create multiplayer/networking design document.
|
- [ ] Create multiplayer/networking design document.
|
||||||
- [ ] Create persistence design document.
|
- [ ] Create persistence design document.
|
||||||
|
- [ ] Create Earth-scale terrain/tile streaming design document.
|
||||||
|
- [x] Create tile registry/database design document.
|
||||||
|
- [x] Create real-world terrain source data evaluation document.
|
||||||
- [ ] Create economy and AGR design document.
|
- [ ] Create economy and AGR design document.
|
||||||
- [ ] Create art direction document.
|
- [ ] Create art direction document.
|
||||||
- [ ] Create UX/HUD direction document.
|
- [ ] Create UX/HUD direction document.
|
||||||
@@ -240,8 +187,12 @@ Current tooling decisions:
|
|||||||
- [ ] Define what qualifies as the 6-month MVP.
|
- [ ] Define what qualifies as the 6-month MVP.
|
||||||
- [ ] Define what will not be included in MVP.
|
- [ ] Define what will not be included in MVP.
|
||||||
- [ ] Define target player count for MVP test.
|
- [ ] Define target player count for MVP test.
|
||||||
- [ ] Define target map size for MVP.
|
- [x] Select the real-world "Ground Zero" 1 km x 1 km MVP tile.
|
||||||
- [ ] Define MVP biome.
|
- [x] Define target map size for MVP around the Ground Zero tile.
|
||||||
|
- [x] Define MVP biome.
|
||||||
|
- [x] Define acceptable real terrain accuracy for MVP.
|
||||||
|
- [x] Define acceptable real bathymetry/ocean-depth handling for MVP if Ground Zero is coastal.
|
||||||
|
- [ ] Define acceptable first-pass biome/resource accuracy for MVP.
|
||||||
- [ ] Define MVP day/night length.
|
- [ ] Define MVP day/night length.
|
||||||
- [ ] Define survival pressure target.
|
- [ ] Define survival pressure target.
|
||||||
- [ ] Define basic success loop.
|
- [ ] Define basic success loop.
|
||||||
@@ -271,8 +222,116 @@ Current tooling decisions:
|
|||||||
- [ ] Add a build log retention policy.
|
- [ ] Add a build log retention policy.
|
||||||
- [ ] Add a simple operational runbook for rebooting/recovering Windows-Builder, Ubuntu-Codex, and DevBox.
|
- [ ] Add a simple operational runbook for rebooting/recovering Windows-Builder, Ubuntu-Codex, and DevBox.
|
||||||
|
|
||||||
|
## 0.7 Earth-Scale Terrain Architecture
|
||||||
|
|
||||||
|
Long-term goal: Agrarian can eventually expand toward an Earth-scale world made
|
||||||
|
from real terrain and ocean data. The intended unit is a 1 km x 1 km tile. At
|
||||||
|
full Earth scale this implies roughly 510-520 million possible tiles, added over
|
||||||
|
many years, streamed from servers, cached locally, scrubbed when unused, and
|
||||||
|
redownloaded when a player returns to a region.
|
||||||
|
|
||||||
|
- [ ] Define canonical tile coordinate system for 1 km x 1 km world tiles.
|
||||||
|
- [ ] Decide how latitude/longitude maps to Unreal coordinates and World Partition cells.
|
||||||
|
- [ ] Decide how to handle projection distortion, poles, and dateline crossing.
|
||||||
|
- [x] Define authoritative tile manifest schema.
|
||||||
|
- [x] Define tile status states: unknown, queued, source-data-found, generated, validated, packaged, published, deprecated.
|
||||||
|
- [ ] Define tile adjacency and stitching rules.
|
||||||
|
- [ ] Define tile versioning rules so terrain can improve without corrupting player state.
|
||||||
|
- [ ] Define server-side tile delivery protocol.
|
||||||
|
- [ ] Define client local tile cache layout.
|
||||||
|
- [ ] Define local cache retention policy for old or unused tiles.
|
||||||
|
- [ ] Define local cache redownload/revalidation behavior.
|
||||||
|
- [x] Define minimum metadata per tile: coordinates, biome, elevation range, water coverage, source datasets, generation version, neighbors, and publish status.
|
||||||
|
- [x] Evaluate real terrain/elevation data sources.
|
||||||
|
- [x] Evaluate bathymetry/ocean-depth data sources.
|
||||||
|
- [ ] Evaluate river, lake, coastline, road, and land-cover data sources.
|
||||||
|
- [ ] Define automated terrain import pipeline for a single 1 km tile.
|
||||||
|
- [ ] Define automated biome and natural resource inference pipeline.
|
||||||
|
- [ ] Define QA checks for terrain seams, water edges, slope extremes, and missing source data.
|
||||||
|
- [x] Build a small tile-tracking database prototype before scaling beyond MVP.
|
||||||
|
- [x] Build an MVP tile registry table for the Ground Zero tile and immediate neighbors.
|
||||||
|
- [ ] Keep World Partition compatibility as a hard requirement for all terrain decisions.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
# Version .01 / 0.01 - Foundation Baseline
|
||||||
|
|
||||||
|
Status: in progress.
|
||||||
|
|
||||||
|
Purpose: establish the project in GitHub, prove Windows builds from a clean checkout, and create the first C++ gameplay foundation for survival, inventory, crafting, building, persistence, admin testing, wildlife, and early terrain pipeline planning.
|
||||||
|
|
||||||
|
Completed in version .01:
|
||||||
|
|
||||||
|
- [x] Created GitHub repository `pacificao/AgrarianGameBuild`.
|
||||||
|
- [x] Imported the Unreal project into Git with Git LFS.
|
||||||
|
- [x] Added Unreal-safe `.gitignore` and `.gitattributes`.
|
||||||
|
- [x] Added Windows build and cleanup helper scripts.
|
||||||
|
- [x] Removed stale `VisualStudioTools` plugin reference.
|
||||||
|
- [x] Confirmed Windows build succeeds through `Scripts/BuildEditor-Windows.bat`.
|
||||||
|
- [x] Added replicated survival component.
|
||||||
|
- [x] Added replicated inventory component.
|
||||||
|
- [x] Added interaction interface and server-authoritative interaction path.
|
||||||
|
- [x] Added resource node actor.
|
||||||
|
- [x] Added campfire actor.
|
||||||
|
- [x] Added primitive shelter actor.
|
||||||
|
- [x] Added world time/weather game state.
|
||||||
|
- [x] Added crafting component.
|
||||||
|
- [x] Added item definition and recipe data asset classes.
|
||||||
|
- [x] Added building placement component.
|
||||||
|
- [x] Added save game structures and persistence subsystem scaffold.
|
||||||
|
- [x] Added persistent actor component and placed actor save/restore foundation.
|
||||||
|
- [x] Added admin/dev console commands.
|
||||||
|
- [x] Added replicated wildlife base actor.
|
||||||
|
- [x] Built `AgrarianGameEditor Win64 Development` successfully on Windows-Builder.
|
||||||
|
- [x] Added a Codex headless Windows build lane through `/home/nathan/bin/agrarian-build-editor`.
|
||||||
|
- [x] Added a project-local Unreal Python execution wrapper for headless editor asset updates.
|
||||||
|
- [x] Installed VS 2022 Build Tools MSVC `14.44.35207` for Unreal 5.7 compatibility.
|
||||||
|
- [x] Fixed `AAgrarianDebugHUD` compile issue caused by `const` HUD helper methods calling non-const `AHUD::DrawText`.
|
||||||
|
- [x] Confirmed the project loads through Unreal Editor command mode and the default test map passes map check.
|
||||||
|
- [x] Created `IA_Interact` input action.
|
||||||
|
- [x] Bound `IA_Interact` to `E` and `Gamepad_FaceButton_Left`.
|
||||||
|
- [x] Assigned `IA_Interact` to the character Blueprint's `InteractAction`.
|
||||||
|
- [x] Created item definition assets for wood, stone, fiber, food, meat, hide, and primitive structure parts.
|
||||||
|
- [x] Created recipe data assets for campfire, primitive shelter, basic tool, and bandage.
|
||||||
|
- [x] Created item definition assets for craft result items: campfire, primitive shelter, basic tool, and bandage.
|
||||||
|
- [x] Created Blueprint child actors for wood resource, campfire, primitive shelter, and first wildlife species.
|
||||||
|
- [x] Placed and verified wood resource, campfire, primitive shelter, and rabbit wildlife Blueprints in the test map.
|
||||||
|
- [x] Added command-mode smoke verification for wildlife damage/death/harvest loop.
|
||||||
|
- [x] Added automation coverage for live GameInstance persistence save/restore through `UAgrarianPersistenceSubsystem`.
|
||||||
|
- [x] Selected Ground Zero MVP terrain tile and added first tile registry schema.
|
||||||
|
- [x] Prototyped real elevation import for the Ground Zero tile using USGS EPQS samples.
|
||||||
|
- [x] Acquired and extracted final USGS 3DEP 1-meter DEM source for the Ground Zero tile.
|
||||||
|
- [x] Converted extracted 1-meter DEM subset into an Unreal Landscape-ready R16 heightmap and import plan.
|
||||||
|
- [x] Imported the Ground Zero R16 heightmap into `/Game/Agrarian/Maps/L_GroundZeroTerrain_Test`.
|
||||||
|
- [x] Verified the Ground Zero terrain test map is centered and spans 1 km x 1 km in Unreal.
|
||||||
|
|
||||||
|
Open version .01 tasks:
|
||||||
|
|
||||||
|
- [x] Confirm the project opens cleanly in Unreal Editor after the latest wildlife commit.
|
||||||
|
- [x] Create `IA_Interact` input action.
|
||||||
|
- [x] Bind `IA_Interact` to `E` and a gamepad button.
|
||||||
|
- [x] Assign `IA_Interact` to the character Blueprint's `InteractAction`.
|
||||||
|
- [x] Create item definition assets for wood, stone, fiber, food, meat, hide, and primitive structure parts.
|
||||||
|
- [x] Create recipe data assets for campfire, primitive shelter, basic tool, and bandage.
|
||||||
|
- [x] Create item definition assets for craft result items: campfire, primitive shelter, basic tool, and bandage.
|
||||||
|
- [x] Create Blueprint child actors for wood resource, campfire, primitive shelter, and first wildlife species.
|
||||||
|
- [x] Place resource nodes, campfire, shelter, and wildlife in the test map.
|
||||||
|
- [x] Added command-mode smoke verification for gather -> inventory -> craft -> place shelter -> save/load data loop.
|
||||||
|
- [x] Added fiber gathering and primitive shelter part recipes so shelter ingredients are naturally obtainable.
|
||||||
|
- [~] Add a simple HUD/debug display for survival and inventory.
|
||||||
|
- [x] Test gather -> inventory -> craft -> place shelter -> save/load loop.
|
||||||
|
- [x] Make primitive shelter ingredients naturally obtainable in normal play instead of smoke-test seeded.
|
||||||
|
- [x] Add a PIE/server persistence test that exercises `UAgrarianPersistenceSubsystem` with a live GameInstance.
|
||||||
|
- [x] Test wildlife damage/death/harvest loop.
|
||||||
|
- [ ] Decide whether to keep the current template variants or remove unused starter variants.
|
||||||
|
- [x] Choose Ground Zero 1 km MVP tile.
|
||||||
|
- [x] Build first tile metadata/registry prototype for Ground Zero.
|
||||||
|
- [x] Prove automated import of real terrain data into the MVP tile.
|
||||||
|
- [x] Define acceptable real terrain accuracy and final DEM/lidar source requirements for the MVP tile.
|
||||||
|
- [x] Acquire or extract the final USGS 3DEP DEM/lidar source for the Ground Zero tile.
|
||||||
|
- [x] Convert the extracted 1-meter DEM subset into an Unreal Landscape-ready heightmap and import plan.
|
||||||
|
- [x] Import the Ground Zero R16 heightmap into an Unreal terrain test map.
|
||||||
|
|
||||||
# Phase 1 - Foundational Survival MVP
|
# Phase 1 - Foundational Survival MVP
|
||||||
|
|
||||||
Goal: Build a small playable multiplayer prototype that proves survival, weather, inventory, gathering, crafting, shelter, and basic persistence can work together.
|
Goal: Build a small playable multiplayer prototype that proves survival, weather, inventory, gathering, crafting, shelter, and basic persistence can work together.
|
||||||
@@ -287,6 +346,9 @@ Target deliverable: A small group can join a server, spawn into one biome, gathe
|
|||||||
- [ ] Decide first-person, third-person, or hybrid camera.
|
- [ ] Decide first-person, third-person, or hybrid camera.
|
||||||
- [x] Implement movement.
|
- [x] Implement movement.
|
||||||
- [ ] Implement sprinting.
|
- [ ] Implement sprinting.
|
||||||
|
- [ ] Define real-world baseline walking speed.
|
||||||
|
- [ ] Define real-world baseline running speed.
|
||||||
|
- [ ] Connect movement speed to age, condition, strength, endurance, hunger, thirst, injury, carried weight, and terrain.
|
||||||
- [ ] Implement crouching if needed.
|
- [ ] Implement crouching if needed.
|
||||||
- [x] Implement jumping if needed.
|
- [x] Implement jumping if needed.
|
||||||
- [x] Implement interaction trace.
|
- [x] Implement interaction trace.
|
||||||
@@ -335,11 +397,20 @@ Target deliverable: A small group can join a server, spawn into one biome, gathe
|
|||||||
|
|
||||||
## 1.4 Single Biome MVP Map
|
## 1.4 Single Biome MVP Map
|
||||||
|
|
||||||
- [ ] Choose MVP biome.
|
- [x] Choose Ground Zero real-world 1 km x 1 km MVP tile.
|
||||||
|
- [x] Choose MVP biome based on Ground Zero location.
|
||||||
- [~] Create playable test map.
|
- [~] Create playable test map.
|
||||||
- [ ] Add terrain base.
|
- [x] Add terrain base from real elevation data.
|
||||||
|
- [ ] Add first-pass water depth/shoreline handling if applicable.
|
||||||
|
- [ ] Add first-pass hill, mountain, river, stream, lake, and coastline handling if present in Ground Zero.
|
||||||
|
- [x] Add source metadata record for the MVP tile.
|
||||||
|
- [x] Add generated tile metadata record for the MVP tile.
|
||||||
|
- [x] Verify terrain scale is 1 km x 1 km in Unreal.
|
||||||
|
- [x] Verify terrain tile origin and centered Unreal bounds for the Ground Zero test map.
|
||||||
|
- [ ] Verify neighboring tile edge coordinates against the registry before multi-tile stitching.
|
||||||
- [ ] Add foliage pass.
|
- [ ] Add foliage pass.
|
||||||
- [~] Add resource nodes.
|
- [~] Add resource nodes.
|
||||||
|
- [ ] Add biome-appropriate natural resources based on Ground Zero.
|
||||||
- [ ] Add water source.
|
- [ ] Add water source.
|
||||||
- [ ] Add weather exposure zones if needed.
|
- [ ] Add weather exposure zones if needed.
|
||||||
- [ ] Add landmark or ruin placeholder.
|
- [ ] Add landmark or ruin placeholder.
|
||||||
@@ -371,7 +442,7 @@ Target deliverable: A small group can join a server, spawn into one biome, gathe
|
|||||||
- [x] Create resource node base class.
|
- [x] Create resource node base class.
|
||||||
- [x] Add wood resource.
|
- [x] Add wood resource.
|
||||||
- [ ] Add stone resource.
|
- [ ] Add stone resource.
|
||||||
- [ ] Add fiber resource.
|
- [x] Add fiber resource.
|
||||||
- [ ] Add edible plant resource.
|
- [ ] Add edible plant resource.
|
||||||
- [ ] Add water gathering interaction.
|
- [ ] Add water gathering interaction.
|
||||||
- [x] Add resource depletion.
|
- [x] Add resource depletion.
|
||||||
@@ -390,6 +461,7 @@ Target deliverable: A small group can join a server, spawn into one biome, gathe
|
|||||||
- [x] Add primitive tool recipe.
|
- [x] Add primitive tool recipe.
|
||||||
- [x] Add campfire recipe.
|
- [x] Add campfire recipe.
|
||||||
- [x] Add shelter recipe.
|
- [x] Add shelter recipe.
|
||||||
|
- [x] Add recipes or gather paths for primitive shelter structure parts.
|
||||||
- [ ] Add simple container recipe.
|
- [ ] Add simple container recipe.
|
||||||
- [x] Add bandage or basic treatment recipe.
|
- [x] Add bandage or basic treatment recipe.
|
||||||
- [ ] Add crafting UI.
|
- [ ] Add crafting UI.
|
||||||
@@ -450,7 +522,7 @@ Target deliverable: A small group can join a server, spawn into one biome, gathe
|
|||||||
- [x] Add flee behavior.
|
- [x] Add flee behavior.
|
||||||
- [x] Add aggression behavior if needed.
|
- [x] Add aggression behavior if needed.
|
||||||
- [x] Add health.
|
- [x] Add health.
|
||||||
- [ ] Add damage.
|
- [x] Add damage.
|
||||||
- [x] Add harvesting interaction.
|
- [x] Add harvesting interaction.
|
||||||
- [x] Add meat/hide resources.
|
- [x] Add meat/hide resources.
|
||||||
- [ ] Add spawn manager.
|
- [ ] Add spawn manager.
|
||||||
@@ -462,6 +534,8 @@ Target deliverable: A small group can join a server, spawn into one biome, gathe
|
|||||||
- [ ] Confirm listen server vs dedicated server for MVP.
|
- [ ] Confirm listen server vs dedicated server for MVP.
|
||||||
- [ ] Create dedicated server build target if needed.
|
- [ ] Create dedicated server build target if needed.
|
||||||
- [ ] Add server travel flow.
|
- [ ] Add server travel flow.
|
||||||
|
- [ ] Define server authority over streamed terrain tiles.
|
||||||
|
- [ ] Define server response when a client requests a missing tile.
|
||||||
- [ ] Add player join flow.
|
- [ ] Add player join flow.
|
||||||
- [ ] Add player spawn flow.
|
- [ ] Add player spawn flow.
|
||||||
- [x] Add replicated player stats.
|
- [x] Add replicated player stats.
|
||||||
@@ -477,6 +551,7 @@ Target deliverable: A small group can join a server, spawn into one biome, gathe
|
|||||||
## 1.13 Persistence MVP
|
## 1.13 Persistence MVP
|
||||||
|
|
||||||
- [ ] Decide MVP persistence scope.
|
- [ ] Decide MVP persistence scope.
|
||||||
|
- [ ] Decide what tile metadata is stored in save data vs external tile registry.
|
||||||
- [ ] Save player identity.
|
- [ ] Save player identity.
|
||||||
- [ ] Save player stats.
|
- [ ] Save player stats.
|
||||||
- [ ] Save player inventory.
|
- [ ] Save player inventory.
|
||||||
@@ -488,6 +563,7 @@ Target deliverable: A small group can join a server, spawn into one biome, gathe
|
|||||||
- [ ] Add server-side save interval.
|
- [ ] Add server-side save interval.
|
||||||
- [x] Add manual admin save command.
|
- [x] Add manual admin save command.
|
||||||
- [ ] Add load-on-server-start.
|
- [ ] Add load-on-server-start.
|
||||||
|
- [ ] Add initial tile registry persistence for Ground Zero.
|
||||||
- [ ] Add backup-before-save option.
|
- [ ] Add backup-before-save option.
|
||||||
- [ ] Add recovery plan for corrupted save.
|
- [ ] Add recovery plan for corrupted save.
|
||||||
- [ ] Document persistence limitations.
|
- [ ] Document persistence limitations.
|
||||||
@@ -818,20 +894,32 @@ Goal: Keep the world alive for future generations through change, decay, scarcit
|
|||||||
|
|
||||||
# Phase 7 - Earth-Scale Expansion
|
# Phase 7 - Earth-Scale Expansion
|
||||||
|
|
||||||
Goal: Expand from a small test world toward a large, regionally diverse, persistent world.
|
Goal: Expand from a small test world toward a huge, regionally diverse, persistent world that can eventually approach Earth scale through staged 1 km x 1 km real-terrain tiles.
|
||||||
|
|
||||||
## 7.1 Terrain And World Partition
|
## 7.1 Terrain And World Partition
|
||||||
|
|
||||||
- [ ] Evaluate Unreal World Partition.
|
- [ ] Evaluate Unreal World Partition.
|
||||||
- [ ] Define terrain generation pipeline.
|
- [ ] Define real terrain generation pipeline.
|
||||||
|
- [ ] Define real bathymetry/ocean-depth generation pipeline.
|
||||||
- [ ] Define hand-authored vs procedural balance.
|
- [ ] Define hand-authored vs procedural balance.
|
||||||
|
- [ ] Define 1 km tile package format.
|
||||||
|
- [ ] Define 1 km tile World Partition placement rules.
|
||||||
|
- [ ] Define tile edge stitching and seam repair pipeline.
|
||||||
|
- [ ] Define terrain vertical scale policy for mountains, hills, and ocean depth.
|
||||||
|
- [ ] Define river, lake, coastline, and wetland generation rules.
|
||||||
|
- [ ] Define missing/low-quality source data fallback rules.
|
||||||
- [ ] Add streaming tests.
|
- [ ] Add streaming tests.
|
||||||
|
- [ ] Add server-delivered tile streaming prototype.
|
||||||
|
- [ ] Add client local tile cache prototype.
|
||||||
|
- [ ] Add stale tile scrubber prototype.
|
||||||
|
- [ ] Add tile redownload and version update prototype.
|
||||||
- [ ] Add biome boundaries.
|
- [ ] Add biome boundaries.
|
||||||
- [ ] Add region metadata.
|
- [ ] Add region metadata.
|
||||||
- [ ] Add server scaling plan.
|
- [ ] Add server scaling plan.
|
||||||
|
|
||||||
## 7.2 Biome Diversity
|
## 7.2 Biome Diversity
|
||||||
|
|
||||||
|
- [ ] Derive biome candidates from real-world land-cover, climate, elevation, and water data.
|
||||||
- [ ] Add forest biome.
|
- [ ] Add forest biome.
|
||||||
- [ ] Add plains biome.
|
- [ ] Add plains biome.
|
||||||
- [ ] Add mountain biome.
|
- [ ] Add mountain biome.
|
||||||
@@ -839,13 +927,20 @@ Goal: Expand from a small test world toward a large, regionally diverse, persist
|
|||||||
- [ ] Add desert/dryland biome.
|
- [ ] Add desert/dryland biome.
|
||||||
- [ ] Add cold biome.
|
- [ ] Add cold biome.
|
||||||
- [ ] Add biome-specific resources.
|
- [ ] Add biome-specific resources.
|
||||||
|
- [ ] Map natural resources to likely real-world geology, flora, water, and climate.
|
||||||
- [ ] Add biome-specific survival pressure.
|
- [ ] Add biome-specific survival pressure.
|
||||||
|
|
||||||
## 7.3 Logistics And Transportation
|
## 7.3 Logistics And Transportation
|
||||||
|
|
||||||
|
- [ ] Keep walking and running speeds close to real-world human pace.
|
||||||
|
- [ ] Keep animal travel speeds close to real-world animal pace.
|
||||||
|
- [ ] Define terrain slope, vegetation, mud, snow, water, injury, fatigue, age, strength, endurance, hunger, thirst, and carried weight modifiers for travel speed.
|
||||||
- [ ] Add roads.
|
- [ ] Add roads.
|
||||||
- [ ] Add carts.
|
- [ ] Add carts.
|
||||||
- [ ] Add pack animals.
|
- [ ] Add pack animals.
|
||||||
|
- [ ] Add horses.
|
||||||
|
- [ ] Add tractors.
|
||||||
|
- [ ] Add cars.
|
||||||
- [ ] Add boats.
|
- [ ] Add boats.
|
||||||
- [ ] Add shipping routes.
|
- [ ] Add shipping routes.
|
||||||
- [ ] Add regional trade value.
|
- [ ] Add regional trade value.
|
||||||
@@ -860,6 +955,18 @@ Goal: Expand from a small test world toward a large, regionally diverse, persist
|
|||||||
- [ ] Add price history.
|
- [ ] Add price history.
|
||||||
- [ ] Add economic dashboards.
|
- [ ] Add economic dashboards.
|
||||||
|
|
||||||
|
## 7.5 Tile Operations At Scale
|
||||||
|
|
||||||
|
- [ ] Define tile generation queue.
|
||||||
|
- [ ] Define tile publish queue.
|
||||||
|
- [ ] Define tile validation dashboard.
|
||||||
|
- [ ] Define operator tools for seeing generated, missing, stale, and failed tiles.
|
||||||
|
- [ ] Define player-demand driven tile prioritization.
|
||||||
|
- [ ] Define prefetch rules around active player regions.
|
||||||
|
- [ ] Define storage and CDN strategy for millions of generated tile packages.
|
||||||
|
- [ ] Define cost controls for serving and regenerating terrain content.
|
||||||
|
- [ ] Define long-term archival strategy for superseded tile versions.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# Phase 8 - Industrial And Automation Era
|
# Phase 8 - Industrial And Automation Era
|
||||||
@@ -989,6 +1096,13 @@ These tracks run across all phases and must not be left as afterthoughts.
|
|||||||
- [ ] Define what lives in external database.
|
- [ ] Define what lives in external database.
|
||||||
- [ ] Define player account model.
|
- [ ] Define player account model.
|
||||||
- [ ] Define world state model.
|
- [ ] Define world state model.
|
||||||
|
- [x] Define global tile registry data model for roughly 510-520 million potential 1 km tiles.
|
||||||
|
- [x] Define tile coordinate primary key strategy.
|
||||||
|
- [x] Define tile metadata tables.
|
||||||
|
- [x] Define tile generation job tables.
|
||||||
|
- [x] Define tile package/version tables.
|
||||||
|
- [ ] Define player/world-state anchoring to tile coordinates.
|
||||||
|
- [ ] Define terrain tile state vs player-made world state separation.
|
||||||
- [ ] Define backup policy.
|
- [ ] Define backup policy.
|
||||||
- [ ] Define rollback policy.
|
- [ ] Define rollback policy.
|
||||||
- [ ] Define migration policy.
|
- [ ] Define migration policy.
|
||||||
@@ -1051,6 +1165,10 @@ These tracks run across all phases and must not be left as afterthoughts.
|
|||||||
- [ ] Define server tick targets.
|
- [ ] Define server tick targets.
|
||||||
- [ ] Define memory budgets.
|
- [ ] Define memory budgets.
|
||||||
- [ ] Define actor count budgets.
|
- [ ] Define actor count budgets.
|
||||||
|
- [ ] Define World Partition streaming budgets.
|
||||||
|
- [ ] Define tile download bandwidth budget.
|
||||||
|
- [ ] Define local tile cache size budget.
|
||||||
|
- [ ] Define cache cleanup rules for unused terrain tiles.
|
||||||
- [ ] Add profiling checklist.
|
- [ ] Add profiling checklist.
|
||||||
- [ ] Add asset size budgets.
|
- [ ] Add asset size budgets.
|
||||||
- [ ] Add LOD strategy.
|
- [ ] Add LOD strategy.
|
||||||
@@ -1086,6 +1204,10 @@ These tracks run across all phases and must not be left as afterthoughts.
|
|||||||
- [ ] Create smoke test checklist.
|
- [ ] Create smoke test checklist.
|
||||||
- [ ] Create multiplayer test checklist.
|
- [ ] Create multiplayer test checklist.
|
||||||
- [ ] Create persistence test checklist.
|
- [ ] Create persistence test checklist.
|
||||||
|
- [ ] Create terrain tile import QA checklist.
|
||||||
|
- [ ] Create terrain seam QA checklist.
|
||||||
|
- [ ] Create biome/resource plausibility QA checklist.
|
||||||
|
- [ ] Create tile cache/download QA checklist.
|
||||||
- [ ] Create performance test checklist.
|
- [ ] Create performance test checklist.
|
||||||
- [ ] Create release candidate checklist.
|
- [ ] Create release candidate checklist.
|
||||||
- [ ] Add bug triage workflow.
|
- [ ] Add bug triage workflow.
|
||||||
@@ -1140,6 +1262,8 @@ These tracks run across all phases and must not be left as afterthoughts.
|
|||||||
- [x] Create networking baseline.
|
- [x] Create networking baseline.
|
||||||
- [x] Create build instructions.
|
- [x] Create build instructions.
|
||||||
- [x] Create operational build lane for Codex.
|
- [x] Create operational build lane for Codex.
|
||||||
|
- [x] Choose Ground Zero candidate tile.
|
||||||
|
- [x] Define first tile registry schema.
|
||||||
|
|
||||||
## Month 2 - Survival Loop
|
## Month 2 - Survival Loop
|
||||||
|
|
||||||
@@ -1153,6 +1277,7 @@ These tracks run across all phases and must not be left as afterthoughts.
|
|||||||
- [x] Add campfire.
|
- [x] Add campfire.
|
||||||
- [x] Add basic crafting.
|
- [x] Add basic crafting.
|
||||||
- [~] Add basic HUD.
|
- [~] Add basic HUD.
|
||||||
|
- [ ] Import first real terrain MVP tile if feasible.
|
||||||
|
|
||||||
## Month 3 - Shelter And Persistence
|
## Month 3 - Shelter And Persistence
|
||||||
|
|
||||||
@@ -1164,6 +1289,7 @@ These tracks run across all phases and must not be left as afterthoughts.
|
|||||||
- [ ] Add save/load for containers.
|
- [ ] Add save/load for containers.
|
||||||
- [ ] Add server restart persistence test.
|
- [ ] Add server restart persistence test.
|
||||||
- [ ] Add one full day/night survival test.
|
- [ ] Add one full day/night survival test.
|
||||||
|
- [ ] Persist MVP tile metadata and generated package version.
|
||||||
|
|
||||||
## Month 4 - Multiplayer And Wildlife
|
## Month 4 - Multiplayer And Wildlife
|
||||||
|
|
||||||
@@ -1250,8 +1376,13 @@ Next version .01 priorities:
|
|||||||
- [x] Place and test the primitive shelter.
|
- [x] Place and test the primitive shelter.
|
||||||
- [x] Place and test the rabbit wildlife Blueprint.
|
- [x] Place and test the rabbit wildlife Blueprint.
|
||||||
- [~] Add simple survival/inventory HUD feedback.
|
- [~] Add simple survival/inventory HUD feedback.
|
||||||
- [ ] Run the first full gather -> craft -> place -> save -> load test.
|
- [x] Run the first full gather -> craft -> place -> save -> load smoke test.
|
||||||
|
- [x] Test wildlife damage/death/harvest loop.
|
||||||
|
- [x] Make all primitive shelter ingredients obtainable through normal play.
|
||||||
|
- [x] Choose Ground Zero 1 km MVP tile.
|
||||||
|
- [x] Define first tile registry schema.
|
||||||
|
- [x] Prototype real terrain import for the selected MVP tile.
|
||||||
|
|
||||||
Immediate next item:
|
Immediate next item:
|
||||||
|
|
||||||
- [ ] Run the first full gather -> inventory -> craft -> place shelter -> save/load loop.
|
- [ ] Add first-pass water depth/shoreline handling if applicable.
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
+11
@@ -0,0 +1,11 @@
|
|||||||
|
<PAMDataset>
|
||||||
|
<PAMRasterBand band="1">
|
||||||
|
<Metadata>
|
||||||
|
<MDI key="STATISTICS_MAXIMUM">96.505706787109</MDI>
|
||||||
|
<MDI key="STATISTICS_MEAN">18.240529416485</MDI>
|
||||||
|
<MDI key="STATISTICS_MINIMUM">3.1603012084961</MDI>
|
||||||
|
<MDI key="STATISTICS_STDDEV">15.607071786077</MDI>
|
||||||
|
<MDI key="STATISTICS_VALID_PERCENT">100</MDI>
|
||||||
|
</Metadata>
|
||||||
|
</PAMRasterBand>
|
||||||
|
</PAMDataset>
|
||||||
+33
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"tile_id": "gz_us_ca_pacifica_utm10n_e544_n4160",
|
||||||
|
"source_tiffs": [
|
||||||
|
"Data/Terrain/Sources/gz_us_ca_pacifica_utm10n_e544_n4160/USGS_1M_10_x54y416_CA_CaliforniaGaps_B23.tif",
|
||||||
|
"Data/Terrain/Sources/gz_us_ca_pacifica_utm10n_e544_n4160/USGS_1M_10_x54y417_CA_CaliforniaGaps_B23.tif"
|
||||||
|
],
|
||||||
|
"output_tiff": "Data/Terrain/Extracted/gz_us_ca_pacifica_utm10n_e544_n4160/gz_us_ca_pacifica_utm10n_e544_n4160_1m_dem_subset.tif",
|
||||||
|
"source_crs": "EPSG:26910",
|
||||||
|
"source_bounds": [
|
||||||
|
[
|
||||||
|
539994.0000213826,
|
||||||
|
4149994.000039856,
|
||||||
|
550006.0000213826,
|
||||||
|
4160006.000039856
|
||||||
|
],
|
||||||
|
[
|
||||||
|
539994.0000156849,
|
||||||
|
4159993.9999746536,
|
||||||
|
550006.0000156849,
|
||||||
|
4170005.9999746536
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"subset_bounds_utm_m": [
|
||||||
|
544000,
|
||||||
|
4160000,
|
||||||
|
545000,
|
||||||
|
4161000
|
||||||
|
],
|
||||||
|
"subset_width_pixels": 1000,
|
||||||
|
"subset_height_pixels": 1000,
|
||||||
|
"pixel_size_x": 1.0,
|
||||||
|
"pixel_size_y": 1.0
|
||||||
|
}
|
||||||
+1090
File diff suppressed because it is too large
Load Diff
BIN
Binary file not shown.
+44
@@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"tile_id": "gz_us_ca_pacifica_utm10n_e544_n4160",
|
||||||
|
"generated_at_utc": "2026-05-14T05:21:46Z",
|
||||||
|
"source": {
|
||||||
|
"name": "USGS Elevation Point Query Service",
|
||||||
|
"url": "https://epqs.nationalmap.gov/v1/json",
|
||||||
|
"units": "Meters",
|
||||||
|
"raster_ids": [
|
||||||
|
65833
|
||||||
|
],
|
||||||
|
"reported_resolutions_m": [
|
||||||
|
1
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"grid": {
|
||||||
|
"projection": "WGS84 / UTM zone 10N",
|
||||||
|
"utm_zone": "10N",
|
||||||
|
"easting_min_m": 544000,
|
||||||
|
"northing_min_m": 4160000,
|
||||||
|
"easting_max_m": 545000,
|
||||||
|
"northing_max_m": 4161000,
|
||||||
|
"tile_size_m": 1000,
|
||||||
|
"center_latitude": 37.5925,
|
||||||
|
"center_longitude": -122.4995
|
||||||
|
},
|
||||||
|
"sample_grid": {
|
||||||
|
"width": 33,
|
||||||
|
"height": 33,
|
||||||
|
"spacing_m": 31.25,
|
||||||
|
"sample_count": 1089
|
||||||
|
},
|
||||||
|
"heightmap": {
|
||||||
|
"format": "r16_little_endian_unsigned",
|
||||||
|
"min_elevation_m": 3.458832026,
|
||||||
|
"max_elevation_m": 96.11089325,
|
||||||
|
"vertical_range_m": 92.65206122400001
|
||||||
|
},
|
||||||
|
"unreal_import_notes": [
|
||||||
|
"Prototype only; final landscape import should use a higher-resolution DEM raster or lidar-derived terrain.",
|
||||||
|
"R16 values are normalized from min_elevation_m to max_elevation_m.",
|
||||||
|
"Use the metadata min/max values to restore vertical scale during Unreal import.",
|
||||||
|
"Horizontal tile size is 1000 m x 1000 m."
|
||||||
|
]
|
||||||
|
}
|
||||||
+189
@@ -0,0 +1,189 @@
|
|||||||
|
{
|
||||||
|
"tile_id": "gz_us_ca_pacifica_utm10n_e544_n4160",
|
||||||
|
"acquired_at_utc": "2026-05-14T05:44:52Z",
|
||||||
|
"tnm_query": {
|
||||||
|
"url": "https://tnmaccess.nationalmap.gov/api/v1/products?bbox=-122.5016359968%2C37.5859588516%2C-122.4902484781%2C37.5950204012&datasets=Digital+Elevation+Model+%28DEM%29+1+meter&prodFormats=GeoTIFF&outputFormat=JSON",
|
||||||
|
"bbox_lon_lat": [
|
||||||
|
-122.50163599677178,
|
||||||
|
37.58595885158331,
|
||||||
|
-122.49024847810712,
|
||||||
|
37.59502040118447
|
||||||
|
],
|
||||||
|
"dataset": "Digital Elevation Model (DEM) 1 meter",
|
||||||
|
"product_count": 2
|
||||||
|
},
|
||||||
|
"selected_product": {
|
||||||
|
"title": "USGS 1 Meter 10 x54y416 CA_CaliforniaGaps_B23",
|
||||||
|
"moreInfo": "This is a tile of the standard one-meter resolution digital elevation model (DEM) produced through the 3D Elevation Program (3DEP) . The elevations in this DEM represent the topographic bare-earth surface. USGS standard one-meter DEMs are produced exclusively from high resolution light detection and ranging (lidar) source data of one-meter or higher resolution. One-meter DEM surfaces are seamless within collection projects, but, not necessarily seamless across projects. The spatial reference used for tiles of the one-meter DEM within the conterminous United States (CONUS) is Universal Transverse Mercator (UTM) in units of meters, and in conformance with the North American Datum of 1983 (NAD83). All bare earth elevation values are in [...]",
|
||||||
|
"sourceId": "68ad17a7d4be0220fe216966",
|
||||||
|
"sourceName": "ScienceBase",
|
||||||
|
"sourceOriginId": null,
|
||||||
|
"sourceOriginName": "gda",
|
||||||
|
"metaUrl": "https://www.sciencebase.gov/catalog/item/68ad17a7d4be0220fe216966",
|
||||||
|
"vendorMetaUrl": "https://thor-f5.er.usgs.gov/ngtoc/metadata/waf/elevation/1_meter/geotiff/CA_CaliforniaGaps_B23/USGS_1M_10_x54y416_CA_CaliforniaGaps_B23.xml",
|
||||||
|
"publicationDate": "2025-08-20",
|
||||||
|
"lastUpdated": "2025-08-26T00:01:03.785-06:00",
|
||||||
|
"dateCreated": "2025-08-25T20:10:47.210-06:00",
|
||||||
|
"sizeInBytes": 279529890,
|
||||||
|
"extent": "10000 x 10000 meter",
|
||||||
|
"format": "GeoTIFF",
|
||||||
|
"downloadURL": "https://prd-tnm.s3.amazonaws.com/StagedProducts/Elevation/1m/Projects/CA_CaliforniaGaps_B23/TIFF/USGS_1M_10_x54y416_CA_CaliforniaGaps_B23.tif",
|
||||||
|
"downloadURLRaster": null,
|
||||||
|
"previewGraphicURL": "https://prd-tnm.s3.amazonaws.com/StagedProducts/Elevation/1m/Projects/CA_CaliforniaGaps_B23/browse/USGS_1M_10_x54y416_CA_CaliforniaGaps_B23.jpg",
|
||||||
|
"downloadLazURL": null,
|
||||||
|
"urls": {
|
||||||
|
"TIFF": "https://prd-tnm.s3.amazonaws.com/StagedProducts/Elevation/1m/Projects/CA_CaliforniaGaps_B23/TIFF/USGS_1M_10_x54y416_CA_CaliforniaGaps_B23.tif"
|
||||||
|
},
|
||||||
|
"datasets": [],
|
||||||
|
"boundingBox": {
|
||||||
|
"minX": -122.54748552499996,
|
||||||
|
"maxX": -122.43368012899998,
|
||||||
|
"minY": 37.49556910100006,
|
||||||
|
"maxY": 37.586189803000025
|
||||||
|
},
|
||||||
|
"bestFitIndex": 0.0,
|
||||||
|
"body": "This is a tile of the standard one-meter resolution digital elevation model (DEM) produced through the 3D Elevation Program (3DEP) . The elevations in this DEM represent the topographic bare-earth surface. USGS standard one-meter DEMs are produced exclusively from high resolution light detection and ranging (lidar) source data of one-meter or higher resolution. One-meter DEM surfaces are seamless within collection projects, but, not necessarily seamless across projects. The spatial reference used for tiles of the one-meter DEM within the conterminous United States (CONUS) is Universal Transverse Mercator (UTM) in units of meters, and in conformance with the North American Datum of 1983 (NAD83). All bare earth elevation values are in meters and are referenced to the North American Vertical Datum of 1988 (NAVD88). Each tile is distributed in the UTM Zone in which it lies. If a tile crosses two UTM zones, it is delivered in both zones. The one-meter DEM is the highest resolution standard DEM offered in the 3DEP product suite. Other 3DEP products are nationally seamless DEMs in resolutions of 1/3, 1, and 2 arc seconds. These seamless DEMs were referred to as the National Elevation Dataset (NED) from about 2000 through 2015 at which time they became the seamless DEM layers under the 3DEP program and lost the NED branding. Other 3DEP products include five-meter DEMs in Alaska as well as various source datasets including the lidar point cloud and interferometric synthetic aperture radar (Ifsar) digital surface models and intensity images. All 3DEP products are public domain.",
|
||||||
|
"processingUrl": "processingUrl",
|
||||||
|
"modificationInfo": "2025-08-25"
|
||||||
|
},
|
||||||
|
"coverage_products": [
|
||||||
|
{
|
||||||
|
"title": "USGS 1 Meter 10 x54y416 CA_CaliforniaGaps_B23",
|
||||||
|
"moreInfo": "This is a tile of the standard one-meter resolution digital elevation model (DEM) produced through the 3D Elevation Program (3DEP) . The elevations in this DEM represent the topographic bare-earth surface. USGS standard one-meter DEMs are produced exclusively from high resolution light detection and ranging (lidar) source data of one-meter or higher resolution. One-meter DEM surfaces are seamless within collection projects, but, not necessarily seamless across projects. The spatial reference used for tiles of the one-meter DEM within the conterminous United States (CONUS) is Universal Transverse Mercator (UTM) in units of meters, and in conformance with the North American Datum of 1983 (NAD83). All bare earth elevation values are in [...]",
|
||||||
|
"sourceId": "68ad17a7d4be0220fe216966",
|
||||||
|
"sourceName": "ScienceBase",
|
||||||
|
"sourceOriginId": null,
|
||||||
|
"sourceOriginName": "gda",
|
||||||
|
"metaUrl": "https://www.sciencebase.gov/catalog/item/68ad17a7d4be0220fe216966",
|
||||||
|
"vendorMetaUrl": "https://thor-f5.er.usgs.gov/ngtoc/metadata/waf/elevation/1_meter/geotiff/CA_CaliforniaGaps_B23/USGS_1M_10_x54y416_CA_CaliforniaGaps_B23.xml",
|
||||||
|
"publicationDate": "2025-08-20",
|
||||||
|
"lastUpdated": "2025-08-26T00:01:03.785-06:00",
|
||||||
|
"dateCreated": "2025-08-25T20:10:47.210-06:00",
|
||||||
|
"sizeInBytes": 279529890,
|
||||||
|
"extent": "10000 x 10000 meter",
|
||||||
|
"format": "GeoTIFF",
|
||||||
|
"downloadURL": "https://prd-tnm.s3.amazonaws.com/StagedProducts/Elevation/1m/Projects/CA_CaliforniaGaps_B23/TIFF/USGS_1M_10_x54y416_CA_CaliforniaGaps_B23.tif",
|
||||||
|
"downloadURLRaster": null,
|
||||||
|
"previewGraphicURL": "https://prd-tnm.s3.amazonaws.com/StagedProducts/Elevation/1m/Projects/CA_CaliforniaGaps_B23/browse/USGS_1M_10_x54y416_CA_CaliforniaGaps_B23.jpg",
|
||||||
|
"downloadLazURL": null,
|
||||||
|
"urls": {
|
||||||
|
"TIFF": "https://prd-tnm.s3.amazonaws.com/StagedProducts/Elevation/1m/Projects/CA_CaliforniaGaps_B23/TIFF/USGS_1M_10_x54y416_CA_CaliforniaGaps_B23.tif"
|
||||||
|
},
|
||||||
|
"datasets": [],
|
||||||
|
"boundingBox": {
|
||||||
|
"minX": -122.54748552499996,
|
||||||
|
"maxX": -122.43368012899998,
|
||||||
|
"minY": 37.49556910100006,
|
||||||
|
"maxY": 37.586189803000025
|
||||||
|
},
|
||||||
|
"bestFitIndex": 0.0,
|
||||||
|
"body": "This is a tile of the standard one-meter resolution digital elevation model (DEM) produced through the 3D Elevation Program (3DEP) . The elevations in this DEM represent the topographic bare-earth surface. USGS standard one-meter DEMs are produced exclusively from high resolution light detection and ranging (lidar) source data of one-meter or higher resolution. One-meter DEM surfaces are seamless within collection projects, but, not necessarily seamless across projects. The spatial reference used for tiles of the one-meter DEM within the conterminous United States (CONUS) is Universal Transverse Mercator (UTM) in units of meters, and in conformance with the North American Datum of 1983 (NAD83). All bare earth elevation values are in meters and are referenced to the North American Vertical Datum of 1988 (NAVD88). Each tile is distributed in the UTM Zone in which it lies. If a tile crosses two UTM zones, it is delivered in both zones. The one-meter DEM is the highest resolution standard DEM offered in the 3DEP product suite. Other 3DEP products are nationally seamless DEMs in resolutions of 1/3, 1, and 2 arc seconds. These seamless DEMs were referred to as the National Elevation Dataset (NED) from about 2000 through 2015 at which time they became the seamless DEM layers under the 3DEP program and lost the NED branding. Other 3DEP products include five-meter DEMs in Alaska as well as various source datasets including the lidar point cloud and interferometric synthetic aperture radar (Ifsar) digital surface models and intensity images. All 3DEP products are public domain.",
|
||||||
|
"processingUrl": "processingUrl",
|
||||||
|
"modificationInfo": "2025-08-25"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "USGS 1 Meter 10 x54y417 CA_CaliforniaGaps_B23",
|
||||||
|
"moreInfo": "This is a tile of the standard one-meter resolution digital elevation model (DEM) produced through the 3D Elevation Program (3DEP) . The elevations in this DEM represent the topographic bare-earth surface. USGS standard one-meter DEMs are produced exclusively from high resolution light detection and ranging (lidar) source data of one-meter or higher resolution. One-meter DEM surfaces are seamless within collection projects, but, not necessarily seamless across projects. The spatial reference used for tiles of the one-meter DEM within the conterminous United States (CONUS) is Universal Transverse Mercator (UTM) in units of meters, and in conformance with the North American Datum of 1983 (NAD83). All bare earth elevation values are in [...]",
|
||||||
|
"sourceId": "68ad17a7d4be0220fe216962",
|
||||||
|
"sourceName": "ScienceBase",
|
||||||
|
"sourceOriginId": null,
|
||||||
|
"sourceOriginName": "gda",
|
||||||
|
"metaUrl": "https://www.sciencebase.gov/catalog/item/68ad17a7d4be0220fe216962",
|
||||||
|
"vendorMetaUrl": "https://thor-f5.er.usgs.gov/ngtoc/metadata/waf/elevation/1_meter/geotiff/CA_CaliforniaGaps_B23/USGS_1M_10_x54y417_CA_CaliforniaGaps_B23.xml",
|
||||||
|
"publicationDate": "2025-08-20",
|
||||||
|
"lastUpdated": "2025-08-26T00:00:51.760-06:00",
|
||||||
|
"dateCreated": "2025-08-25T20:10:47.009-06:00",
|
||||||
|
"sizeInBytes": 230229371,
|
||||||
|
"extent": "10000 x 10000 meter",
|
||||||
|
"format": "GeoTIFF",
|
||||||
|
"downloadURL": "https://prd-tnm.s3.amazonaws.com/StagedProducts/Elevation/1m/Projects/CA_CaliforniaGaps_B23/TIFF/USGS_1M_10_x54y417_CA_CaliforniaGaps_B23.tif",
|
||||||
|
"downloadURLRaster": null,
|
||||||
|
"previewGraphicURL": "https://prd-tnm.s3.amazonaws.com/StagedProducts/Elevation/1m/Projects/CA_CaliforniaGaps_B23/browse/USGS_1M_10_x54y417_CA_CaliforniaGaps_B23.jpg",
|
||||||
|
"downloadLazURL": null,
|
||||||
|
"urls": {
|
||||||
|
"TIFF": "https://prd-tnm.s3.amazonaws.com/StagedProducts/Elevation/1m/Projects/CA_CaliforniaGaps_B23/TIFF/USGS_1M_10_x54y417_CA_CaliforniaGaps_B23.tif"
|
||||||
|
},
|
||||||
|
"datasets": [],
|
||||||
|
"boundingBox": {
|
||||||
|
"minX": -122.54694045199994,
|
||||||
|
"maxX": -122.43299576699997,
|
||||||
|
"minY": 37.585700737000025,
|
||||||
|
"maxY": 37.676321632000054
|
||||||
|
},
|
||||||
|
"bestFitIndex": 0.0,
|
||||||
|
"body": "This is a tile of the standard one-meter resolution digital elevation model (DEM) produced through the 3D Elevation Program (3DEP) . The elevations in this DEM represent the topographic bare-earth surface. USGS standard one-meter DEMs are produced exclusively from high resolution light detection and ranging (lidar) source data of one-meter or higher resolution. One-meter DEM surfaces are seamless within collection projects, but, not necessarily seamless across projects. The spatial reference used for tiles of the one-meter DEM within the conterminous United States (CONUS) is Universal Transverse Mercator (UTM) in units of meters, and in conformance with the North American Datum of 1983 (NAD83). All bare earth elevation values are in meters and are referenced to the North American Vertical Datum of 1988 (NAVD88). Each tile is distributed in the UTM Zone in which it lies. If a tile crosses two UTM zones, it is delivered in both zones. The one-meter DEM is the highest resolution standard DEM offered in the 3DEP product suite. Other 3DEP products are nationally seamless DEMs in resolutions of 1/3, 1, and 2 arc seconds. These seamless DEMs were referred to as the National Elevation Dataset (NED) from about 2000 through 2015 at which time they became the seamless DEM layers under the 3DEP program and lost the NED branding. Other 3DEP products include five-meter DEMs in Alaska as well as various source datasets including the lidar point cloud and interferometric synthetic aperture radar (Ifsar) digital surface models and intensity images. All 3DEP products are public domain.",
|
||||||
|
"processingUrl": "processingUrl",
|
||||||
|
"modificationInfo": "2025-08-25"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"all_products": [
|
||||||
|
{
|
||||||
|
"title": "USGS 1 Meter 10 x54y416 CA_CaliforniaGaps_B23",
|
||||||
|
"moreInfo": "This is a tile of the standard one-meter resolution digital elevation model (DEM) produced through the 3D Elevation Program (3DEP) . The elevations in this DEM represent the topographic bare-earth surface. USGS standard one-meter DEMs are produced exclusively from high resolution light detection and ranging (lidar) source data of one-meter or higher resolution. One-meter DEM surfaces are seamless within collection projects, but, not necessarily seamless across projects. The spatial reference used for tiles of the one-meter DEM within the conterminous United States (CONUS) is Universal Transverse Mercator (UTM) in units of meters, and in conformance with the North American Datum of 1983 (NAD83). All bare earth elevation values are in [...]",
|
||||||
|
"sourceId": "68ad17a7d4be0220fe216966",
|
||||||
|
"sourceName": "ScienceBase",
|
||||||
|
"sourceOriginId": null,
|
||||||
|
"sourceOriginName": "gda",
|
||||||
|
"metaUrl": "https://www.sciencebase.gov/catalog/item/68ad17a7d4be0220fe216966",
|
||||||
|
"vendorMetaUrl": "https://thor-f5.er.usgs.gov/ngtoc/metadata/waf/elevation/1_meter/geotiff/CA_CaliforniaGaps_B23/USGS_1M_10_x54y416_CA_CaliforniaGaps_B23.xml",
|
||||||
|
"publicationDate": "2025-08-20",
|
||||||
|
"lastUpdated": "2025-08-26T00:01:03.785-06:00",
|
||||||
|
"dateCreated": "2025-08-25T20:10:47.210-06:00",
|
||||||
|
"sizeInBytes": 279529890,
|
||||||
|
"extent": "10000 x 10000 meter",
|
||||||
|
"format": "GeoTIFF",
|
||||||
|
"downloadURL": "https://prd-tnm.s3.amazonaws.com/StagedProducts/Elevation/1m/Projects/CA_CaliforniaGaps_B23/TIFF/USGS_1M_10_x54y416_CA_CaliforniaGaps_B23.tif",
|
||||||
|
"downloadURLRaster": null,
|
||||||
|
"previewGraphicURL": "https://prd-tnm.s3.amazonaws.com/StagedProducts/Elevation/1m/Projects/CA_CaliforniaGaps_B23/browse/USGS_1M_10_x54y416_CA_CaliforniaGaps_B23.jpg",
|
||||||
|
"downloadLazURL": null,
|
||||||
|
"urls": {
|
||||||
|
"TIFF": "https://prd-tnm.s3.amazonaws.com/StagedProducts/Elevation/1m/Projects/CA_CaliforniaGaps_B23/TIFF/USGS_1M_10_x54y416_CA_CaliforniaGaps_B23.tif"
|
||||||
|
},
|
||||||
|
"datasets": [],
|
||||||
|
"boundingBox": {
|
||||||
|
"minX": -122.54748552499996,
|
||||||
|
"maxX": -122.43368012899998,
|
||||||
|
"minY": 37.49556910100006,
|
||||||
|
"maxY": 37.586189803000025
|
||||||
|
},
|
||||||
|
"bestFitIndex": 0.0,
|
||||||
|
"body": "This is a tile of the standard one-meter resolution digital elevation model (DEM) produced through the 3D Elevation Program (3DEP) . The elevations in this DEM represent the topographic bare-earth surface. USGS standard one-meter DEMs are produced exclusively from high resolution light detection and ranging (lidar) source data of one-meter or higher resolution. One-meter DEM surfaces are seamless within collection projects, but, not necessarily seamless across projects. The spatial reference used for tiles of the one-meter DEM within the conterminous United States (CONUS) is Universal Transverse Mercator (UTM) in units of meters, and in conformance with the North American Datum of 1983 (NAD83). All bare earth elevation values are in meters and are referenced to the North American Vertical Datum of 1988 (NAVD88). Each tile is distributed in the UTM Zone in which it lies. If a tile crosses two UTM zones, it is delivered in both zones. The one-meter DEM is the highest resolution standard DEM offered in the 3DEP product suite. Other 3DEP products are nationally seamless DEMs in resolutions of 1/3, 1, and 2 arc seconds. These seamless DEMs were referred to as the National Elevation Dataset (NED) from about 2000 through 2015 at which time they became the seamless DEM layers under the 3DEP program and lost the NED branding. Other 3DEP products include five-meter DEMs in Alaska as well as various source datasets including the lidar point cloud and interferometric synthetic aperture radar (Ifsar) digital surface models and intensity images. All 3DEP products are public domain.",
|
||||||
|
"processingUrl": "processingUrl",
|
||||||
|
"modificationInfo": "2025-08-25"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "USGS 1 Meter 10 x54y417 CA_CaliforniaGaps_B23",
|
||||||
|
"moreInfo": "This is a tile of the standard one-meter resolution digital elevation model (DEM) produced through the 3D Elevation Program (3DEP) . The elevations in this DEM represent the topographic bare-earth surface. USGS standard one-meter DEMs are produced exclusively from high resolution light detection and ranging (lidar) source data of one-meter or higher resolution. One-meter DEM surfaces are seamless within collection projects, but, not necessarily seamless across projects. The spatial reference used for tiles of the one-meter DEM within the conterminous United States (CONUS) is Universal Transverse Mercator (UTM) in units of meters, and in conformance with the North American Datum of 1983 (NAD83). All bare earth elevation values are in [...]",
|
||||||
|
"sourceId": "68ad17a7d4be0220fe216962",
|
||||||
|
"sourceName": "ScienceBase",
|
||||||
|
"sourceOriginId": null,
|
||||||
|
"sourceOriginName": "gda",
|
||||||
|
"metaUrl": "https://www.sciencebase.gov/catalog/item/68ad17a7d4be0220fe216962",
|
||||||
|
"vendorMetaUrl": "https://thor-f5.er.usgs.gov/ngtoc/metadata/waf/elevation/1_meter/geotiff/CA_CaliforniaGaps_B23/USGS_1M_10_x54y417_CA_CaliforniaGaps_B23.xml",
|
||||||
|
"publicationDate": "2025-08-20",
|
||||||
|
"lastUpdated": "2025-08-26T00:00:51.760-06:00",
|
||||||
|
"dateCreated": "2025-08-25T20:10:47.009-06:00",
|
||||||
|
"sizeInBytes": 230229371,
|
||||||
|
"extent": "10000 x 10000 meter",
|
||||||
|
"format": "GeoTIFF",
|
||||||
|
"downloadURL": "https://prd-tnm.s3.amazonaws.com/StagedProducts/Elevation/1m/Projects/CA_CaliforniaGaps_B23/TIFF/USGS_1M_10_x54y417_CA_CaliforniaGaps_B23.tif",
|
||||||
|
"downloadURLRaster": null,
|
||||||
|
"previewGraphicURL": "https://prd-tnm.s3.amazonaws.com/StagedProducts/Elevation/1m/Projects/CA_CaliforniaGaps_B23/browse/USGS_1M_10_x54y417_CA_CaliforniaGaps_B23.jpg",
|
||||||
|
"downloadLazURL": null,
|
||||||
|
"urls": {
|
||||||
|
"TIFF": "https://prd-tnm.s3.amazonaws.com/StagedProducts/Elevation/1m/Projects/CA_CaliforniaGaps_B23/TIFF/USGS_1M_10_x54y417_CA_CaliforniaGaps_B23.tif"
|
||||||
|
},
|
||||||
|
"datasets": [],
|
||||||
|
"boundingBox": {
|
||||||
|
"minX": -122.54694045199994,
|
||||||
|
"maxX": -122.43299576699997,
|
||||||
|
"minY": 37.585700737000025,
|
||||||
|
"maxY": 37.676321632000054
|
||||||
|
},
|
||||||
|
"bestFitIndex": 0.0,
|
||||||
|
"body": "This is a tile of the standard one-meter resolution digital elevation model (DEM) produced through the 3D Elevation Program (3DEP) . The elevations in this DEM represent the topographic bare-earth surface. USGS standard one-meter DEMs are produced exclusively from high resolution light detection and ranging (lidar) source data of one-meter or higher resolution. One-meter DEM surfaces are seamless within collection projects, but, not necessarily seamless across projects. The spatial reference used for tiles of the one-meter DEM within the conterminous United States (CONUS) is Universal Transverse Mercator (UTM) in units of meters, and in conformance with the North American Datum of 1983 (NAD83). All bare earth elevation values are in meters and are referenced to the North American Vertical Datum of 1988 (NAVD88). Each tile is distributed in the UTM Zone in which it lies. If a tile crosses two UTM zones, it is delivered in both zones. The one-meter DEM is the highest resolution standard DEM offered in the 3DEP product suite. Other 3DEP products are nationally seamless DEMs in resolutions of 1/3, 1, and 2 arc seconds. These seamless DEMs were referred to as the National Elevation Dataset (NED) from about 2000 through 2015 at which time they became the seamless DEM layers under the 3DEP program and lost the NED branding. Other 3DEP products include five-meter DEMs in Alaska as well as various source datasets including the lidar point cloud and interferometric synthetic aperture radar (Ifsar) digital surface models and intensity images. All 3DEP products are public domain.",
|
||||||
|
"processingUrl": "processingUrl",
|
||||||
|
"modificationInfo": "2025-08-25"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
BIN
Binary file not shown.
+4
File diff suppressed because one or more lines are too long
+47
@@ -0,0 +1,47 @@
|
|||||||
|
{
|
||||||
|
"tile_id": "gz_us_ca_pacifica_utm10n_e544_n4160",
|
||||||
|
"generated_at_utc": "2026-05-14T06:16:06Z",
|
||||||
|
"source_dem": "Data/Terrain/Extracted/gz_us_ca_pacifica_utm10n_e544_n4160/gz_us_ca_pacifica_utm10n_e544_n4160_1m_dem_subset.tif",
|
||||||
|
"source_crs": "EPSG:26910",
|
||||||
|
"source_bounds": [
|
||||||
|
544000.0,
|
||||||
|
4160000.0,
|
||||||
|
545000.0,
|
||||||
|
4161000.0
|
||||||
|
],
|
||||||
|
"source_size_pixels": [
|
||||||
|
1000,
|
||||||
|
1000
|
||||||
|
],
|
||||||
|
"source_pixel_size_m": [
|
||||||
|
1.0,
|
||||||
|
1.0
|
||||||
|
],
|
||||||
|
"heightmap": {
|
||||||
|
"format": "r16_little_endian_unsigned",
|
||||||
|
"width": 1009,
|
||||||
|
"height": 1009,
|
||||||
|
"r16_path": "Data/Terrain/Unreal/gz_us_ca_pacifica_utm10n_e544_n4160/gz_us_ca_pacifica_utm10n_e544_n4160_unreal_1009.r16",
|
||||||
|
"png16_path": "",
|
||||||
|
"preview_pgm_path": "Data/Terrain/Unreal/gz_us_ca_pacifica_utm10n_e544_n4160/gz_us_ca_pacifica_utm10n_e544_n4160_unreal_1009_preview.pgm",
|
||||||
|
"min_elevation_m": 3.176246404647827,
|
||||||
|
"max_elevation_m": 96.50486755371094,
|
||||||
|
"vertical_range_m": 93.32862114906311,
|
||||||
|
"encoding": "unreal_landscape_midpoint_32768_sea_level"
|
||||||
|
},
|
||||||
|
"unreal_landscape_import": {
|
||||||
|
"landscape_resolution": "1009 x 1009",
|
||||||
|
"tile_world_size_m": 1000.0,
|
||||||
|
"x_scale_cm": 99.2063492063492,
|
||||||
|
"y_scale_cm": 99.2063492063492,
|
||||||
|
"z_scale_cm": 100.0,
|
||||||
|
"z_offset_m": 0.0,
|
||||||
|
"notes": [
|
||||||
|
"Use the R16 file for import.",
|
||||||
|
"Set X/Y scale to x_scale_cm/y_scale_cm so the 1009 samples span 1000 real meters.",
|
||||||
|
"Set Z scale to z_scale_cm.",
|
||||||
|
"Height values are encoded so Unreal landscape zero height corresponds to approximately sea level.",
|
||||||
|
"1009 x 1009 is a valid Unreal Landscape import size close to the 1000m source tile."
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,284 @@
|
|||||||
|
{
|
||||||
|
"schema_version": 1,
|
||||||
|
"grid_scheme": "prototype_utm_1km",
|
||||||
|
"tiles": [
|
||||||
|
{
|
||||||
|
"tile_id": "gz_us_ca_pacifica_utm10n_e544_n4160",
|
||||||
|
"display_name": "Ground Zero - Pacifica Linda Mar / San Pedro Valley",
|
||||||
|
"grid": {
|
||||||
|
"projection": "WGS84 / UTM zone 10N",
|
||||||
|
"utm_zone": "10N",
|
||||||
|
"easting_min_m": 544000,
|
||||||
|
"northing_min_m": 4160000,
|
||||||
|
"easting_max_m": 545000,
|
||||||
|
"northing_max_m": 4161000,
|
||||||
|
"tile_size_m": 1000,
|
||||||
|
"center_latitude": 37.5925,
|
||||||
|
"center_longitude": -122.4995
|
||||||
|
},
|
||||||
|
"status": "source_data_found",
|
||||||
|
"biome_primary": "coastal_california_scrub_woodland",
|
||||||
|
"biome_secondary": [
|
||||||
|
"valley_slope",
|
||||||
|
"coastal_influence"
|
||||||
|
],
|
||||||
|
"resource_hints": [
|
||||||
|
"wood",
|
||||||
|
"fiber",
|
||||||
|
"stone",
|
||||||
|
"fresh_water",
|
||||||
|
"coastal_wildlife"
|
||||||
|
],
|
||||||
|
"generation_version": 1,
|
||||||
|
"package_version": 0,
|
||||||
|
"neighbors": {
|
||||||
|
"n": "gz_us_ca_pacifica_utm10n_e544_n4161",
|
||||||
|
"ne": "gz_us_ca_pacifica_utm10n_e545_n4161",
|
||||||
|
"e": "gz_us_ca_pacifica_utm10n_e545_n4160",
|
||||||
|
"se": "gz_us_ca_pacifica_utm10n_e545_n4159",
|
||||||
|
"s": "gz_us_ca_pacifica_utm10n_e544_n4159",
|
||||||
|
"sw": "gz_us_ca_pacifica_utm10n_e543_n4159",
|
||||||
|
"w": "gz_us_ca_pacifica_utm10n_e543_n4160",
|
||||||
|
"nw": "gz_us_ca_pacifica_utm10n_e543_n4161"
|
||||||
|
},
|
||||||
|
"sources": [
|
||||||
|
{
|
||||||
|
"source_kind": "elevation",
|
||||||
|
"source_name": "USGS 1 Meter 10 x54y416 CA_CaliforniaGaps_B23",
|
||||||
|
"source_uri": "https://prd-tnm.s3.amazonaws.com/StagedProducts/Elevation/1m/Projects/CA_CaliforniaGaps_B23/TIFF/USGS_1M_10_x54y416_CA_CaliforniaGaps_B23.tif",
|
||||||
|
"license_name": "US public domain",
|
||||||
|
"source_version": "2025-08-20",
|
||||||
|
"coverage_status": "confirmed",
|
||||||
|
"local_metadata_path": "Data/Terrain/Sources/gz_us_ca_pacifica_utm10n_e544_n4160/gz_us_ca_pacifica_utm10n_e544_n4160_tnm_1m_dem_product.json",
|
||||||
|
"local_source_path": "Data/Terrain/Sources/gz_us_ca_pacifica_utm10n_e544_n4160/USGS_1M_10_x54y416_CA_CaliforniaGaps_B23.tif",
|
||||||
|
"local_source_folder": "Data/Terrain/Sources/gz_us_ca_pacifica_utm10n_e544_n4160"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source_kind": "elevation_prototype",
|
||||||
|
"source_name": "USGS Elevation Point Query Service",
|
||||||
|
"source_uri": "https://epqs.nationalmap.gov/v1/json",
|
||||||
|
"license_name": "US public domain",
|
||||||
|
"source_version": "prototype",
|
||||||
|
"coverage_status": "confirmed"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source_kind": "hydrography",
|
||||||
|
"source_name": "USGS National Hydrography Dataset",
|
||||||
|
"source_uri": "https://www.usgs.gov/national-hydrography",
|
||||||
|
"license_name": "US public domain",
|
||||||
|
"source_version": "candidate",
|
||||||
|
"coverage_status": "candidate"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source_kind": "bathymetry",
|
||||||
|
"source_name": "NOAA/NCEI coastal DEM or Coastal Relief Model",
|
||||||
|
"source_uri": "https://www.ncei.noaa.gov/products/coastal-relief-model",
|
||||||
|
"license_name": "US public domain",
|
||||||
|
"source_version": "mvp_target_if_coastal",
|
||||||
|
"coverage_status": "needed"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source_kind": "land_cover",
|
||||||
|
"source_name": "USGS National Land Cover Database",
|
||||||
|
"source_uri": "https://www.usgs.gov/centers/eros/science/national-land-cover-database",
|
||||||
|
"license_name": "US public domain",
|
||||||
|
"source_version": "candidate",
|
||||||
|
"coverage_status": "candidate"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"notes": "Final MVP 1-meter USGS DEM source acquired. Prototype heightmap remains generated separately until DEM extraction/import is run."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tile_id": "gz_us_ca_pacifica_utm10n_e543_n4159",
|
||||||
|
"display_name": "Ground Zero neighbor SW",
|
||||||
|
"grid": {
|
||||||
|
"projection": "WGS84 / UTM zone 10N",
|
||||||
|
"utm_zone": "10N",
|
||||||
|
"easting_min_m": 543000,
|
||||||
|
"northing_min_m": 4159000,
|
||||||
|
"easting_max_m": 544000,
|
||||||
|
"northing_max_m": 4160000,
|
||||||
|
"tile_size_m": 1000,
|
||||||
|
"center_latitude": 37.5835,
|
||||||
|
"center_longitude": -122.5108
|
||||||
|
},
|
||||||
|
"status": "unknown",
|
||||||
|
"biome_primary": "unknown",
|
||||||
|
"biome_secondary": [],
|
||||||
|
"resource_hints": [],
|
||||||
|
"generation_version": 0,
|
||||||
|
"package_version": 0,
|
||||||
|
"neighbors": {},
|
||||||
|
"sources": [],
|
||||||
|
"notes": "Placeholder neighbor for stitching and prefetch planning."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tile_id": "gz_us_ca_pacifica_utm10n_e544_n4159",
|
||||||
|
"display_name": "Ground Zero neighbor S",
|
||||||
|
"grid": {
|
||||||
|
"projection": "WGS84 / UTM zone 10N",
|
||||||
|
"utm_zone": "10N",
|
||||||
|
"easting_min_m": 544000,
|
||||||
|
"northing_min_m": 4159000,
|
||||||
|
"easting_max_m": 545000,
|
||||||
|
"northing_max_m": 4160000,
|
||||||
|
"tile_size_m": 1000,
|
||||||
|
"center_latitude": 37.5835,
|
||||||
|
"center_longitude": -122.4995
|
||||||
|
},
|
||||||
|
"status": "unknown",
|
||||||
|
"biome_primary": "unknown",
|
||||||
|
"biome_secondary": [],
|
||||||
|
"resource_hints": [],
|
||||||
|
"generation_version": 0,
|
||||||
|
"package_version": 0,
|
||||||
|
"neighbors": {},
|
||||||
|
"sources": [],
|
||||||
|
"notes": "Placeholder neighbor for stitching and prefetch planning."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tile_id": "gz_us_ca_pacifica_utm10n_e545_n4159",
|
||||||
|
"display_name": "Ground Zero neighbor SE",
|
||||||
|
"grid": {
|
||||||
|
"projection": "WGS84 / UTM zone 10N",
|
||||||
|
"utm_zone": "10N",
|
||||||
|
"easting_min_m": 545000,
|
||||||
|
"northing_min_m": 4159000,
|
||||||
|
"easting_max_m": 546000,
|
||||||
|
"northing_max_m": 4160000,
|
||||||
|
"tile_size_m": 1000,
|
||||||
|
"center_latitude": 37.5835,
|
||||||
|
"center_longitude": -122.4882
|
||||||
|
},
|
||||||
|
"status": "unknown",
|
||||||
|
"biome_primary": "unknown",
|
||||||
|
"biome_secondary": [],
|
||||||
|
"resource_hints": [],
|
||||||
|
"generation_version": 0,
|
||||||
|
"package_version": 0,
|
||||||
|
"neighbors": {},
|
||||||
|
"sources": [],
|
||||||
|
"notes": "Placeholder neighbor for stitching and prefetch planning."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tile_id": "gz_us_ca_pacifica_utm10n_e543_n4160",
|
||||||
|
"display_name": "Ground Zero neighbor W",
|
||||||
|
"grid": {
|
||||||
|
"projection": "WGS84 / UTM zone 10N",
|
||||||
|
"utm_zone": "10N",
|
||||||
|
"easting_min_m": 543000,
|
||||||
|
"northing_min_m": 4160000,
|
||||||
|
"easting_max_m": 544000,
|
||||||
|
"northing_max_m": 4161000,
|
||||||
|
"tile_size_m": 1000,
|
||||||
|
"center_latitude": 37.5925,
|
||||||
|
"center_longitude": -122.5108
|
||||||
|
},
|
||||||
|
"status": "unknown",
|
||||||
|
"biome_primary": "unknown",
|
||||||
|
"biome_secondary": [],
|
||||||
|
"resource_hints": [],
|
||||||
|
"generation_version": 0,
|
||||||
|
"package_version": 0,
|
||||||
|
"neighbors": {},
|
||||||
|
"sources": [],
|
||||||
|
"notes": "Placeholder neighbor for stitching and prefetch planning."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tile_id": "gz_us_ca_pacifica_utm10n_e545_n4160",
|
||||||
|
"display_name": "Ground Zero neighbor E",
|
||||||
|
"grid": {
|
||||||
|
"projection": "WGS84 / UTM zone 10N",
|
||||||
|
"utm_zone": "10N",
|
||||||
|
"easting_min_m": 545000,
|
||||||
|
"northing_min_m": 4160000,
|
||||||
|
"easting_max_m": 546000,
|
||||||
|
"northing_max_m": 4161000,
|
||||||
|
"tile_size_m": 1000,
|
||||||
|
"center_latitude": 37.5925,
|
||||||
|
"center_longitude": -122.4882
|
||||||
|
},
|
||||||
|
"status": "unknown",
|
||||||
|
"biome_primary": "unknown",
|
||||||
|
"biome_secondary": [],
|
||||||
|
"resource_hints": [],
|
||||||
|
"generation_version": 0,
|
||||||
|
"package_version": 0,
|
||||||
|
"neighbors": {},
|
||||||
|
"sources": [],
|
||||||
|
"notes": "Placeholder neighbor for stitching and prefetch planning."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tile_id": "gz_us_ca_pacifica_utm10n_e543_n4161",
|
||||||
|
"display_name": "Ground Zero neighbor NW",
|
||||||
|
"grid": {
|
||||||
|
"projection": "WGS84 / UTM zone 10N",
|
||||||
|
"utm_zone": "10N",
|
||||||
|
"easting_min_m": 543000,
|
||||||
|
"northing_min_m": 4161000,
|
||||||
|
"easting_max_m": 544000,
|
||||||
|
"northing_max_m": 4162000,
|
||||||
|
"tile_size_m": 1000,
|
||||||
|
"center_latitude": 37.6015,
|
||||||
|
"center_longitude": -122.5108
|
||||||
|
},
|
||||||
|
"status": "unknown",
|
||||||
|
"biome_primary": "unknown",
|
||||||
|
"biome_secondary": [],
|
||||||
|
"resource_hints": [],
|
||||||
|
"generation_version": 0,
|
||||||
|
"package_version": 0,
|
||||||
|
"neighbors": {},
|
||||||
|
"sources": [],
|
||||||
|
"notes": "Placeholder neighbor for stitching and prefetch planning."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tile_id": "gz_us_ca_pacifica_utm10n_e544_n4161",
|
||||||
|
"display_name": "Ground Zero neighbor N",
|
||||||
|
"grid": {
|
||||||
|
"projection": "WGS84 / UTM zone 10N",
|
||||||
|
"utm_zone": "10N",
|
||||||
|
"easting_min_m": 544000,
|
||||||
|
"northing_min_m": 4161000,
|
||||||
|
"easting_max_m": 545000,
|
||||||
|
"northing_max_m": 4162000,
|
||||||
|
"tile_size_m": 1000,
|
||||||
|
"center_latitude": 37.6015,
|
||||||
|
"center_longitude": -122.4995
|
||||||
|
},
|
||||||
|
"status": "unknown",
|
||||||
|
"biome_primary": "unknown",
|
||||||
|
"biome_secondary": [],
|
||||||
|
"resource_hints": [],
|
||||||
|
"generation_version": 0,
|
||||||
|
"package_version": 0,
|
||||||
|
"neighbors": {},
|
||||||
|
"sources": [],
|
||||||
|
"notes": "Placeholder neighbor for stitching and prefetch planning."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tile_id": "gz_us_ca_pacifica_utm10n_e545_n4161",
|
||||||
|
"display_name": "Ground Zero neighbor NE",
|
||||||
|
"grid": {
|
||||||
|
"projection": "WGS84 / UTM zone 10N",
|
||||||
|
"utm_zone": "10N",
|
||||||
|
"easting_min_m": 545000,
|
||||||
|
"northing_min_m": 4161000,
|
||||||
|
"easting_max_m": 546000,
|
||||||
|
"northing_max_m": 4162000,
|
||||||
|
"tile_size_m": 1000,
|
||||||
|
"center_latitude": 37.6015,
|
||||||
|
"center_longitude": -122.4882
|
||||||
|
},
|
||||||
|
"status": "unknown",
|
||||||
|
"biome_primary": "unknown",
|
||||||
|
"biome_secondary": [],
|
||||||
|
"resource_hints": [],
|
||||||
|
"generation_version": 0,
|
||||||
|
"package_version": 0,
|
||||||
|
"neighbors": {},
|
||||||
|
"sources": [],
|
||||||
|
"notes": "Placeholder neighbor for stitching and prefetch planning."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,191 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "https://agrarian.local/schemas/tile_registry.schema.json",
|
||||||
|
"title": "Agrarian Terrain Tile Registry",
|
||||||
|
"type": "object",
|
||||||
|
"required": ["schema_version", "grid_scheme", "tiles"],
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"schema_version": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1
|
||||||
|
},
|
||||||
|
"grid_scheme": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"tiles": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/$defs/tile"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"$defs": {
|
||||||
|
"tile_status": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"unknown",
|
||||||
|
"queued",
|
||||||
|
"source_data_found",
|
||||||
|
"generated",
|
||||||
|
"validated",
|
||||||
|
"packaged",
|
||||||
|
"published",
|
||||||
|
"deprecated",
|
||||||
|
"blocked"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"tile": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"tile_id",
|
||||||
|
"display_name",
|
||||||
|
"grid",
|
||||||
|
"status",
|
||||||
|
"biome_primary",
|
||||||
|
"generation_version",
|
||||||
|
"package_version",
|
||||||
|
"neighbors",
|
||||||
|
"sources"
|
||||||
|
],
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"tile_id": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[a-z0-9_]+$"
|
||||||
|
},
|
||||||
|
"display_name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"grid": {
|
||||||
|
"$ref": "#/$defs/grid"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"$ref": "#/$defs/tile_status"
|
||||||
|
},
|
||||||
|
"biome_primary": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"biome_secondary": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"resource_hints": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"generation_version": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 0
|
||||||
|
},
|
||||||
|
"package_version": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 0
|
||||||
|
},
|
||||||
|
"neighbors": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"n": { "type": "string" },
|
||||||
|
"ne": { "type": "string" },
|
||||||
|
"e": { "type": "string" },
|
||||||
|
"se": { "type": "string" },
|
||||||
|
"s": { "type": "string" },
|
||||||
|
"sw": { "type": "string" },
|
||||||
|
"w": { "type": "string" },
|
||||||
|
"nw": { "type": "string" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sources": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/$defs/source"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notes": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"grid": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"projection",
|
||||||
|
"utm_zone",
|
||||||
|
"easting_min_m",
|
||||||
|
"northing_min_m",
|
||||||
|
"easting_max_m",
|
||||||
|
"northing_max_m",
|
||||||
|
"tile_size_m",
|
||||||
|
"center_latitude",
|
||||||
|
"center_longitude"
|
||||||
|
],
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"projection": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"utm_zone": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"easting_min_m": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"northing_min_m": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"easting_max_m": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"northing_max_m": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"tile_size_m": {
|
||||||
|
"type": "integer",
|
||||||
|
"const": 1000
|
||||||
|
},
|
||||||
|
"center_latitude": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": -90,
|
||||||
|
"maximum": 90
|
||||||
|
},
|
||||||
|
"center_longitude": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": -180,
|
||||||
|
"maximum": 180
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"source": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["source_kind", "source_name", "coverage_status"],
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"source_kind": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"source_name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"source_uri": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"license_name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"source_version": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"coverage_status": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["needed", "candidate", "confirmed", "missing", "not_applicable"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
-- Agrarian terrain tile registry prototype.
|
||||||
|
-- This schema is intentionally separate from player/world persistence so
|
||||||
|
-- terrain packages can be regenerated without overwriting player history.
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS terrain_tiles (
|
||||||
|
tile_id TEXT PRIMARY KEY,
|
||||||
|
display_name TEXT NOT NULL,
|
||||||
|
grid_scheme TEXT NOT NULL,
|
||||||
|
projection TEXT NOT NULL,
|
||||||
|
utm_zone TEXT,
|
||||||
|
easting_min_m INTEGER NOT NULL,
|
||||||
|
northing_min_m INTEGER NOT NULL,
|
||||||
|
easting_max_m INTEGER NOT NULL,
|
||||||
|
northing_max_m INTEGER NOT NULL,
|
||||||
|
tile_size_m INTEGER NOT NULL CHECK (tile_size_m = 1000),
|
||||||
|
center_latitude REAL NOT NULL CHECK (center_latitude >= -90 AND center_latitude <= 90),
|
||||||
|
center_longitude REAL NOT NULL CHECK (center_longitude >= -180 AND center_longitude <= 180),
|
||||||
|
status TEXT NOT NULL CHECK (status IN (
|
||||||
|
'unknown',
|
||||||
|
'queued',
|
||||||
|
'source_data_found',
|
||||||
|
'generated',
|
||||||
|
'validated',
|
||||||
|
'packaged',
|
||||||
|
'published',
|
||||||
|
'deprecated',
|
||||||
|
'blocked'
|
||||||
|
)),
|
||||||
|
biome_primary TEXT NOT NULL DEFAULT 'unknown',
|
||||||
|
generation_version INTEGER NOT NULL DEFAULT 0,
|
||||||
|
package_version INTEGER NOT NULL DEFAULT 0,
|
||||||
|
notes TEXT NOT NULL DEFAULT '',
|
||||||
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_terrain_tiles_grid
|
||||||
|
ON terrain_tiles (grid_scheme, projection, utm_zone, easting_min_m, northing_min_m);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_terrain_tiles_status
|
||||||
|
ON terrain_tiles (status);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS terrain_tile_neighbors (
|
||||||
|
tile_id TEXT NOT NULL,
|
||||||
|
direction TEXT NOT NULL CHECK (direction IN ('n', 'ne', 'e', 'se', 's', 'sw', 'w', 'nw')),
|
||||||
|
neighbor_tile_id TEXT NOT NULL,
|
||||||
|
PRIMARY KEY (tile_id, direction),
|
||||||
|
FOREIGN KEY (tile_id) REFERENCES terrain_tiles(tile_id),
|
||||||
|
FOREIGN KEY (neighbor_tile_id) REFERENCES terrain_tiles(tile_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS terrain_tile_sources (
|
||||||
|
tile_source_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
tile_id TEXT NOT NULL,
|
||||||
|
source_kind TEXT NOT NULL,
|
||||||
|
source_name TEXT NOT NULL,
|
||||||
|
source_uri TEXT NOT NULL DEFAULT '',
|
||||||
|
license_name TEXT NOT NULL DEFAULT '',
|
||||||
|
source_version TEXT NOT NULL DEFAULT '',
|
||||||
|
coverage_status TEXT NOT NULL CHECK (coverage_status IN (
|
||||||
|
'needed',
|
||||||
|
'candidate',
|
||||||
|
'confirmed',
|
||||||
|
'missing',
|
||||||
|
'not_applicable'
|
||||||
|
)),
|
||||||
|
FOREIGN KEY (tile_id) REFERENCES terrain_tiles(tile_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_terrain_tile_sources_tile
|
||||||
|
ON terrain_tile_sources (tile_id, source_kind);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS terrain_tile_packages (
|
||||||
|
package_id TEXT PRIMARY KEY,
|
||||||
|
tile_id TEXT NOT NULL,
|
||||||
|
package_version INTEGER NOT NULL,
|
||||||
|
unreal_engine_version TEXT NOT NULL,
|
||||||
|
world_partition_ready INTEGER NOT NULL DEFAULT 0 CHECK (world_partition_ready IN (0, 1)),
|
||||||
|
package_uri TEXT NOT NULL DEFAULT '',
|
||||||
|
content_hash TEXT NOT NULL DEFAULT '',
|
||||||
|
package_size_bytes INTEGER NOT NULL DEFAULT 0,
|
||||||
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
published_at TEXT,
|
||||||
|
FOREIGN KEY (tile_id) REFERENCES terrain_tiles(tile_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_terrain_tile_packages_tile
|
||||||
|
ON terrain_tile_packages (tile_id, package_version);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS terrain_tile_generation_jobs (
|
||||||
|
job_id TEXT PRIMARY KEY,
|
||||||
|
tile_id TEXT NOT NULL,
|
||||||
|
job_type TEXT NOT NULL,
|
||||||
|
status TEXT NOT NULL CHECK (status IN (
|
||||||
|
'queued',
|
||||||
|
'running',
|
||||||
|
'succeeded',
|
||||||
|
'failed',
|
||||||
|
'cancelled'
|
||||||
|
)),
|
||||||
|
requested_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
started_at TEXT,
|
||||||
|
finished_at TEXT,
|
||||||
|
log_uri TEXT NOT NULL DEFAULT '',
|
||||||
|
error_summary TEXT NOT NULL DEFAULT '',
|
||||||
|
FOREIGN KEY (tile_id) REFERENCES terrain_tiles(tile_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_terrain_tile_generation_jobs_tile
|
||||||
|
ON terrain_tile_generation_jobs (tile_id, status);
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
# Ground Zero DEM Acquisition
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
This document records the official USGS 3DEP DEM product selected for the Ground
|
||||||
|
Zero MVP tile.
|
||||||
|
|
||||||
|
## Acquisition Script
|
||||||
|
|
||||||
|
```text
|
||||||
|
Scripts/acquire_ground_zero_dem.py
|
||||||
|
```
|
||||||
|
|
||||||
|
The script:
|
||||||
|
|
||||||
|
- Reads the Ground Zero tile bounds from `Data/Tiles/ground_zero_tiles.json`.
|
||||||
|
- Converts the 1 km UTM tile bounds to latitude/longitude.
|
||||||
|
- Queries the USGS TNMAccess API for `Digital Elevation Model (DEM) 1 meter`
|
||||||
|
products in GeoTIFF format.
|
||||||
|
- Stores the full TNM query response and selected product record.
|
||||||
|
- Downloads the selected GeoTIFF source.
|
||||||
|
- Updates the tile registry source record.
|
||||||
|
|
||||||
|
## Selected Product
|
||||||
|
|
||||||
|
```text
|
||||||
|
title: USGS 1 Meter 10 x54y416 CA_CaliforniaGaps_B23
|
||||||
|
dataset: Digital Elevation Model (DEM) 1 meter
|
||||||
|
format: GeoTIFF
|
||||||
|
publication_date: 2025-08-20
|
||||||
|
source: USGS TNMAccess / ScienceBase / 3DEP
|
||||||
|
```
|
||||||
|
|
||||||
|
The product is a 10 km x 10 km 1-meter DEM tile that covers the 1 km Ground Zero
|
||||||
|
MVP tile.
|
||||||
|
|
||||||
|
## Local Files
|
||||||
|
|
||||||
|
```text
|
||||||
|
Data/Terrain/Sources/gz_us_ca_pacifica_utm10n_e544_n4160/
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected files:
|
||||||
|
|
||||||
|
- `gz_us_ca_pacifica_utm10n_e544_n4160_tnm_1m_dem_product.json`
|
||||||
|
- `USGS_1M_10_x54y416_CA_CaliforniaGaps_B23.tif`
|
||||||
|
- `USGS_1M_10_x54y417_CA_CaliforniaGaps_B23.tif`
|
||||||
|
|
||||||
|
Ground Zero sits on a 3DEP 10 km tile boundary, so both source GeoTIFFs are
|
||||||
|
needed to extract the full 1 km MVP tile.
|
||||||
|
|
||||||
|
## Subset Extraction
|
||||||
|
|
||||||
|
```text
|
||||||
|
Scripts/extract_ground_zero_dem_subset.py
|
||||||
|
```
|
||||||
|
|
||||||
|
This script extracts the exact 1 km Ground Zero subset from the source GeoTIFFs.
|
||||||
|
It requires `rasterio` or equivalent GDAL Python support.
|
||||||
|
|
||||||
|
The extraction output is:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Data/Terrain/Extracted/gz_us_ca_pacifica_utm10n_e544_n4160/
|
||||||
|
```
|
||||||
|
|
||||||
|
Generated files:
|
||||||
|
|
||||||
|
- `gz_us_ca_pacifica_utm10n_e544_n4160_1m_dem_subset.tif`
|
||||||
|
- `gz_us_ca_pacifica_utm10n_e544_n4160_1m_dem_subset_metadata.json`
|
||||||
|
|
||||||
|
Current extracted subset:
|
||||||
|
|
||||||
|
```text
|
||||||
|
size: 1000 x 1000 pixels
|
||||||
|
pixel_size: 1 m x 1 m
|
||||||
|
crs: EPSG:26910 / NAD83 UTM zone 10N
|
||||||
|
bounds: E 544000-545000, N 4160000-4161000
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- The USGS product is the final MVP target elevation source.
|
||||||
|
- The older USGS EPQS 33 x 33 heightmap remains useful as a fast prototype and
|
||||||
|
sanity check, but the 1-meter GeoTIFF is the source we should use for the real
|
||||||
|
Unreal terrain tile.
|
||||||
|
- The selected products are larger than the 1 km MVP tile; this is expected for
|
||||||
|
3DEP 1-meter DEM products.
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
# Ground Zero MVP Tile
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Ground Zero for the first real-terrain MVP tile is the Linda Mar / San Pedro
|
||||||
|
Valley edge in Pacifica, California.
|
||||||
|
|
||||||
|
This is a practical first tile because it gives the terrain pipeline varied
|
||||||
|
coastal California terrain in a compact 1 km square:
|
||||||
|
|
||||||
|
- Pacific coastal influence without starting in a dense city center.
|
||||||
|
- Hills, valley slope, drainage, and vegetation transitions.
|
||||||
|
- Nearby ocean/coastline/bathymetry cases for early pipeline design.
|
||||||
|
- Real public terrain, land-cover, hydrography, and coastline datasets are
|
||||||
|
available from US sources.
|
||||||
|
- The environment supports the first survival loop: wood, fiber, water, shelter,
|
||||||
|
weather exposure, and wildlife placeholders.
|
||||||
|
|
||||||
|
## Tile Identity
|
||||||
|
|
||||||
|
The MVP tile uses a simple UTM 1 km grid ID for the first prototype. This is not
|
||||||
|
the final global indexing decision; it is the concrete local key we can build
|
||||||
|
against now.
|
||||||
|
|
||||||
|
```text
|
||||||
|
tile_id: gz_us_ca_pacifica_utm10n_e544_n4160
|
||||||
|
display_name: Ground Zero - Pacifica Linda Mar / San Pedro Valley
|
||||||
|
country: United States
|
||||||
|
region: California
|
||||||
|
nearest_place: Pacifica
|
||||||
|
utm_zone: 10N
|
||||||
|
utm_easting_min_m: 544000
|
||||||
|
utm_northing_min_m: 4160000
|
||||||
|
utm_easting_max_m: 545000
|
||||||
|
utm_northing_max_m: 4161000
|
||||||
|
nominal_center_latitude: 37.5925
|
||||||
|
nominal_center_longitude: -122.4995
|
||||||
|
tile_size_meters: 1000
|
||||||
|
```
|
||||||
|
|
||||||
|
## MVP Scope
|
||||||
|
|
||||||
|
For the first pass, this tile should prove the pipeline rather than final art
|
||||||
|
quality.
|
||||||
|
|
||||||
|
- Import real elevation data into a 1 km x 1 km Unreal terrain.
|
||||||
|
- Preserve real-world horizontal scale.
|
||||||
|
- Use a conservative vertical scale that can represent hills accurately without
|
||||||
|
making traversal unusable.
|
||||||
|
- Generate a tile metadata record and store it in the registry.
|
||||||
|
- Track source datasets and generation version.
|
||||||
|
- Add at least the eight neighboring tile records as placeholders.
|
||||||
|
- Validate that edges can be stitched to adjacent 1 km tiles.
|
||||||
|
- Infer first-pass biome and resources from terrain, land cover, water, and
|
||||||
|
coastal influence.
|
||||||
|
|
||||||
|
## Initial Biome Direction
|
||||||
|
|
||||||
|
Working biome label:
|
||||||
|
|
||||||
|
```text
|
||||||
|
coastal_california_scrub_woodland
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected early resources:
|
||||||
|
|
||||||
|
- Wood from scrub/woodland patches.
|
||||||
|
- Fiber from grasses, reeds, and coastal scrub.
|
||||||
|
- Water from drainage/creek source where present in source data.
|
||||||
|
- Stone from slope and exposed terrain placeholders.
|
||||||
|
- Wildlife placeholders appropriate to coastal California foothills.
|
||||||
|
|
||||||
|
## Source Data Targets
|
||||||
|
|
||||||
|
First-pass source candidates:
|
||||||
|
|
||||||
|
- USGS 3DEP elevation data for terrain height.
|
||||||
|
- USGS National Hydrography Dataset or equivalent for streams/water.
|
||||||
|
- NOAA coastline and bathymetry datasets for coastal/ocean handling.
|
||||||
|
- USGS National Land Cover Database or equivalent for land-cover/biome hints.
|
||||||
|
- OpenStreetMap only as a secondary reference for roads/trails/landmarks.
|
||||||
|
|
||||||
|
## Open Decisions
|
||||||
|
|
||||||
|
- Whether the final global tile grid stays UTM-based by zone or moves to a
|
||||||
|
single global equal-area grid.
|
||||||
|
- Exact Unreal origin strategy for tiles outside the MVP area.
|
||||||
|
- How much coastline/ocean data belongs in the same tile package versus a
|
||||||
|
separate water/bathymetry layer.
|
||||||
|
- Whether player-built state is stored per terrain tile, per settlement region,
|
||||||
|
or in a separate spatial entity table.
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
# MVP Terrain Accuracy And Source Requirements
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
This document defines the terrain accuracy bar for the Ground Zero MVP tile and
|
||||||
|
the source data requirements for moving from the current point-sampled prototype
|
||||||
|
to a real Unreal terrain import pipeline.
|
||||||
|
|
||||||
|
The goal is practical accuracy, not survey-grade simulation. Terrain should be
|
||||||
|
recognizably based on the real place, preserve the correct horizontal scale, and
|
||||||
|
support believable traversal, water, biome, and resource placement.
|
||||||
|
|
||||||
|
## MVP Tile
|
||||||
|
|
||||||
|
```text
|
||||||
|
tile_id: gz_us_ca_pacifica_utm10n_e544_n4160
|
||||||
|
location: Linda Mar / San Pedro Valley edge, Pacifica, California
|
||||||
|
tile_size: 1 km x 1 km
|
||||||
|
prototype_grid: WGS84 / UTM zone 10N
|
||||||
|
```
|
||||||
|
|
||||||
|
## Accuracy Tiers
|
||||||
|
|
||||||
|
### Tier 0 - Prototype Complete
|
||||||
|
|
||||||
|
This is the current state.
|
||||||
|
|
||||||
|
- Point-sampled USGS elevation data is available for the tile.
|
||||||
|
- A normalized `.r16` heightmap exists.
|
||||||
|
- Source and generation metadata are recorded.
|
||||||
|
- Tile registry status can move to `generated`.
|
||||||
|
|
||||||
|
Tier 0 is enough to prove the automation path, but not enough for final MVP
|
||||||
|
terrain art or gameplay.
|
||||||
|
|
||||||
|
### Tier 1 - MVP Required
|
||||||
|
|
||||||
|
This is the minimum bar for the first playable real-terrain MVP tile.
|
||||||
|
|
||||||
|
- Horizontal scale must remain 1 Unreal kilometer per real 1 kilometer.
|
||||||
|
- Ground elevation source should be 1 meter where available.
|
||||||
|
- If 1 meter source is unavailable, 1/3 arc-second or better can be used
|
||||||
|
temporarily, but the tile must be flagged as lower-confidence.
|
||||||
|
- Terrain vertical values must preserve real elevation range within a documented
|
||||||
|
tolerance after import.
|
||||||
|
- The generated Unreal landscape must include metadata for source dataset,
|
||||||
|
source date/version, vertical datum, horizontal datum/projection, min/max
|
||||||
|
elevation, and generation version.
|
||||||
|
- Terrain should not have visible seams at tile edges when adjacent tiles use
|
||||||
|
the same source and generation version.
|
||||||
|
- Slopes should be smoothed only enough for playable traversal; smoothing must
|
||||||
|
not erase major hills, valleys, drainage cuts, or coastal features.
|
||||||
|
- Water features present in source data should be represented at least as
|
||||||
|
gameplay placeholders.
|
||||||
|
- Biome/resource placement should use source-derived hints, not purely manual
|
||||||
|
placement.
|
||||||
|
|
||||||
|
### Tier 2 - Preferred After MVP
|
||||||
|
|
||||||
|
- Use lidar-derived source DEM or point-cloud processing where available.
|
||||||
|
- Preserve drainage, road/trail cuts, ridgelines, and terrain breaks with higher
|
||||||
|
fidelity.
|
||||||
|
- Build separate masks for slope, wetness, vegetation class, water, coastline,
|
||||||
|
and traversal difficulty.
|
||||||
|
- Add automated QA maps for slope extremes, seam mismatch, missing samples, and
|
||||||
|
source confidence.
|
||||||
|
- Generate World Partition-ready landscape tiles directly from the pipeline.
|
||||||
|
|
||||||
|
## Final Ground Elevation Source Requirements
|
||||||
|
|
||||||
|
Primary source:
|
||||||
|
|
||||||
|
- USGS 3DEP 1-meter DEM or USGS Seamless 1-Meter DEM where available.
|
||||||
|
|
||||||
|
Preferred fallback order:
|
||||||
|
|
||||||
|
1. USGS 3DEP 1-meter DEM / Seamless 1-Meter DEM.
|
||||||
|
2. USGS 3DEP source DEM or lidar-derived DEM with better local coverage.
|
||||||
|
3. USGS 3DEP 1/3 arc-second DEM.
|
||||||
|
4. USGS Elevation Point Query Service only for quick validation and prototypes.
|
||||||
|
|
||||||
|
The point-query service should not be the final production importer because it
|
||||||
|
is inefficient for dense raster generation and does not provide the full source
|
||||||
|
surface metadata we need for repeatable tiles.
|
||||||
|
|
||||||
|
## Coastal And Bathymetry Requirements
|
||||||
|
|
||||||
|
The Ground Zero region is coastal, so the MVP terrain requirements must account
|
||||||
|
for ocean/coastal data even if the first Ground Zero tile is mostly land.
|
||||||
|
|
||||||
|
Minimum coastal requirements:
|
||||||
|
|
||||||
|
- Determine whether each tile contains ocean, shoreline, beach, creek mouth, or
|
||||||
|
wetland features.
|
||||||
|
- If a tile includes ocean or nearshore water, use NOAA/NCEI coastal DEM,
|
||||||
|
bathymetry, or Coastal Relief Model data where appropriate.
|
||||||
|
- Track vertical datum and unit differences between land and bathymetric sources.
|
||||||
|
- Do not use bathymetric products for navigation.
|
||||||
|
- Represent ocean depth as gameplay terrain/water depth data, not as a real
|
||||||
|
nautical chart.
|
||||||
|
|
||||||
|
Preferred source order:
|
||||||
|
|
||||||
|
1. NOAA/NCEI coastal DEM or CUDEM where available for the specific coast.
|
||||||
|
2. NOAA/NCEI Coastal Relief Model for broader topographic/bathymetric context.
|
||||||
|
3. NOAA/NCEI bathymetry data viewer products for source discovery.
|
||||||
|
4. Coarser global relief only as placeholder context, not final MVP tile data.
|
||||||
|
|
||||||
|
## Import Requirements For Unreal
|
||||||
|
|
||||||
|
The terrain pipeline should produce:
|
||||||
|
|
||||||
|
- Raw heightmap in Unreal-compatible format.
|
||||||
|
- Metadata JSON.
|
||||||
|
- Tile registry update.
|
||||||
|
- Optional preview CSV or GeoJSON for debugging.
|
||||||
|
- Source confidence report.
|
||||||
|
|
||||||
|
Required metadata:
|
||||||
|
|
||||||
|
- `tile_id`
|
||||||
|
- source dataset name
|
||||||
|
- source URI or stable identifier
|
||||||
|
- source license/usage note
|
||||||
|
- source acquisition/publication date if available
|
||||||
|
- source resolution
|
||||||
|
- horizontal datum/projection
|
||||||
|
- vertical datum
|
||||||
|
- min elevation
|
||||||
|
- max elevation
|
||||||
|
- normalized heightmap format
|
||||||
|
- Unreal landscape scale assumptions
|
||||||
|
- generation version
|
||||||
|
- pipeline version
|
||||||
|
|
||||||
|
## Acceptance Tests
|
||||||
|
|
||||||
|
Before a terrain tile can be considered MVP-ready:
|
||||||
|
|
||||||
|
- [ ] Source DEM/raster covers the full 1 km tile.
|
||||||
|
- [ ] Generated tile is exactly 1000 m x 1000 m in projected coordinates.
|
||||||
|
- [ ] Heightmap resolution and Unreal landscape scale are documented.
|
||||||
|
- [ ] Min/max elevation in metadata matches generated heightmap scaling.
|
||||||
|
- [ ] Tile can be regenerated and produces the same content hash from the same
|
||||||
|
source inputs.
|
||||||
|
- [ ] Tile registry status moves through `source_data_found`, `generated`, and
|
||||||
|
`validated` intentionally.
|
||||||
|
- [ ] Adjacent-edge sample rows/columns can be compared for seam validation.
|
||||||
|
- [ ] Water/coastline handling is documented for the tile.
|
||||||
|
- [ ] Biome/resource hints are recorded.
|
||||||
|
|
||||||
|
## Current Prototype Gap List
|
||||||
|
|
||||||
|
The current `33 x 33` USGS point-sampled heightmap is useful, but it must be
|
||||||
|
replaced or supplemented before MVP terrain lock.
|
||||||
|
|
||||||
|
Known gaps:
|
||||||
|
|
||||||
|
- It samples point elevations instead of downloading a complete DEM raster.
|
||||||
|
- It has coarse 31.25 m sample spacing.
|
||||||
|
- It does not yet preserve source vertical datum metadata.
|
||||||
|
- It does not include land-cover, hydrography, coastline, or bathymetry layers.
|
||||||
|
- It is not yet imported into an Unreal Landscape actor.
|
||||||
|
- It is not yet validated against neighboring tile seams.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
For the Ground Zero MVP tile, the target terrain source is USGS 3DEP 1-meter DEM
|
||||||
|
or the USGS Seamless 1-Meter DEM if it is available for the tile. NOAA/NCEI
|
||||||
|
coastal DEM or Coastal Relief Model data should be used for coastal and
|
||||||
|
bathymetric context where the MVP tile or its immediate neighbors intersect
|
||||||
|
shoreline/ocean features.
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
# Ground Zero Terrain Import Prototype
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
This prototype proves that the selected Ground Zero tile can be sampled from
|
||||||
|
real elevation data and converted into files Unreal can import or inspect.
|
||||||
|
|
||||||
|
It is not the final terrain pipeline. The final pipeline should use raster DEM
|
||||||
|
or lidar-derived terrain data at higher resolution. This first pass is useful
|
||||||
|
because it validates tile coordinates, source access, height normalization, and
|
||||||
|
metadata recording.
|
||||||
|
|
||||||
|
## Script
|
||||||
|
|
||||||
|
```text
|
||||||
|
Scripts/prototype_ground_zero_terrain.py
|
||||||
|
```
|
||||||
|
|
||||||
|
The script:
|
||||||
|
|
||||||
|
- Reads the Ground Zero tile from `Data/Tiles/ground_zero_tiles.json`.
|
||||||
|
- Converts UTM sample points to latitude/longitude.
|
||||||
|
- Queries the USGS Elevation Point Query Service in meters.
|
||||||
|
- Writes CSV elevation samples.
|
||||||
|
- Writes a little-endian unsigned `.r16` prototype heightmap.
|
||||||
|
- Writes terrain generation metadata.
|
||||||
|
- Marks the Ground Zero tile as `generated` in the tile registry seed.
|
||||||
|
|
||||||
|
## Default Output
|
||||||
|
|
||||||
|
```text
|
||||||
|
Data/Terrain/Generated/gz_us_ca_pacifica_utm10n_e544_n4160/
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected files:
|
||||||
|
|
||||||
|
- `gz_us_ca_pacifica_utm10n_e544_n4160_elevation_samples_33.csv`
|
||||||
|
- `gz_us_ca_pacifica_utm10n_e544_n4160_heightmap_33.r16`
|
||||||
|
- `gz_us_ca_pacifica_utm10n_e544_n4160_terrain_metadata.json`
|
||||||
|
|
||||||
|
## Run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 Scripts/prototype_ground_zero_terrain.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Optional:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 Scripts/prototype_ground_zero_terrain.py --grid-size 65 --workers 8
|
||||||
|
```
|
||||||
|
|
||||||
|
## Import Notes
|
||||||
|
|
||||||
|
- The `.r16` file is normalized from the sampled minimum elevation to sampled
|
||||||
|
maximum elevation.
|
||||||
|
- Use the generated metadata file to recover the real elevation range.
|
||||||
|
- Horizontal tile scale is 1000 m x 1000 m.
|
||||||
|
- The default 33 x 33 grid is intentionally small for a fast proof of concept.
|
||||||
|
- Final landscape import should use a valid Unreal Landscape resolution and a
|
||||||
|
higher-resolution DEM/lidar source.
|
||||||
|
|
||||||
|
## Current Source
|
||||||
|
|
||||||
|
Primary prototype source:
|
||||||
|
|
||||||
|
```text
|
||||||
|
USGS Elevation Point Query Service
|
||||||
|
https://epqs.nationalmap.gov/v1/json
|
||||||
|
```
|
||||||
|
|
||||||
|
The service is backed by USGS elevation data and returns point elevations in
|
||||||
|
meters for latitude/longitude coordinates.
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
# Tile Registry Schema
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
The tile registry is the authoritative operational index for terrain tiles. It
|
||||||
|
tracks what tiles exist, where they are, which source data generated them, what
|
||||||
|
package version is published, and whether the tile is ready for clients.
|
||||||
|
|
||||||
|
The first implementation only needs the Ground Zero tile and neighbors. The
|
||||||
|
schema is intentionally shaped so it can scale toward hundreds of millions of
|
||||||
|
possible 1 km tiles later.
|
||||||
|
|
||||||
|
## Prototype Grid
|
||||||
|
|
||||||
|
For the MVP, the tile key uses UTM zone 10N and the lower-left 1 km grid corner.
|
||||||
|
|
||||||
|
```text
|
||||||
|
gz_us_ca_pacifica_utm10n_e544_n4160
|
||||||
|
```
|
||||||
|
|
||||||
|
Fields encoded in the prototype ID:
|
||||||
|
|
||||||
|
- Location family: `gz_us_ca_pacifica`
|
||||||
|
- Projection: `utm10n`
|
||||||
|
- Easting kilometer: `e544`
|
||||||
|
- Northing kilometer: `n4160`
|
||||||
|
|
||||||
|
The final global grid is still a design decision. The MVP schema keeps explicit
|
||||||
|
projection and metric bounds so tiles can be migrated later if the global index
|
||||||
|
changes.
|
||||||
|
|
||||||
|
## Tile Status
|
||||||
|
|
||||||
|
Allowed status values:
|
||||||
|
|
||||||
|
- `unknown`: placeholder exists, no source work started.
|
||||||
|
- `queued`: selected for source lookup or generation.
|
||||||
|
- `source_data_found`: required source datasets are identified.
|
||||||
|
- `generated`: terrain package generated but not validated.
|
||||||
|
- `validated`: QA checks passed.
|
||||||
|
- `packaged`: client/server package created.
|
||||||
|
- `published`: package is available to clients.
|
||||||
|
- `deprecated`: superseded by a newer tile version.
|
||||||
|
- `blocked`: source or generation issue needs manual review.
|
||||||
|
|
||||||
|
## Core Tables
|
||||||
|
|
||||||
|
### `terrain_tiles`
|
||||||
|
|
||||||
|
Tracks one logical 1 km tile.
|
||||||
|
|
||||||
|
Required fields:
|
||||||
|
|
||||||
|
- `tile_id`
|
||||||
|
- `grid_scheme`
|
||||||
|
- `projection`
|
||||||
|
- `utm_zone`
|
||||||
|
- `easting_min_m`
|
||||||
|
- `northing_min_m`
|
||||||
|
- `easting_max_m`
|
||||||
|
- `northing_max_m`
|
||||||
|
- `tile_size_m`
|
||||||
|
- `center_latitude`
|
||||||
|
- `center_longitude`
|
||||||
|
- `status`
|
||||||
|
- `biome_primary`
|
||||||
|
- `generation_version`
|
||||||
|
- `package_version`
|
||||||
|
- `created_at`
|
||||||
|
- `updated_at`
|
||||||
|
|
||||||
|
### `terrain_tile_neighbors`
|
||||||
|
|
||||||
|
Tracks adjacency for stitching and prefetching.
|
||||||
|
|
||||||
|
Required fields:
|
||||||
|
|
||||||
|
- `tile_id`
|
||||||
|
- `direction`
|
||||||
|
- `neighbor_tile_id`
|
||||||
|
|
||||||
|
### `terrain_tile_sources`
|
||||||
|
|
||||||
|
Tracks datasets used or intended for each tile.
|
||||||
|
|
||||||
|
Required fields:
|
||||||
|
|
||||||
|
- `tile_id`
|
||||||
|
- `source_kind`
|
||||||
|
- `source_name`
|
||||||
|
- `source_uri`
|
||||||
|
- `license_name`
|
||||||
|
- `source_version`
|
||||||
|
- `coverage_status`
|
||||||
|
|
||||||
|
### `terrain_tile_packages`
|
||||||
|
|
||||||
|
Tracks generated downloadable packages.
|
||||||
|
|
||||||
|
Required fields:
|
||||||
|
|
||||||
|
- `package_id`
|
||||||
|
- `tile_id`
|
||||||
|
- `package_version`
|
||||||
|
- `unreal_engine_version`
|
||||||
|
- `world_partition_ready`
|
||||||
|
- `package_uri`
|
||||||
|
- `content_hash`
|
||||||
|
- `package_size_bytes`
|
||||||
|
- `created_at`
|
||||||
|
- `published_at`
|
||||||
|
|
||||||
|
### `terrain_tile_generation_jobs`
|
||||||
|
|
||||||
|
Tracks generation pipeline work.
|
||||||
|
|
||||||
|
Required fields:
|
||||||
|
|
||||||
|
- `job_id`
|
||||||
|
- `tile_id`
|
||||||
|
- `job_type`
|
||||||
|
- `status`
|
||||||
|
- `requested_at`
|
||||||
|
- `started_at`
|
||||||
|
- `finished_at`
|
||||||
|
- `log_uri`
|
||||||
|
- `error_summary`
|
||||||
|
|
||||||
|
## Separation Of Concerns
|
||||||
|
|
||||||
|
Terrain tile state should be separate from player-made world state.
|
||||||
|
|
||||||
|
Terrain registry owns:
|
||||||
|
|
||||||
|
- Source terrain and water data.
|
||||||
|
- Generated landscape package.
|
||||||
|
- Biome/resource hints.
|
||||||
|
- Tile status and package version.
|
||||||
|
- Client cache/version compatibility.
|
||||||
|
|
||||||
|
Player/world persistence owns:
|
||||||
|
|
||||||
|
- Player inventory, stats, and position.
|
||||||
|
- Placed structures.
|
||||||
|
- Resource depletion, if needed.
|
||||||
|
- Claims, settlements, containers, and ownership.
|
||||||
|
- Tile-local gameplay changes.
|
||||||
|
|
||||||
|
This separation lets us regenerate terrain tiles later without overwriting
|
||||||
|
player-built history.
|
||||||
|
|
||||||
|
## First Validation Rules
|
||||||
|
|
||||||
|
- Tile bounds must be exactly 1000 m x 1000 m in the projected coordinate system.
|
||||||
|
- Center latitude/longitude must fall inside tile bounds.
|
||||||
|
- Every published tile must have at least one elevation source.
|
||||||
|
- Every published tile must have a generation version and package version.
|
||||||
|
- Neighbor records must be reciprocal once adjacent tiles are generated.
|
||||||
|
- A tile package cannot be `published` until it is `validated`.
|
||||||
|
- Terrain package hash must change when package version changes.
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
# Ground Zero Unreal Landscape Import Plan
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
This document describes how to import the extracted Ground Zero 1-meter DEM
|
||||||
|
subset into Unreal as a Landscape heightmap.
|
||||||
|
|
||||||
|
## Source
|
||||||
|
|
||||||
|
```text
|
||||||
|
Data/Terrain/Extracted/gz_us_ca_pacifica_utm10n_e544_n4160/gz_us_ca_pacifica_utm10n_e544_n4160_1m_dem_subset.tif
|
||||||
|
```
|
||||||
|
|
||||||
|
Source properties:
|
||||||
|
|
||||||
|
- Size: 1000 x 1000 pixels.
|
||||||
|
- Pixel size: 1 m x 1 m.
|
||||||
|
- CRS: EPSG:26910 / NAD83 UTM zone 10N.
|
||||||
|
- Bounds: E 544000-545000, N 4160000-4161000.
|
||||||
|
- Elevation range: about 3.16 m to 96.51 m.
|
||||||
|
|
||||||
|
## Conversion Script
|
||||||
|
|
||||||
|
```text
|
||||||
|
Scripts/convert_ground_zero_dem_to_unreal_heightmap.py
|
||||||
|
```
|
||||||
|
|
||||||
|
The script:
|
||||||
|
|
||||||
|
- Reads the extracted 1-meter DEM subset.
|
||||||
|
- Resamples it to 1009 x 1009.
|
||||||
|
- Encodes elevation into unsigned 16-bit height values using Unreal's
|
||||||
|
landscape midpoint, so sea level is approximately Unreal Z 0.
|
||||||
|
- Writes a little-endian `.r16` heightmap.
|
||||||
|
- Writes a small grayscale preview file.
|
||||||
|
- Writes import metadata with Unreal scale values.
|
||||||
|
|
||||||
|
## Output
|
||||||
|
|
||||||
|
```text
|
||||||
|
Data/Terrain/Unreal/gz_us_ca_pacifica_utm10n_e544_n4160/
|
||||||
|
```
|
||||||
|
|
||||||
|
Files:
|
||||||
|
|
||||||
|
- `gz_us_ca_pacifica_utm10n_e544_n4160_unreal_1009.r16`
|
||||||
|
- `gz_us_ca_pacifica_utm10n_e544_n4160_unreal_1009_preview.pgm`
|
||||||
|
- `gz_us_ca_pacifica_utm10n_e544_n4160_unreal_heightmap_metadata.json`
|
||||||
|
|
||||||
|
If Pillow is installed, the script also writes:
|
||||||
|
|
||||||
|
- `gz_us_ca_pacifica_utm10n_e544_n4160_unreal_1009.png`
|
||||||
|
|
||||||
|
## Unreal Import Settings
|
||||||
|
|
||||||
|
Use the generated metadata file as the source of truth.
|
||||||
|
|
||||||
|
Current settings:
|
||||||
|
|
||||||
|
```text
|
||||||
|
heightmap resolution: 1009 x 1009
|
||||||
|
tile world size: 1000 m x 1000 m
|
||||||
|
X scale: 99.2063492063492 cm
|
||||||
|
Y scale: 99.2063492063492 cm
|
||||||
|
Z scale: 100.0 cm
|
||||||
|
Z offset: 0.0 m
|
||||||
|
height encoding: Unreal landscape midpoint 32768 = approximately sea level
|
||||||
|
```
|
||||||
|
|
||||||
|
Why 1009:
|
||||||
|
|
||||||
|
- Unreal Landscape import requires specific valid dimensions.
|
||||||
|
- 1009 x 1009 is a valid import size and close to the 1000 x 1000 source DEM.
|
||||||
|
- X/Y scale maps 1008 intervals across exactly 1000 real meters.
|
||||||
|
|
||||||
|
## Import Steps
|
||||||
|
|
||||||
|
1. Open the Unreal Editor.
|
||||||
|
2. Open or create the Ground Zero terrain test map.
|
||||||
|
3. Go to Landscape mode.
|
||||||
|
4. Choose Import from File.
|
||||||
|
5. Select `gz_us_ca_pacifica_utm10n_e544_n4160_unreal_1009.r16`.
|
||||||
|
6. Set the landscape resolution to 1009 x 1009 if Unreal does not auto-detect it.
|
||||||
|
7. Set X scale and Y scale from metadata.
|
||||||
|
8. Set Z scale from metadata.
|
||||||
|
9. Place the landscape so the tile origin maps to the project terrain origin.
|
||||||
|
10. Save the map under the project terrain test area.
|
||||||
|
|
||||||
|
The repeatable project import path is:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Scripts/setup_ground_zero_terrain_map.py
|
||||||
|
Scripts/verify_ground_zero_terrain_map.py
|
||||||
|
```
|
||||||
|
|
||||||
|
The generated terrain test map is:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/Game/Agrarian/Maps/L_GroundZeroTerrain_Test
|
||||||
|
```
|
||||||
|
|
||||||
|
## Validation After Import
|
||||||
|
|
||||||
|
- Landscape bounds should be 1000 m x 1000 m.
|
||||||
|
- Imported elevation range should visually match the source: low coastal/valley
|
||||||
|
terrain rising into hills.
|
||||||
|
- No extreme spikes or inverted terrain should appear.
|
||||||
|
- The north/south/east/west edges should remain suitable for seam comparison
|
||||||
|
against neighboring tiles.
|
||||||
|
- The landscape should be treated as prototype terrain until material, water,
|
||||||
|
biome, and resource layers are added.
|
||||||
@@ -21,7 +21,7 @@ if not exist "%BUILD_BAT%" (
|
|||||||
|
|
||||||
echo Building AgrarianGameEditor with UnrealBuildTool...
|
echo Building AgrarianGameEditor with UnrealBuildTool...
|
||||||
echo Log: %LOG_FILE%
|
echo Log: %LOG_FILE%
|
||||||
call "%BUILD_BAT%" AgrarianGameEditor Win64 Development -Project="%PROJECT_FILE%" -WaitMutex -architecture=x64 > "%LOG_FILE%" 2>&1
|
call "%BUILD_BAT%" AgrarianGameEditor Win64 Development -Project="%PROJECT_FILE%" -WaitMutex -architecture=x64 -NoUBA > "%LOG_FILE%" 2>&1
|
||||||
set "BUILD_EXIT_CODE=%ERRORLEVEL%"
|
set "BUILD_EXIT_CODE=%ERRORLEVEL%"
|
||||||
|
|
||||||
type "%LOG_FILE%"
|
type "%LOG_FILE%"
|
||||||
|
|||||||
@@ -0,0 +1,186 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Acquire the best available USGS 3DEP DEM source for Ground Zero.
|
||||||
|
|
||||||
|
This script queries the official USGS TNMAccess API for the selected 1 km tile,
|
||||||
|
stores the product metadata, downloads the chosen 1-meter GeoTIFF source, and
|
||||||
|
updates the tile registry source record.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
import urllib.parse
|
||||||
|
import urllib.request
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from prototype_ground_zero_terrain import TARGET_TILE_ID, utm_to_lat_lon
|
||||||
|
|
||||||
|
|
||||||
|
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
REGISTRY_PATH = PROJECT_ROOT / "Data" / "Tiles" / "ground_zero_tiles.json"
|
||||||
|
SOURCE_ROOT = PROJECT_ROOT / "Data" / "Terrain" / "Sources" / TARGET_TILE_ID
|
||||||
|
TNM_PRODUCTS_URL = "https://tnmaccess.nationalmap.gov/api/v1/products"
|
||||||
|
TARGET_DATASET = "Digital Elevation Model (DEM) 1 meter"
|
||||||
|
|
||||||
|
|
||||||
|
def load_tile() -> dict:
|
||||||
|
registry = json.loads(REGISTRY_PATH.read_text())
|
||||||
|
for tile in registry["tiles"]:
|
||||||
|
if tile["tile_id"] == TARGET_TILE_ID:
|
||||||
|
return tile
|
||||||
|
raise RuntimeError(f"Could not find {TARGET_TILE_ID} in {REGISTRY_PATH}")
|
||||||
|
|
||||||
|
|
||||||
|
def tile_bbox_lon_lat(tile: dict) -> tuple[float, float, float, float]:
|
||||||
|
grid = tile["grid"]
|
||||||
|
corners = [
|
||||||
|
(grid["easting_min_m"], grid["northing_min_m"]),
|
||||||
|
(grid["easting_max_m"], grid["northing_min_m"]),
|
||||||
|
(grid["easting_max_m"], grid["northing_max_m"]),
|
||||||
|
(grid["easting_min_m"], grid["northing_max_m"]),
|
||||||
|
]
|
||||||
|
lat_lons = [utm_to_lat_lon(easting, northing, 10, True) for easting, northing in corners]
|
||||||
|
return (
|
||||||
|
min(lon for _, lon in lat_lons),
|
||||||
|
min(lat for lat, _ in lat_lons),
|
||||||
|
max(lon for _, lon in lat_lons),
|
||||||
|
max(lat for lat, _ in lat_lons),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def query_tnm_products(bbox: tuple[float, float, float, float]) -> dict:
|
||||||
|
params = urllib.parse.urlencode(
|
||||||
|
{
|
||||||
|
"bbox": ",".join(f"{value:.10f}" for value in bbox),
|
||||||
|
"datasets": TARGET_DATASET,
|
||||||
|
"prodFormats": "GeoTIFF",
|
||||||
|
"outputFormat": "JSON",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
url = f"{TNM_PRODUCTS_URL}?{params}"
|
||||||
|
with urllib.request.urlopen(url, timeout=60) as response:
|
||||||
|
payload = json.loads(response.read().decode("utf-8"))
|
||||||
|
payload["query_url"] = url
|
||||||
|
return payload
|
||||||
|
|
||||||
|
|
||||||
|
def choose_product(products: dict) -> dict:
|
||||||
|
items = products.get("items", [])
|
||||||
|
if not items:
|
||||||
|
raise RuntimeError("TNMAccess returned no 1-meter DEM products for Ground Zero")
|
||||||
|
|
||||||
|
def product_score(item: dict) -> tuple[int, str]:
|
||||||
|
title = item.get("title", "")
|
||||||
|
size = int(item.get("sizeInBytes") or 0)
|
||||||
|
is_geotiff = 1 if item.get("format") == "GeoTIFF" else 0
|
||||||
|
has_download = 1 if item.get("downloadURL") else 0
|
||||||
|
return (has_download + is_geotiff, f"{size:020d}_{title}")
|
||||||
|
|
||||||
|
return sorted(items, key=product_score, reverse=True)[0]
|
||||||
|
|
||||||
|
|
||||||
|
def coverage_products(products: dict) -> list[dict]:
|
||||||
|
items = products.get("items", [])
|
||||||
|
if not items:
|
||||||
|
raise RuntimeError("TNMAccess returned no 1-meter DEM products for Ground Zero")
|
||||||
|
return [item for item in items if item.get("downloadURL")]
|
||||||
|
|
||||||
|
|
||||||
|
def download_file(url: str, output_path: Path) -> None:
|
||||||
|
if output_path.exists() and output_path.stat().st_size > 0:
|
||||||
|
print(f"Using existing download: {output_path}")
|
||||||
|
return
|
||||||
|
|
||||||
|
temp_path = output_path.with_suffix(output_path.suffix + ".part")
|
||||||
|
with urllib.request.urlopen(url, timeout=120) as response, temp_path.open("wb") as output_file:
|
||||||
|
while True:
|
||||||
|
chunk = response.read(1024 * 1024)
|
||||||
|
if not chunk:
|
||||||
|
break
|
||||||
|
output_file.write(chunk)
|
||||||
|
temp_path.replace(output_path)
|
||||||
|
|
||||||
|
|
||||||
|
def update_registry_source(product: dict, metadata_path: Path, geotiff_path: Path) -> None:
|
||||||
|
registry = json.loads(REGISTRY_PATH.read_text())
|
||||||
|
for tile in registry["tiles"]:
|
||||||
|
if tile["tile_id"] != TARGET_TILE_ID:
|
||||||
|
continue
|
||||||
|
|
||||||
|
for source in tile["sources"]:
|
||||||
|
if source["source_kind"] == "elevation":
|
||||||
|
source["source_name"] = product.get("title", source["source_name"])
|
||||||
|
source["source_uri"] = product.get("downloadURL", source["source_uri"])
|
||||||
|
source["source_version"] = product.get("publicationDate", "downloaded")
|
||||||
|
source["coverage_status"] = "confirmed"
|
||||||
|
source["local_metadata_path"] = str(metadata_path.relative_to(PROJECT_ROOT))
|
||||||
|
source["local_source_path"] = str(geotiff_path.relative_to(PROJECT_ROOT))
|
||||||
|
source["local_source_folder"] = str(geotiff_path.parent.relative_to(PROJECT_ROOT))
|
||||||
|
|
||||||
|
tile["status"] = "source_data_found"
|
||||||
|
tile["notes"] = (
|
||||||
|
"Final MVP 1-meter USGS DEM source acquired. "
|
||||||
|
"Prototype heightmap remains generated separately until DEM extraction/import is run."
|
||||||
|
)
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
raise RuntimeError(f"Could not update missing tile {TARGET_TILE_ID}")
|
||||||
|
|
||||||
|
REGISTRY_PATH.write_text(json.dumps(registry, indent=2) + "\n")
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument("--metadata-only", action="store_true", help="Query and write metadata without downloading the GeoTIFF")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
tile = load_tile()
|
||||||
|
bbox = tile_bbox_lon_lat(tile)
|
||||||
|
SOURCE_ROOT.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
products = query_tnm_products(bbox)
|
||||||
|
products_to_download = coverage_products(products)
|
||||||
|
product = choose_product(products)
|
||||||
|
|
||||||
|
metadata = {
|
||||||
|
"tile_id": TARGET_TILE_ID,
|
||||||
|
"acquired_at_utc": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
||||||
|
"tnm_query": {
|
||||||
|
"url": products["query_url"],
|
||||||
|
"bbox_lon_lat": bbox,
|
||||||
|
"dataset": TARGET_DATASET,
|
||||||
|
"product_count": products.get("total"),
|
||||||
|
},
|
||||||
|
"selected_product": product,
|
||||||
|
"coverage_products": products_to_download,
|
||||||
|
"all_products": products.get("items", []),
|
||||||
|
}
|
||||||
|
metadata_path = SOURCE_ROOT / f"{TARGET_TILE_ID}_tnm_1m_dem_product.json"
|
||||||
|
metadata_path.write_text(json.dumps(metadata, indent=2) + "\n")
|
||||||
|
|
||||||
|
geotiff_paths = []
|
||||||
|
for coverage_product in products_to_download:
|
||||||
|
download_url = coverage_product.get("downloadURL")
|
||||||
|
filename = Path(urllib.parse.urlparse(download_url).path).name
|
||||||
|
geotiff_path = SOURCE_ROOT / filename
|
||||||
|
geotiff_paths.append(geotiff_path)
|
||||||
|
if not args.metadata_only:
|
||||||
|
download_file(download_url, geotiff_path)
|
||||||
|
|
||||||
|
update_registry_source(product, metadata_path, geotiff_paths[0])
|
||||||
|
|
||||||
|
print(f"Selected: {product.get('title')}")
|
||||||
|
print(f"Published: {product.get('publicationDate')}")
|
||||||
|
print(f"Size: {product.get('sizeInBytes')} bytes")
|
||||||
|
print(f"Metadata: {metadata_path}")
|
||||||
|
for geotiff_path in geotiff_paths:
|
||||||
|
if args.metadata_only:
|
||||||
|
print(f"GeoTIFF download skipped: {geotiff_path}")
|
||||||
|
else:
|
||||||
|
print(f"GeoTIFF: {geotiff_path}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Convert the extracted Ground Zero 1m DEM subset into Unreal heightmaps."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import struct
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import rasterio
|
||||||
|
from rasterio.enums import Resampling
|
||||||
|
from rasterio.warp import reproject
|
||||||
|
|
||||||
|
from prototype_ground_zero_terrain import TARGET_TILE_ID
|
||||||
|
|
||||||
|
|
||||||
|
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
EXTRACT_ROOT = PROJECT_ROOT / "Data" / "Terrain" / "Extracted" / TARGET_TILE_ID
|
||||||
|
UNREAL_ROOT = PROJECT_ROOT / "Data" / "Terrain" / "Unreal" / TARGET_TILE_ID
|
||||||
|
SOURCE_TIFF = EXTRACT_ROOT / f"{TARGET_TILE_ID}_1m_dem_subset.tif"
|
||||||
|
UNREAL_SIZE = 1009
|
||||||
|
TILE_SIZE_M = 1000.0
|
||||||
|
UNREAL_Z_SCALE_CM = 100.0
|
||||||
|
|
||||||
|
|
||||||
|
def encode_to_unreal_uint16(data: np.ma.MaskedArray) -> tuple[np.ndarray, float, float]:
|
||||||
|
min_elevation = float(data.min())
|
||||||
|
max_elevation = float(data.max())
|
||||||
|
# Unreal Landscape encodes zero height around 32768. At Z scale 100, each
|
||||||
|
# height unit is 100 / 128 cm and the full range is roughly +/- 256 m.
|
||||||
|
height_values = 32768.0 + (data.filled(0.0) * 100.0 * 128.0 / UNREAL_Z_SCALE_CM)
|
||||||
|
height_uint16 = np.rint(np.clip(height_values, 0.0, 65535.0)).astype("<u2")
|
||||||
|
return height_uint16, min_elevation, max_elevation
|
||||||
|
|
||||||
|
|
||||||
|
def resample_to_unreal_size(source: rasterio.io.DatasetReader) -> np.ma.MaskedArray:
|
||||||
|
source_data = source.read(1, masked=True)
|
||||||
|
destination = np.empty((UNREAL_SIZE, UNREAL_SIZE), dtype=np.float32)
|
||||||
|
|
||||||
|
dst_transform = rasterio.transform.from_bounds(
|
||||||
|
source.bounds.left,
|
||||||
|
source.bounds.bottom,
|
||||||
|
source.bounds.right,
|
||||||
|
source.bounds.top,
|
||||||
|
UNREAL_SIZE,
|
||||||
|
UNREAL_SIZE,
|
||||||
|
)
|
||||||
|
|
||||||
|
reproject(
|
||||||
|
source=source_data.filled(source.nodata),
|
||||||
|
destination=destination,
|
||||||
|
src_transform=source.transform,
|
||||||
|
src_crs=source.crs,
|
||||||
|
src_nodata=source.nodata,
|
||||||
|
dst_transform=dst_transform,
|
||||||
|
dst_crs=source.crs,
|
||||||
|
dst_nodata=source.nodata,
|
||||||
|
resampling=Resampling.bilinear,
|
||||||
|
)
|
||||||
|
|
||||||
|
return np.ma.masked_equal(destination, source.nodata)
|
||||||
|
|
||||||
|
|
||||||
|
def write_r16(height_data: np.ndarray, output_path: Path) -> None:
|
||||||
|
output_path.write_bytes(height_data.tobytes(order="C"))
|
||||||
|
|
||||||
|
|
||||||
|
def write_png16(height_data: np.ndarray, output_path: Path) -> bool:
|
||||||
|
try:
|
||||||
|
from PIL import Image
|
||||||
|
except ModuleNotFoundError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
image = Image.fromarray(height_data, mode="I;16")
|
||||||
|
image.save(output_path)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def write_preview_pgm(height_data: np.ndarray, output_path: Path) -> None:
|
||||||
|
preview = (height_data.astype(np.float32) / 65535.0 * 255.0).astype(np.uint8)
|
||||||
|
header = f"P5\n{UNREAL_SIZE} {UNREAL_SIZE}\n255\n".encode("ascii")
|
||||||
|
output_path.write_bytes(header + preview.tobytes(order="C"))
|
||||||
|
|
||||||
|
|
||||||
|
def write_import_metadata(
|
||||||
|
output_path: Path,
|
||||||
|
source: rasterio.io.DatasetReader,
|
||||||
|
min_elevation: float,
|
||||||
|
max_elevation: float,
|
||||||
|
r16_path: Path,
|
||||||
|
png_path: Path,
|
||||||
|
png_written: bool,
|
||||||
|
preview_path: Path,
|
||||||
|
) -> None:
|
||||||
|
vertical_range = max_elevation - min_elevation
|
||||||
|
metadata = {
|
||||||
|
"tile_id": TARGET_TILE_ID,
|
||||||
|
"generated_at_utc": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
||||||
|
"source_dem": str(SOURCE_TIFF.relative_to(PROJECT_ROOT)),
|
||||||
|
"source_crs": str(source.crs),
|
||||||
|
"source_bounds": list(source.bounds),
|
||||||
|
"source_size_pixels": [source.width, source.height],
|
||||||
|
"source_pixel_size_m": [abs(source.transform.a), abs(source.transform.e)],
|
||||||
|
"heightmap": {
|
||||||
|
"format": "r16_little_endian_unsigned",
|
||||||
|
"width": UNREAL_SIZE,
|
||||||
|
"height": UNREAL_SIZE,
|
||||||
|
"r16_path": str(r16_path.relative_to(PROJECT_ROOT)),
|
||||||
|
"png16_path": str(png_path.relative_to(PROJECT_ROOT)) if png_written else "",
|
||||||
|
"preview_pgm_path": str(preview_path.relative_to(PROJECT_ROOT)),
|
||||||
|
"min_elevation_m": min_elevation,
|
||||||
|
"max_elevation_m": max_elevation,
|
||||||
|
"vertical_range_m": vertical_range,
|
||||||
|
"encoding": "unreal_landscape_midpoint_32768_sea_level",
|
||||||
|
},
|
||||||
|
"unreal_landscape_import": {
|
||||||
|
"landscape_resolution": "1009 x 1009",
|
||||||
|
"tile_world_size_m": TILE_SIZE_M,
|
||||||
|
"x_scale_cm": 100.0 * TILE_SIZE_M / (UNREAL_SIZE - 1),
|
||||||
|
"y_scale_cm": 100.0 * TILE_SIZE_M / (UNREAL_SIZE - 1),
|
||||||
|
"z_scale_cm": UNREAL_Z_SCALE_CM,
|
||||||
|
"z_offset_m": 0.0,
|
||||||
|
"notes": [
|
||||||
|
"Use the R16 file for import.",
|
||||||
|
"Set X/Y scale to x_scale_cm/y_scale_cm so the 1009 samples span 1000 real meters.",
|
||||||
|
"Set Z scale to z_scale_cm.",
|
||||||
|
"Height values are encoded so Unreal landscape zero height corresponds to approximately sea level.",
|
||||||
|
"1009 x 1009 is a valid Unreal Landscape import size close to the 1000m source tile."
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
output_path.write_text(json.dumps(metadata, indent=2) + "\n")
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
UNREAL_ROOT.mkdir(parents=True, exist_ok=True)
|
||||||
|
r16_path = UNREAL_ROOT / f"{TARGET_TILE_ID}_unreal_1009.r16"
|
||||||
|
png_path = UNREAL_ROOT / f"{TARGET_TILE_ID}_unreal_1009.png"
|
||||||
|
preview_path = UNREAL_ROOT / f"{TARGET_TILE_ID}_unreal_1009_preview.pgm"
|
||||||
|
metadata_path = UNREAL_ROOT / f"{TARGET_TILE_ID}_unreal_heightmap_metadata.json"
|
||||||
|
|
||||||
|
with rasterio.open(SOURCE_TIFF) as source:
|
||||||
|
resampled = resample_to_unreal_size(source)
|
||||||
|
height_data, min_elevation, max_elevation = encode_to_unreal_uint16(resampled)
|
||||||
|
write_r16(height_data, r16_path)
|
||||||
|
png_written = write_png16(height_data, png_path)
|
||||||
|
write_preview_pgm(height_data, preview_path)
|
||||||
|
write_import_metadata(metadata_path, source, min_elevation, max_elevation, r16_path, png_path, png_written, preview_path)
|
||||||
|
|
||||||
|
print(f"R16: {r16_path}")
|
||||||
|
print(f"Preview: {preview_path}")
|
||||||
|
if png_written:
|
||||||
|
print(f"PNG16: {png_path}")
|
||||||
|
else:
|
||||||
|
print("PNG16 skipped: Pillow is not installed")
|
||||||
|
print(f"Metadata: {metadata_path}")
|
||||||
|
print(f"Elevation range: {min_elevation:.3f}m to {max_elevation:.3f}m")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Extract the 1 km Ground Zero subset from the acquired USGS DEM.
|
||||||
|
|
||||||
|
This script requires either rasterio or GDAL Python bindings. The current
|
||||||
|
Ubuntu-Codex image does not include those packages by default, so this file is
|
||||||
|
checked in as the repeatable extraction step once the geospatial dependency is
|
||||||
|
available.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from prototype_ground_zero_terrain import TARGET_TILE_ID
|
||||||
|
|
||||||
|
|
||||||
|
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
REGISTRY_PATH = PROJECT_ROOT / "Data" / "Tiles" / "ground_zero_tiles.json"
|
||||||
|
SOURCE_ROOT = PROJECT_ROOT / "Data" / "Terrain" / "Sources" / TARGET_TILE_ID
|
||||||
|
EXTRACT_ROOT = PROJECT_ROOT / "Data" / "Terrain" / "Extracted" / TARGET_TILE_ID
|
||||||
|
|
||||||
|
|
||||||
|
def load_ground_zero() -> dict:
|
||||||
|
registry = json.loads(REGISTRY_PATH.read_text())
|
||||||
|
for tile in registry["tiles"]:
|
||||||
|
if tile["tile_id"] == TARGET_TILE_ID:
|
||||||
|
return tile
|
||||||
|
raise RuntimeError(f"Could not find {TARGET_TILE_ID}")
|
||||||
|
|
||||||
|
|
||||||
|
def find_source_tiffs(tile: dict) -> list[Path]:
|
||||||
|
for source in tile["sources"]:
|
||||||
|
if source["source_kind"] != "elevation":
|
||||||
|
continue
|
||||||
|
if source.get("local_source_folder"):
|
||||||
|
folder = PROJECT_ROOT / source["local_source_folder"]
|
||||||
|
candidates = sorted(folder.glob("*.tif"))
|
||||||
|
if candidates:
|
||||||
|
return candidates
|
||||||
|
if source.get("local_source_path"):
|
||||||
|
path = PROJECT_ROOT / source["local_source_path"]
|
||||||
|
if path.exists():
|
||||||
|
return [path]
|
||||||
|
candidates = sorted(SOURCE_ROOT.glob("*.tif"))
|
||||||
|
if candidates:
|
||||||
|
return candidates
|
||||||
|
raise RuntimeError("No acquired source GeoTIFF found. Run acquire_ground_zero_dem.py first.")
|
||||||
|
|
||||||
|
|
||||||
|
def extract_with_rasterio(source_tiffs: list[Path], tile: dict) -> None:
|
||||||
|
import rasterio
|
||||||
|
from rasterio.merge import merge
|
||||||
|
from rasterio.windows import from_bounds
|
||||||
|
|
||||||
|
EXTRACT_ROOT.mkdir(parents=True, exist_ok=True)
|
||||||
|
grid = tile["grid"]
|
||||||
|
output_tiff = EXTRACT_ROOT / f"{TARGET_TILE_ID}_1m_dem_subset.tif"
|
||||||
|
output_metadata = EXTRACT_ROOT / f"{TARGET_TILE_ID}_1m_dem_subset_metadata.json"
|
||||||
|
bounds = (
|
||||||
|
grid["easting_min_m"],
|
||||||
|
grid["northing_min_m"],
|
||||||
|
grid["easting_max_m"],
|
||||||
|
grid["northing_max_m"],
|
||||||
|
)
|
||||||
|
|
||||||
|
datasets = [rasterio.open(path) for path in source_tiffs]
|
||||||
|
try:
|
||||||
|
if len(datasets) == 1:
|
||||||
|
dataset = datasets[0]
|
||||||
|
source_bounds = [list(dataset.bounds)]
|
||||||
|
source_crs = str(dataset.crs)
|
||||||
|
window = from_bounds(*bounds, dataset.transform).round_offsets().round_lengths()
|
||||||
|
data = dataset.read(window=window)
|
||||||
|
transform = dataset.window_transform(window)
|
||||||
|
profile = dataset.profile
|
||||||
|
else:
|
||||||
|
source_bounds = [list(dataset.bounds) for dataset in datasets]
|
||||||
|
source_crs = str(datasets[0].crs)
|
||||||
|
data, transform = merge(datasets, bounds=bounds, res=(1.0, 1.0), nodata=-999999)
|
||||||
|
profile = datasets[0].profile
|
||||||
|
|
||||||
|
profile.update(
|
||||||
|
{
|
||||||
|
"height": data.shape[1],
|
||||||
|
"width": data.shape[2],
|
||||||
|
"transform": transform,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
with rasterio.open(output_tiff, "w", **profile) as output:
|
||||||
|
output.write(data)
|
||||||
|
|
||||||
|
metadata = {
|
||||||
|
"tile_id": TARGET_TILE_ID,
|
||||||
|
"source_tiffs": [str(path.relative_to(PROJECT_ROOT)) for path in source_tiffs],
|
||||||
|
"output_tiff": str(output_tiff.relative_to(PROJECT_ROOT)),
|
||||||
|
"source_crs": source_crs,
|
||||||
|
"source_bounds": source_bounds,
|
||||||
|
"subset_bounds_utm_m": list(bounds),
|
||||||
|
"subset_width_pixels": int(data.shape[2]),
|
||||||
|
"subset_height_pixels": int(data.shape[1]),
|
||||||
|
"pixel_size_x": abs(transform.a),
|
||||||
|
"pixel_size_y": abs(transform.e),
|
||||||
|
}
|
||||||
|
output_metadata.write_text(json.dumps(metadata, indent=2) + "\n")
|
||||||
|
finally:
|
||||||
|
for dataset in datasets:
|
||||||
|
dataset.close()
|
||||||
|
|
||||||
|
print(f"Subset GeoTIFF: {output_tiff}")
|
||||||
|
print(f"Metadata: {output_metadata}")
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
tile = load_ground_zero()
|
||||||
|
source_tiffs = find_source_tiffs(tile)
|
||||||
|
try:
|
||||||
|
extract_with_rasterio(source_tiffs, tile)
|
||||||
|
except ModuleNotFoundError as exc:
|
||||||
|
raise SystemExit(
|
||||||
|
"Missing rasterio. Install rasterio or GDAL Python bindings, then rerun this script."
|
||||||
|
) from exc
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,269 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Generate a first real-elevation heightmap for the Ground Zero terrain tile.
|
||||||
|
|
||||||
|
This prototype intentionally uses only the Python standard library so it can run
|
||||||
|
on Ubuntu-Codex without extra GIS packages. It samples USGS EPQS elevations over
|
||||||
|
the selected 1 km UTM tile, writes CSV samples, writes a little-endian R16
|
||||||
|
heightmap, and records generation metadata.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import csv
|
||||||
|
import json
|
||||||
|
import math
|
||||||
|
import struct
|
||||||
|
import time
|
||||||
|
import urllib.error
|
||||||
|
import urllib.parse
|
||||||
|
import urllib.request
|
||||||
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
REGISTRY_PATH = PROJECT_ROOT / "Data" / "Tiles" / "ground_zero_tiles.json"
|
||||||
|
OUTPUT_ROOT = PROJECT_ROOT / "Data" / "Terrain" / "Generated"
|
||||||
|
USGS_EPQS_URL = "https://epqs.nationalmap.gov/v1/json"
|
||||||
|
TARGET_TILE_ID = "gz_us_ca_pacifica_utm10n_e544_n4160"
|
||||||
|
|
||||||
|
|
||||||
|
def utm_to_lat_lon(easting: float, northing: float, zone_number: int, northern: bool = True) -> tuple[float, float]:
|
||||||
|
"""Convert UTM WGS84 coordinates to latitude/longitude in degrees."""
|
||||||
|
a = 6378137.0
|
||||||
|
ecc_squared = 0.0066943799901413165
|
||||||
|
k0 = 0.9996
|
||||||
|
|
||||||
|
x = easting - 500000.0
|
||||||
|
y = northing
|
||||||
|
if not northern:
|
||||||
|
y -= 10000000.0
|
||||||
|
|
||||||
|
lon_origin = (zone_number - 1) * 6 - 180 + 3
|
||||||
|
ecc_prime_squared = ecc_squared / (1 - ecc_squared)
|
||||||
|
m = y / k0
|
||||||
|
mu = m / (a * (1 - ecc_squared / 4 - 3 * ecc_squared**2 / 64 - 5 * ecc_squared**3 / 256))
|
||||||
|
|
||||||
|
e1 = (1 - math.sqrt(1 - ecc_squared)) / (1 + math.sqrt(1 - ecc_squared))
|
||||||
|
j1 = 3 * e1 / 2 - 27 * e1**3 / 32
|
||||||
|
j2 = 21 * e1**2 / 16 - 55 * e1**4 / 32
|
||||||
|
j3 = 151 * e1**3 / 96
|
||||||
|
j4 = 1097 * e1**4 / 512
|
||||||
|
fp = mu + j1 * math.sin(2 * mu) + j2 * math.sin(4 * mu) + j3 * math.sin(6 * mu) + j4 * math.sin(8 * mu)
|
||||||
|
|
||||||
|
sin_fp = math.sin(fp)
|
||||||
|
cos_fp = math.cos(fp)
|
||||||
|
tan_fp = math.tan(fp)
|
||||||
|
|
||||||
|
c1 = ecc_prime_squared * cos_fp**2
|
||||||
|
t1 = tan_fp**2
|
||||||
|
n1 = a / math.sqrt(1 - ecc_squared * sin_fp**2)
|
||||||
|
r1 = a * (1 - ecc_squared) / ((1 - ecc_squared * sin_fp**2) ** 1.5)
|
||||||
|
d = x / (n1 * k0)
|
||||||
|
|
||||||
|
lat = fp - (n1 * tan_fp / r1) * (
|
||||||
|
d**2 / 2
|
||||||
|
- (5 + 3 * t1 + 10 * c1 - 4 * c1**2 - 9 * ecc_prime_squared) * d**4 / 24
|
||||||
|
+ (61 + 90 * t1 + 298 * c1 + 45 * t1**2 - 252 * ecc_prime_squared - 3 * c1**2) * d**6 / 720
|
||||||
|
)
|
||||||
|
lon = math.radians(lon_origin) + (
|
||||||
|
d
|
||||||
|
- (1 + 2 * t1 + c1) * d**3 / 6
|
||||||
|
+ (5 - 2 * c1 + 28 * t1 - 3 * c1**2 + 8 * ecc_prime_squared + 24 * t1**2) * d**5 / 120
|
||||||
|
) / cos_fp
|
||||||
|
|
||||||
|
return math.degrees(lat), math.degrees(lon)
|
||||||
|
|
||||||
|
|
||||||
|
def query_elevation(latitude: float, longitude: float, retries: int = 3) -> dict:
|
||||||
|
params = urllib.parse.urlencode({"x": f"{longitude:.8f}", "y": f"{latitude:.8f}", "units": "Meters"})
|
||||||
|
url = f"{USGS_EPQS_URL}?{params}"
|
||||||
|
last_error = None
|
||||||
|
|
||||||
|
for attempt in range(retries):
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(url, timeout=20) as response:
|
||||||
|
payload = json.loads(response.read().decode("utf-8"))
|
||||||
|
return {
|
||||||
|
"elevation_m": float(payload["value"]),
|
||||||
|
"raster_id": payload.get("rasterId"),
|
||||||
|
"resolution_m": payload.get("resolution"),
|
||||||
|
"query_url": url,
|
||||||
|
}
|
||||||
|
except (urllib.error.URLError, TimeoutError, KeyError, ValueError) as exc:
|
||||||
|
last_error = exc
|
||||||
|
time.sleep(0.5 * (attempt + 1))
|
||||||
|
|
||||||
|
raise RuntimeError(f"USGS elevation query failed for {latitude}, {longitude}: {last_error}")
|
||||||
|
|
||||||
|
|
||||||
|
def load_ground_zero_tile() -> dict:
|
||||||
|
registry = json.loads(REGISTRY_PATH.read_text())
|
||||||
|
for tile in registry["tiles"]:
|
||||||
|
if tile["tile_id"] == TARGET_TILE_ID:
|
||||||
|
return tile
|
||||||
|
raise RuntimeError(f"Could not find {TARGET_TILE_ID} in {REGISTRY_PATH}")
|
||||||
|
|
||||||
|
|
||||||
|
def build_sample_points(tile: dict, grid_size: int) -> list[dict]:
|
||||||
|
grid = tile["grid"]
|
||||||
|
e_min = grid["easting_min_m"]
|
||||||
|
n_min = grid["northing_min_m"]
|
||||||
|
spacing = grid["tile_size_m"] / (grid_size - 1)
|
||||||
|
points = []
|
||||||
|
|
||||||
|
for row in range(grid_size):
|
||||||
|
northing = n_min + row * spacing
|
||||||
|
for col in range(grid_size):
|
||||||
|
easting = e_min + col * spacing
|
||||||
|
latitude, longitude = utm_to_lat_lon(easting, northing, 10, True)
|
||||||
|
points.append(
|
||||||
|
{
|
||||||
|
"row": row,
|
||||||
|
"col": col,
|
||||||
|
"easting_m": easting,
|
||||||
|
"northing_m": northing,
|
||||||
|
"latitude": latitude,
|
||||||
|
"longitude": longitude,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return points
|
||||||
|
|
||||||
|
|
||||||
|
def sample_elevations(points: list[dict], workers: int) -> list[dict]:
|
||||||
|
sampled = []
|
||||||
|
with ThreadPoolExecutor(max_workers=workers) as executor:
|
||||||
|
future_to_point = {
|
||||||
|
executor.submit(query_elevation, point["latitude"], point["longitude"]): point for point in points
|
||||||
|
}
|
||||||
|
for future in as_completed(future_to_point):
|
||||||
|
point = future_to_point[future]
|
||||||
|
elevation = future.result()
|
||||||
|
point.update(elevation)
|
||||||
|
sampled.append(point)
|
||||||
|
|
||||||
|
return sorted(sampled, key=lambda item: (item["row"], item["col"]))
|
||||||
|
|
||||||
|
|
||||||
|
def write_csv(samples: list[dict], output_path: Path) -> None:
|
||||||
|
fieldnames = [
|
||||||
|
"row",
|
||||||
|
"col",
|
||||||
|
"easting_m",
|
||||||
|
"northing_m",
|
||||||
|
"latitude",
|
||||||
|
"longitude",
|
||||||
|
"elevation_m",
|
||||||
|
"raster_id",
|
||||||
|
"resolution_m",
|
||||||
|
]
|
||||||
|
with output_path.open("w", newline="") as output_file:
|
||||||
|
writer = csv.DictWriter(output_file, fieldnames=fieldnames)
|
||||||
|
writer.writeheader()
|
||||||
|
for sample in samples:
|
||||||
|
writer.writerow({field: sample.get(field, "") for field in fieldnames})
|
||||||
|
|
||||||
|
|
||||||
|
def write_r16(samples: list[dict], output_path: Path) -> tuple[float, float]:
|
||||||
|
elevations = [sample["elevation_m"] for sample in samples]
|
||||||
|
min_elevation = min(elevations)
|
||||||
|
max_elevation = max(elevations)
|
||||||
|
elevation_range = max(max_elevation - min_elevation, 0.001)
|
||||||
|
|
||||||
|
with output_path.open("wb") as output_file:
|
||||||
|
for sample in samples:
|
||||||
|
normalized = (sample["elevation_m"] - min_elevation) / elevation_range
|
||||||
|
output_file.write(struct.pack("<H", round(normalized * 65535)))
|
||||||
|
|
||||||
|
return min_elevation, max_elevation
|
||||||
|
|
||||||
|
|
||||||
|
def write_metadata(tile: dict, samples: list[dict], grid_size: int, min_elevation: float, max_elevation: float, output_path: Path) -> None:
|
||||||
|
raster_ids = sorted({sample["raster_id"] for sample in samples if sample.get("raster_id") is not None})
|
||||||
|
resolutions = sorted({sample["resolution_m"] for sample in samples if sample.get("resolution_m") is not None})
|
||||||
|
metadata = {
|
||||||
|
"tile_id": tile["tile_id"],
|
||||||
|
"generated_at_utc": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
||||||
|
"source": {
|
||||||
|
"name": "USGS Elevation Point Query Service",
|
||||||
|
"url": USGS_EPQS_URL,
|
||||||
|
"units": "Meters",
|
||||||
|
"raster_ids": raster_ids,
|
||||||
|
"reported_resolutions_m": resolutions,
|
||||||
|
},
|
||||||
|
"grid": tile["grid"],
|
||||||
|
"sample_grid": {
|
||||||
|
"width": grid_size,
|
||||||
|
"height": grid_size,
|
||||||
|
"spacing_m": tile["grid"]["tile_size_m"] / (grid_size - 1),
|
||||||
|
"sample_count": len(samples),
|
||||||
|
},
|
||||||
|
"heightmap": {
|
||||||
|
"format": "r16_little_endian_unsigned",
|
||||||
|
"min_elevation_m": min_elevation,
|
||||||
|
"max_elevation_m": max_elevation,
|
||||||
|
"vertical_range_m": max_elevation - min_elevation,
|
||||||
|
},
|
||||||
|
"unreal_import_notes": [
|
||||||
|
"Prototype only; final landscape import should use a higher-resolution DEM raster or lidar-derived terrain.",
|
||||||
|
"R16 values are normalized from min_elevation_m to max_elevation_m.",
|
||||||
|
"Use the metadata min/max values to restore vertical scale during Unreal import.",
|
||||||
|
"Horizontal tile size is 1000 m x 1000 m."
|
||||||
|
],
|
||||||
|
}
|
||||||
|
output_path.write_text(json.dumps(metadata, indent=2) + "\n")
|
||||||
|
|
||||||
|
|
||||||
|
def update_registry(tile_id: str) -> None:
|
||||||
|
registry = json.loads(REGISTRY_PATH.read_text())
|
||||||
|
for tile in registry["tiles"]:
|
||||||
|
if tile["tile_id"] == tile_id:
|
||||||
|
tile["status"] = "generated"
|
||||||
|
tile["generation_version"] = max(tile.get("generation_version", 0), 1)
|
||||||
|
tile["package_version"] = max(tile.get("package_version", 0), 0)
|
||||||
|
tile["notes"] = (
|
||||||
|
"Prototype USGS EPQS elevation heightmap generated. "
|
||||||
|
"Use Data/Terrain/Generated/gz_us_ca_pacifica_utm10n_e544_n4160 for samples, R16 heightmap, and metadata."
|
||||||
|
)
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
raise RuntimeError(f"Could not update missing tile {tile_id}")
|
||||||
|
|
||||||
|
REGISTRY_PATH.write_text(json.dumps(registry, indent=2) + "\n")
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument("--grid-size", type=int, default=33, help="Sample grid width/height. Default: 33")
|
||||||
|
parser.add_argument("--workers", type=int, default=8, help="Concurrent USGS requests. Default: 8")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if args.grid_size < 2:
|
||||||
|
raise SystemExit("--grid-size must be at least 2")
|
||||||
|
|
||||||
|
tile = load_ground_zero_tile()
|
||||||
|
points = build_sample_points(tile, args.grid_size)
|
||||||
|
output_dir = OUTPUT_ROOT / tile["tile_id"]
|
||||||
|
output_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
samples = sample_elevations(points, args.workers)
|
||||||
|
csv_path = output_dir / f"{tile['tile_id']}_elevation_samples_{args.grid_size}.csv"
|
||||||
|
r16_path = output_dir / f"{tile['tile_id']}_heightmap_{args.grid_size}.r16"
|
||||||
|
metadata_path = output_dir / f"{tile['tile_id']}_terrain_metadata.json"
|
||||||
|
|
||||||
|
write_csv(samples, csv_path)
|
||||||
|
min_elevation, max_elevation = write_r16(samples, r16_path)
|
||||||
|
write_metadata(tile, samples, args.grid_size, min_elevation, max_elevation, metadata_path)
|
||||||
|
update_registry(tile["tile_id"])
|
||||||
|
|
||||||
|
print(f"Generated {len(samples)} samples for {tile['tile_id']}")
|
||||||
|
print(f"Elevation range: {min_elevation:.3f}m to {max_elevation:.3f}m")
|
||||||
|
print(f"CSV: {csv_path}")
|
||||||
|
print(f"R16: {r16_path}")
|
||||||
|
print(f"Metadata: {metadata_path}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import unreal
|
||||||
|
|
||||||
|
|
||||||
|
MAP_PATH = "/Game/Agrarian/Maps/L_GroundZeroTerrain_Test"
|
||||||
|
PROJECT_ROOT = Path(r"Z:\AgrarianGameBulid")
|
||||||
|
TILE_ID = "gz_us_ca_pacifica_utm10n_e544_n4160"
|
||||||
|
UNREAL_TERRAIN_ROOT = PROJECT_ROOT / "Data" / "Terrain" / "Unreal" / TILE_ID
|
||||||
|
HEIGHTMAP_PATH = UNREAL_TERRAIN_ROOT / f"{TILE_ID}_unreal_1009.r16"
|
||||||
|
METADATA_PATH = UNREAL_TERRAIN_ROOT / f"{TILE_ID}_unreal_heightmap_metadata.json"
|
||||||
|
LANDSCAPE_LABEL = "AGR_GroundZero_Landscape"
|
||||||
|
|
||||||
|
|
||||||
|
def get_actor_label(actor):
|
||||||
|
try:
|
||||||
|
return actor.get_actor_label()
|
||||||
|
except Exception:
|
||||||
|
return actor.get_name()
|
||||||
|
|
||||||
|
|
||||||
|
def remove_existing_landscape():
|
||||||
|
for actor in unreal.EditorLevelLibrary.get_all_level_actors():
|
||||||
|
if get_actor_label(actor) == LANDSCAPE_LABEL:
|
||||||
|
unreal.EditorLevelLibrary.destroy_actor(actor)
|
||||||
|
|
||||||
|
|
||||||
|
def create_or_load_map():
|
||||||
|
unreal.EditorAssetLibrary.make_directory("/Game/Agrarian/Maps")
|
||||||
|
if unreal.EditorAssetLibrary.does_asset_exist(MAP_PATH):
|
||||||
|
if not unreal.EditorLevelLibrary.load_level(MAP_PATH):
|
||||||
|
raise RuntimeError(f"Could not load map: {MAP_PATH}")
|
||||||
|
return
|
||||||
|
|
||||||
|
level_subsystem = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem)
|
||||||
|
if hasattr(level_subsystem, "new_level"):
|
||||||
|
if not level_subsystem.new_level(MAP_PATH):
|
||||||
|
raise RuntimeError(f"Could not create map: {MAP_PATH}")
|
||||||
|
return
|
||||||
|
|
||||||
|
if hasattr(unreal.EditorLevelLibrary, "new_level"):
|
||||||
|
if not unreal.EditorLevelLibrary.new_level(MAP_PATH):
|
||||||
|
raise RuntimeError(f"Could not create map: {MAP_PATH}")
|
||||||
|
return
|
||||||
|
|
||||||
|
raise RuntimeError("No supported Unreal Python API found for creating a level")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
create_or_load_map()
|
||||||
|
remove_existing_landscape()
|
||||||
|
|
||||||
|
with METADATA_PATH.open("r", encoding="utf-8") as metadata_file:
|
||||||
|
metadata = json.load(metadata_file)
|
||||||
|
|
||||||
|
import_settings = metadata["unreal_landscape_import"]
|
||||||
|
heightmap = metadata["heightmap"]
|
||||||
|
result = unreal.AgrarianEditorAutomationLibrary.import_landscape_heightmap_into_editor_world(
|
||||||
|
str(HEIGHTMAP_PATH),
|
||||||
|
int(heightmap["width"]),
|
||||||
|
int(heightmap["height"]),
|
||||||
|
float(import_settings["x_scale_cm"]),
|
||||||
|
float(import_settings["y_scale_cm"]),
|
||||||
|
float(import_settings["z_scale_cm"]),
|
||||||
|
LANDSCAPE_LABEL,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not str(result).startswith("PASS:"):
|
||||||
|
raise RuntimeError(str(result))
|
||||||
|
|
||||||
|
unreal.log(str(result))
|
||||||
|
unreal.EditorLevelLibrary.save_current_level()
|
||||||
|
unreal.log(f"Ground Zero terrain map saved: {MAP_PATH}")
|
||||||
|
|
||||||
|
|
||||||
|
main()
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
import unreal
|
||||||
|
|
||||||
|
|
||||||
|
ITEM_FOLDER = "/Game/Agrarian/DataAssets/Items"
|
||||||
|
|
||||||
|
ITEMS = [
|
||||||
|
{
|
||||||
|
"asset": "DA_Item_Wood",
|
||||||
|
"item_id": "wood",
|
||||||
|
"display_name": "Wood",
|
||||||
|
"description": "Usable branches, logs, and rough-cut timber for fires, tools, and early structures.",
|
||||||
|
"item_type": unreal.AgrarianItemType.RESOURCE,
|
||||||
|
"unit_weight": 1.0,
|
||||||
|
"max_stack_size": 99,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"asset": "DA_Item_Stone",
|
||||||
|
"item_id": "stone",
|
||||||
|
"display_name": "Stone",
|
||||||
|
"description": "Field stone suitable for crude tools, fire rings, and primitive construction.",
|
||||||
|
"item_type": unreal.AgrarianItemType.RESOURCE,
|
||||||
|
"unit_weight": 1.5,
|
||||||
|
"max_stack_size": 99,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"asset": "DA_Item_Fiber",
|
||||||
|
"item_id": "fiber",
|
||||||
|
"display_name": "Fiber",
|
||||||
|
"description": "Plant fiber that can be twisted, woven, or bound into survival gear and simple structures.",
|
||||||
|
"item_type": unreal.AgrarianItemType.RESOURCE,
|
||||||
|
"unit_weight": 0.1,
|
||||||
|
"max_stack_size": 99,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"asset": "DA_Item_Food",
|
||||||
|
"item_id": "food",
|
||||||
|
"display_name": "Food",
|
||||||
|
"description": "Foraged edible food that can stave off hunger in the earliest survival loop.",
|
||||||
|
"item_type": unreal.AgrarianItemType.FOOD,
|
||||||
|
"unit_weight": 0.3,
|
||||||
|
"max_stack_size": 50,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"asset": "DA_Item_Meat",
|
||||||
|
"item_id": "meat",
|
||||||
|
"display_name": "Meat",
|
||||||
|
"description": "Raw harvested meat that should be cooked or preserved before long-term use.",
|
||||||
|
"item_type": unreal.AgrarianItemType.FOOD,
|
||||||
|
"unit_weight": 0.5,
|
||||||
|
"max_stack_size": 50,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"asset": "DA_Item_Hide",
|
||||||
|
"item_id": "hide",
|
||||||
|
"display_name": "Hide",
|
||||||
|
"description": "Animal hide used for shelter, clothing, containers, and other early craft work.",
|
||||||
|
"item_type": unreal.AgrarianItemType.RESOURCE,
|
||||||
|
"unit_weight": 0.7,
|
||||||
|
"max_stack_size": 50,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"asset": "DA_Item_PrimitiveFrame",
|
||||||
|
"item_id": "primitive_frame",
|
||||||
|
"display_name": "Primitive Frame",
|
||||||
|
"description": "A lashed support frame used as the backbone for early shelters and simple buildables.",
|
||||||
|
"item_type": unreal.AgrarianItemType.STRUCTURE,
|
||||||
|
"unit_weight": 3.0,
|
||||||
|
"max_stack_size": 20,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"asset": "DA_Item_PrimitiveWallPanel",
|
||||||
|
"item_id": "primitive_wall_panel",
|
||||||
|
"display_name": "Primitive Wall Panel",
|
||||||
|
"description": "A crude wall panel built from wood and fiber for early shelter construction.",
|
||||||
|
"item_type": unreal.AgrarianItemType.STRUCTURE,
|
||||||
|
"unit_weight": 4.0,
|
||||||
|
"max_stack_size": 20,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"asset": "DA_Item_PrimitiveRoofPanel",
|
||||||
|
"item_id": "primitive_roof_panel",
|
||||||
|
"display_name": "Primitive Roof Panel",
|
||||||
|
"description": "A simple roof panel made from lashed branches, fiber, and cover material.",
|
||||||
|
"item_type": unreal.AgrarianItemType.STRUCTURE,
|
||||||
|
"unit_weight": 4.0,
|
||||||
|
"max_stack_size": 20,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"asset": "DA_Item_Campfire",
|
||||||
|
"item_id": "campfire",
|
||||||
|
"display_name": "Campfire",
|
||||||
|
"description": "A placeable fire ring for warmth, light, and early cooking.",
|
||||||
|
"item_type": unreal.AgrarianItemType.STRUCTURE,
|
||||||
|
"unit_weight": 12.0,
|
||||||
|
"max_stack_size": 5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"asset": "DA_Item_PrimitiveShelter",
|
||||||
|
"item_id": "primitive_shelter",
|
||||||
|
"display_name": "Primitive Shelter",
|
||||||
|
"description": "A compact early shelter that provides basic weather protection.",
|
||||||
|
"item_type": unreal.AgrarianItemType.STRUCTURE,
|
||||||
|
"unit_weight": 25.0,
|
||||||
|
"max_stack_size": 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"asset": "DA_Item_BasicTool",
|
||||||
|
"item_id": "basic_tool",
|
||||||
|
"display_name": "Basic Tool",
|
||||||
|
"description": "A crude stone-and-wood tool for the first gather and build loops.",
|
||||||
|
"item_type": unreal.AgrarianItemType.TOOL,
|
||||||
|
"unit_weight": 1.2,
|
||||||
|
"max_stack_size": 10,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"asset": "DA_Item_Bandage",
|
||||||
|
"item_id": "bandage",
|
||||||
|
"display_name": "Bandage",
|
||||||
|
"description": "A simple treatment item made from hide and fiber.",
|
||||||
|
"item_type": unreal.AgrarianItemType.MEDICINE,
|
||||||
|
"unit_weight": 0.1,
|
||||||
|
"max_stack_size": 20,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def create_or_load_item_asset(asset_name):
|
||||||
|
path = f"{ITEM_FOLDER}/{asset_name}"
|
||||||
|
existing = unreal.EditorAssetLibrary.load_asset(path)
|
||||||
|
if existing:
|
||||||
|
return existing
|
||||||
|
|
||||||
|
factory = unreal.DataAssetFactory()
|
||||||
|
factory.set_editor_property("data_asset_class", unreal.AgrarianItemDefinitionAsset)
|
||||||
|
asset_tools = unreal.AssetToolsHelpers.get_asset_tools()
|
||||||
|
asset = asset_tools.create_asset(asset_name, ITEM_FOLDER, unreal.AgrarianItemDefinitionAsset, factory)
|
||||||
|
if not asset:
|
||||||
|
raise RuntimeError(f"Could not create {path}")
|
||||||
|
return asset
|
||||||
|
|
||||||
|
|
||||||
|
def apply_definition(asset, item):
|
||||||
|
definition = unreal.AgrarianItemDefinition()
|
||||||
|
definition.set_editor_property("item_id", item["item_id"])
|
||||||
|
definition.set_editor_property("display_name", item["display_name"])
|
||||||
|
definition.set_editor_property("description", item["description"])
|
||||||
|
definition.set_editor_property("item_type", item["item_type"])
|
||||||
|
definition.set_editor_property("unit_weight", item["unit_weight"])
|
||||||
|
definition.set_editor_property("max_stack_size", item["max_stack_size"])
|
||||||
|
asset.set_editor_property("definition", definition)
|
||||||
|
unreal.EditorAssetLibrary.save_loaded_asset(asset)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
unreal.EditorAssetLibrary.make_directory(ITEM_FOLDER)
|
||||||
|
for item in ITEMS:
|
||||||
|
asset = create_or_load_item_asset(item["asset"])
|
||||||
|
apply_definition(asset, item)
|
||||||
|
unreal.log(f"Configured item definition: {item['item_id']} -> {ITEM_FOLDER}/{item['asset']}")
|
||||||
|
|
||||||
|
unreal.log("Agrarian item definition setup complete.")
|
||||||
|
|
||||||
|
|
||||||
|
main()
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
import unreal
|
||||||
|
|
||||||
|
|
||||||
|
BLUEPRINT_ROOT = "/Game/Agrarian/Blueprints"
|
||||||
|
RESOURCE_FOLDER = f"{BLUEPRINT_ROOT}/Resources"
|
||||||
|
STRUCTURE_FOLDER = f"{BLUEPRINT_ROOT}/Structures"
|
||||||
|
WILDLIFE_FOLDER = f"{BLUEPRINT_ROOT}/Wildlife"
|
||||||
|
|
||||||
|
WOOD_ITEM_PATH = "/Game/Agrarian/DataAssets/Items/DA_Item_Wood"
|
||||||
|
FIBER_ITEM_PATH = "/Game/Agrarian/DataAssets/Items/DA_Item_Fiber"
|
||||||
|
|
||||||
|
MESH_CUBE_PATH = "/Game/LevelPrototyping/Meshes/SM_Cube"
|
||||||
|
MESH_CYLINDER_PATH = "/Game/LevelPrototyping/Meshes/SM_Cylinder"
|
||||||
|
|
||||||
|
BLUEPRINTS = [
|
||||||
|
{
|
||||||
|
"asset": "BP_WoodResourceNode",
|
||||||
|
"folder": RESOURCE_FOLDER,
|
||||||
|
"parent": unreal.AgrarianResourceNode,
|
||||||
|
"defaults": {
|
||||||
|
"yield_item_definition": WOOD_ITEM_PATH,
|
||||||
|
"remaining_harvests": 16,
|
||||||
|
"quantity_per_harvest": 2,
|
||||||
|
},
|
||||||
|
"mesh": MESH_CUBE_PATH,
|
||||||
|
"scale": unreal.Vector(1.0, 1.0, 1.5),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"asset": "BP_FiberResourceNode",
|
||||||
|
"folder": RESOURCE_FOLDER,
|
||||||
|
"parent": unreal.AgrarianResourceNode,
|
||||||
|
"defaults": {
|
||||||
|
"yield_item_definition": FIBER_ITEM_PATH,
|
||||||
|
"remaining_harvests": 10,
|
||||||
|
"quantity_per_harvest": 3,
|
||||||
|
},
|
||||||
|
"mesh": MESH_CYLINDER_PATH,
|
||||||
|
"scale": unreal.Vector(0.8, 0.8, 1.0),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"asset": "BP_Campfire",
|
||||||
|
"folder": STRUCTURE_FOLDER,
|
||||||
|
"parent": unreal.AgrarianCampfire,
|
||||||
|
"defaults": {
|
||||||
|
"fuel_seconds": 180.0,
|
||||||
|
"warmth_radius": 650.0,
|
||||||
|
"warmth_per_second": 0.03,
|
||||||
|
},
|
||||||
|
"mesh": MESH_CYLINDER_PATH,
|
||||||
|
"scale": unreal.Vector(1.3, 1.3, 0.25),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"asset": "BP_PrimitiveShelter",
|
||||||
|
"folder": STRUCTURE_FOLDER,
|
||||||
|
"parent": unreal.AgrarianShelterActor,
|
||||||
|
"defaults": {
|
||||||
|
"weather_protection": 0.7,
|
||||||
|
},
|
||||||
|
"mesh": MESH_CUBE_PATH,
|
||||||
|
"scale": unreal.Vector(3.0, 2.0, 1.4),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"asset": "BP_RabbitWildlife",
|
||||||
|
"folder": WILDLIFE_FOLDER,
|
||||||
|
"parent": unreal.AgrarianWildlifeBase,
|
||||||
|
"defaults": {
|
||||||
|
"wildlife_id": "rabbit",
|
||||||
|
"display_name": "Rabbit",
|
||||||
|
"max_health": 12.0,
|
||||||
|
"health": 12.0,
|
||||||
|
"wander_radius": 900.0,
|
||||||
|
"wander_speed": 160.0,
|
||||||
|
"flee_speed": 520.0,
|
||||||
|
"aggro_radius": 0.0,
|
||||||
|
"flee_radius": 750.0,
|
||||||
|
"decision_interval_seconds": 1.5,
|
||||||
|
"harvest_yields": [
|
||||||
|
{
|
||||||
|
"item_id": "meat",
|
||||||
|
"display_name": "Meat",
|
||||||
|
"quantity": 1,
|
||||||
|
"unit_weight": 0.5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"item_id": "hide",
|
||||||
|
"display_name": "Hide",
|
||||||
|
"quantity": 2,
|
||||||
|
"unit_weight": 0.7,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def load_required_asset(path):
|
||||||
|
asset = unreal.EditorAssetLibrary.load_asset(path)
|
||||||
|
if not asset:
|
||||||
|
raise RuntimeError(f"Required asset not found: {path}")
|
||||||
|
return asset
|
||||||
|
|
||||||
|
|
||||||
|
def create_or_load_blueprint(asset_name, folder, parent_class):
|
||||||
|
path = f"{folder}/{asset_name}"
|
||||||
|
existing = unreal.EditorAssetLibrary.load_asset(path)
|
||||||
|
if existing:
|
||||||
|
return existing
|
||||||
|
|
||||||
|
factory = unreal.BlueprintFactory()
|
||||||
|
factory.set_editor_property("parent_class", parent_class)
|
||||||
|
asset_tools = unreal.AssetToolsHelpers.get_asset_tools()
|
||||||
|
blueprint = asset_tools.create_asset(asset_name, folder, None, factory)
|
||||||
|
if not blueprint:
|
||||||
|
raise RuntimeError(f"Could not create {path}")
|
||||||
|
return blueprint
|
||||||
|
|
||||||
|
|
||||||
|
def make_stack(data):
|
||||||
|
stack = unreal.AgrarianItemStack()
|
||||||
|
stack.set_editor_property("item_id", data["item_id"])
|
||||||
|
stack.set_editor_property("display_name", data["display_name"])
|
||||||
|
stack.set_editor_property("quantity", data["quantity"])
|
||||||
|
stack.set_editor_property("unit_weight", data["unit_weight"])
|
||||||
|
return stack
|
||||||
|
|
||||||
|
|
||||||
|
def apply_defaults(blueprint, config):
|
||||||
|
blueprint_path = f"{config['folder']}/{config['asset']}"
|
||||||
|
unreal.BlueprintEditorLibrary.compile_blueprint(blueprint)
|
||||||
|
generated_class = unreal.EditorAssetLibrary.load_blueprint_class(blueprint_path)
|
||||||
|
if not generated_class:
|
||||||
|
raise RuntimeError(f"Could not load generated class for {blueprint_path}")
|
||||||
|
|
||||||
|
cdo = unreal.get_default_object(generated_class)
|
||||||
|
|
||||||
|
for property_name, value in config.get("defaults", {}).items():
|
||||||
|
if property_name == "yield_item_definition":
|
||||||
|
value = load_required_asset(value)
|
||||||
|
elif property_name == "harvest_yields":
|
||||||
|
value = [make_stack(stack_data) for stack_data in value]
|
||||||
|
|
||||||
|
cdo.set_editor_property(property_name, value)
|
||||||
|
|
||||||
|
mesh_path = config.get("mesh")
|
||||||
|
if mesh_path:
|
||||||
|
mesh_component = cdo.get_editor_property("mesh")
|
||||||
|
mesh_component.set_editor_property("static_mesh", load_required_asset(mesh_path))
|
||||||
|
mesh_component.set_editor_property("relative_scale3d", config.get("scale", unreal.Vector(1.0, 1.0, 1.0)))
|
||||||
|
|
||||||
|
unreal.BlueprintEditorLibrary.compile_blueprint(blueprint)
|
||||||
|
unreal.EditorAssetLibrary.save_loaded_asset(blueprint)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
for folder in (RESOURCE_FOLDER, STRUCTURE_FOLDER, WILDLIFE_FOLDER):
|
||||||
|
unreal.EditorAssetLibrary.make_directory(folder)
|
||||||
|
|
||||||
|
for config in BLUEPRINTS:
|
||||||
|
blueprint = create_or_load_blueprint(config["asset"], config["folder"], config["parent"])
|
||||||
|
apply_defaults(blueprint, config)
|
||||||
|
unreal.log(f"Configured Blueprint: {config['folder']}/{config['asset']}")
|
||||||
|
|
||||||
|
unreal.log("Agrarian playable Blueprint setup complete.")
|
||||||
|
|
||||||
|
|
||||||
|
main()
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
import unreal
|
||||||
|
|
||||||
|
|
||||||
|
RECIPE_FOLDER = "/Game/Agrarian/DataAssets/Recipes"
|
||||||
|
|
||||||
|
ITEM_DISPLAY = {
|
||||||
|
"wood": ("Wood", 1.0),
|
||||||
|
"stone": ("Stone", 1.5),
|
||||||
|
"fiber": ("Fiber", 0.1),
|
||||||
|
"hide": ("Hide", 0.7),
|
||||||
|
"primitive_frame": ("Primitive Frame", 3.0),
|
||||||
|
"primitive_wall_panel": ("Primitive Wall Panel", 4.0),
|
||||||
|
"primitive_roof_panel": ("Primitive Roof Panel", 4.0),
|
||||||
|
"campfire": ("Campfire", 12.0),
|
||||||
|
"primitive_shelter": ("Primitive Shelter", 25.0),
|
||||||
|
"basic_tool": ("Basic Tool", 1.2),
|
||||||
|
"bandage": ("Bandage", 0.1),
|
||||||
|
}
|
||||||
|
|
||||||
|
RECIPES = [
|
||||||
|
{
|
||||||
|
"asset": "DA_Recipe_Campfire",
|
||||||
|
"recipe_id": "campfire",
|
||||||
|
"display_name": "Campfire",
|
||||||
|
"ingredients": [("wood", 5), ("stone", 8), ("fiber", 2)],
|
||||||
|
"result": ("campfire", 1),
|
||||||
|
"craft_seconds": 8.0,
|
||||||
|
"requires_campfire": False,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"asset": "DA_Recipe_PrimitiveShelter",
|
||||||
|
"recipe_id": "primitive_shelter",
|
||||||
|
"display_name": "Primitive Shelter",
|
||||||
|
"ingredients": [
|
||||||
|
("primitive_frame", 2),
|
||||||
|
("primitive_wall_panel", 4),
|
||||||
|
("primitive_roof_panel", 2),
|
||||||
|
("hide", 2),
|
||||||
|
("fiber", 6),
|
||||||
|
],
|
||||||
|
"result": ("primitive_shelter", 1),
|
||||||
|
"craft_seconds": 20.0,
|
||||||
|
"requires_campfire": False,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"asset": "DA_Recipe_PrimitiveFrame",
|
||||||
|
"recipe_id": "primitive_frame",
|
||||||
|
"display_name": "Primitive Frame",
|
||||||
|
"ingredients": [("wood", 4), ("fiber", 2)],
|
||||||
|
"result": ("primitive_frame", 1),
|
||||||
|
"craft_seconds": 8.0,
|
||||||
|
"requires_campfire": False,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"asset": "DA_Recipe_PrimitiveWallPanel",
|
||||||
|
"recipe_id": "primitive_wall_panel",
|
||||||
|
"display_name": "Primitive Wall Panel",
|
||||||
|
"ingredients": [("wood", 3), ("fiber", 2)],
|
||||||
|
"result": ("primitive_wall_panel", 1),
|
||||||
|
"craft_seconds": 6.0,
|
||||||
|
"requires_campfire": False,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"asset": "DA_Recipe_PrimitiveRoofPanel",
|
||||||
|
"recipe_id": "primitive_roof_panel",
|
||||||
|
"display_name": "Primitive Roof Panel",
|
||||||
|
"ingredients": [("wood", 3), ("fiber", 3)],
|
||||||
|
"result": ("primitive_roof_panel", 1),
|
||||||
|
"craft_seconds": 7.0,
|
||||||
|
"requires_campfire": False,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"asset": "DA_Recipe_BasicTool",
|
||||||
|
"recipe_id": "basic_tool",
|
||||||
|
"display_name": "Basic Tool",
|
||||||
|
"ingredients": [("wood", 1), ("stone", 2), ("fiber", 1)],
|
||||||
|
"result": ("basic_tool", 1),
|
||||||
|
"craft_seconds": 6.0,
|
||||||
|
"requires_campfire": False,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"asset": "DA_Recipe_Bandage",
|
||||||
|
"recipe_id": "bandage",
|
||||||
|
"display_name": "Bandage",
|
||||||
|
"ingredients": [("fiber", 3), ("hide", 1)],
|
||||||
|
"result": ("bandage", 1),
|
||||||
|
"craft_seconds": 4.0,
|
||||||
|
"requires_campfire": False,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def make_stack(item_id, quantity):
|
||||||
|
display_name, unit_weight = ITEM_DISPLAY[item_id]
|
||||||
|
stack = unreal.AgrarianItemStack()
|
||||||
|
stack.set_editor_property("item_id", item_id)
|
||||||
|
stack.set_editor_property("display_name", display_name)
|
||||||
|
stack.set_editor_property("quantity", quantity)
|
||||||
|
stack.set_editor_property("unit_weight", unit_weight)
|
||||||
|
return stack
|
||||||
|
|
||||||
|
|
||||||
|
def set_requires_campfire(recipe, value):
|
||||||
|
for property_name in ("requires_campfire", "b_requires_campfire"):
|
||||||
|
try:
|
||||||
|
recipe.set_editor_property(property_name, value)
|
||||||
|
return
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if value:
|
||||||
|
raise RuntimeError("Could not set bRequiresCampfire on recipe asset")
|
||||||
|
|
||||||
|
|
||||||
|
def create_or_load_recipe_asset(asset_name):
|
||||||
|
path = f"{RECIPE_FOLDER}/{asset_name}"
|
||||||
|
existing = unreal.EditorAssetLibrary.load_asset(path)
|
||||||
|
if existing:
|
||||||
|
return existing
|
||||||
|
|
||||||
|
factory = unreal.DataAssetFactory()
|
||||||
|
factory.set_editor_property("data_asset_class", unreal.AgrarianRecipeDataAsset)
|
||||||
|
asset_tools = unreal.AssetToolsHelpers.get_asset_tools()
|
||||||
|
asset = asset_tools.create_asset(asset_name, RECIPE_FOLDER, unreal.AgrarianRecipeDataAsset, factory)
|
||||||
|
if not asset:
|
||||||
|
raise RuntimeError(f"Could not create {path}")
|
||||||
|
return asset
|
||||||
|
|
||||||
|
|
||||||
|
def apply_recipe(asset, recipe_data):
|
||||||
|
recipe = unreal.AgrarianRecipe()
|
||||||
|
recipe.set_editor_property("recipe_id", recipe_data["recipe_id"])
|
||||||
|
recipe.set_editor_property("display_name", recipe_data["display_name"])
|
||||||
|
recipe.set_editor_property(
|
||||||
|
"ingredients",
|
||||||
|
[make_stack(item_id, quantity) for item_id, quantity in recipe_data["ingredients"]],
|
||||||
|
)
|
||||||
|
result_id, result_quantity = recipe_data["result"]
|
||||||
|
recipe.set_editor_property("result", make_stack(result_id, result_quantity))
|
||||||
|
recipe.set_editor_property("craft_seconds", recipe_data["craft_seconds"])
|
||||||
|
set_requires_campfire(recipe, recipe_data["requires_campfire"])
|
||||||
|
asset.set_editor_property("recipe", recipe)
|
||||||
|
unreal.EditorAssetLibrary.save_loaded_asset(asset)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
unreal.EditorAssetLibrary.make_directory(RECIPE_FOLDER)
|
||||||
|
for recipe_data in RECIPES:
|
||||||
|
asset = create_or_load_recipe_asset(recipe_data["asset"])
|
||||||
|
apply_recipe(asset, recipe_data)
|
||||||
|
unreal.log(f"Configured recipe: {recipe_data['recipe_id']} -> {RECIPE_FOLDER}/{recipe_data['asset']}")
|
||||||
|
|
||||||
|
unreal.log("Agrarian recipe setup complete.")
|
||||||
|
|
||||||
|
|
||||||
|
main()
|
||||||
@@ -10,6 +10,12 @@ PLACEMENTS = [
|
|||||||
"location": unreal.Vector(650.0, -150.0, 120.0),
|
"location": unreal.Vector(650.0, -150.0, 120.0),
|
||||||
"rotation": unreal.Rotator(0.0, 15.0, 0.0),
|
"rotation": unreal.Rotator(0.0, 15.0, 0.0),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"label": "AGR_FiberResourceNode_01",
|
||||||
|
"class_path": "/Game/Agrarian/Blueprints/Resources/BP_FiberResourceNode",
|
||||||
|
"location": unreal.Vector(560.0, 140.0, 90.0),
|
||||||
|
"rotation": unreal.Rotator(0.0, -10.0, 0.0),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"label": "AGR_Campfire_01",
|
"label": "AGR_Campfire_01",
|
||||||
"class_path": "/Game/Agrarian/Blueprints/Structures/BP_Campfire",
|
"class_path": "/Game/Agrarian/Blueprints/Structures/BP_Campfire",
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import unreal
|
||||||
|
|
||||||
|
|
||||||
|
MAP_PATH = "/Game/Agrarian/Maps/L_GroundZeroTerrain_Test"
|
||||||
|
PROJECT_ROOT = Path(r"Z:\AgrarianGameBulid")
|
||||||
|
TILE_ID = "gz_us_ca_pacifica_utm10n_e544_n4160"
|
||||||
|
METADATA_PATH = PROJECT_ROOT / "Data" / "Terrain" / "Unreal" / TILE_ID / f"{TILE_ID}_unreal_heightmap_metadata.json"
|
||||||
|
LANDSCAPE_LABEL = "AGR_GroundZero_Landscape"
|
||||||
|
|
||||||
|
|
||||||
|
def nearly_equal(left, right, tolerance=0.01):
|
||||||
|
return abs(float(left) - float(right)) <= tolerance
|
||||||
|
|
||||||
|
|
||||||
|
def get_actor_label(actor):
|
||||||
|
try:
|
||||||
|
return actor.get_actor_label()
|
||||||
|
except Exception:
|
||||||
|
return actor.get_name()
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if not unreal.EditorLevelLibrary.load_level(MAP_PATH):
|
||||||
|
raise RuntimeError(f"Could not load map: {MAP_PATH}")
|
||||||
|
|
||||||
|
with METADATA_PATH.open("r", encoding="utf-8") as metadata_file:
|
||||||
|
metadata = json.load(metadata_file)
|
||||||
|
|
||||||
|
expected = metadata["unreal_landscape_import"]
|
||||||
|
actors = unreal.EditorLevelLibrary.get_all_level_actors()
|
||||||
|
landscapes = [actor for actor in actors if get_actor_label(actor) == LANDSCAPE_LABEL]
|
||||||
|
if len(landscapes) != 1:
|
||||||
|
raise RuntimeError(f"Expected exactly one {LANDSCAPE_LABEL}, found {len(landscapes)}")
|
||||||
|
|
||||||
|
landscape = landscapes[0]
|
||||||
|
scale = landscape.get_actor_scale3d()
|
||||||
|
failures = []
|
||||||
|
if not nearly_equal(scale.x, expected["x_scale_cm"]):
|
||||||
|
failures.append(f"X scale expected {expected['x_scale_cm']}, got {scale.x}")
|
||||||
|
if not nearly_equal(scale.y, expected["y_scale_cm"]):
|
||||||
|
failures.append(f"Y scale expected {expected['y_scale_cm']}, got {scale.y}")
|
||||||
|
if not nearly_equal(scale.z, expected["z_scale_cm"]):
|
||||||
|
failures.append(f"Z scale expected {expected['z_scale_cm']}, got {scale.z}")
|
||||||
|
|
||||||
|
bounds_origin, bounds_extent = landscape.get_actor_bounds(False)
|
||||||
|
expected_extent = float(expected["tile_world_size_m"]) * 100.0 * 0.5
|
||||||
|
if not nearly_equal(bounds_extent.x, expected_extent, tolerance=150.0):
|
||||||
|
failures.append(f"X extent expected about {expected_extent}, got {bounds_extent.x}")
|
||||||
|
if not nearly_equal(bounds_extent.y, expected_extent, tolerance=150.0):
|
||||||
|
failures.append(f"Y extent expected about {expected_extent}, got {bounds_extent.y}")
|
||||||
|
if abs(bounds_origin.x) > 150.0 or abs(bounds_origin.y) > 150.0:
|
||||||
|
failures.append(f"Bounds origin expected near XY zero, got {bounds_origin}")
|
||||||
|
|
||||||
|
if failures:
|
||||||
|
raise RuntimeError("Ground Zero terrain verification failed: " + "; ".join(failures))
|
||||||
|
|
||||||
|
unreal.log(
|
||||||
|
"Ground Zero terrain verification complete: "
|
||||||
|
f"scale={scale}, bounds_origin={bounds_origin}, bounds_extent={bounds_extent}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
main()
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import unreal
|
||||||
|
|
||||||
|
|
||||||
|
ITEM_FOLDER = "/Game/Agrarian/DataAssets/Items"
|
||||||
|
|
||||||
|
EXPECTED_ITEMS = {
|
||||||
|
"DA_Item_Wood": ("wood", unreal.AgrarianItemType.RESOURCE),
|
||||||
|
"DA_Item_Stone": ("stone", unreal.AgrarianItemType.RESOURCE),
|
||||||
|
"DA_Item_Fiber": ("fiber", unreal.AgrarianItemType.RESOURCE),
|
||||||
|
"DA_Item_Food": ("food", unreal.AgrarianItemType.FOOD),
|
||||||
|
"DA_Item_Meat": ("meat", unreal.AgrarianItemType.FOOD),
|
||||||
|
"DA_Item_Hide": ("hide", unreal.AgrarianItemType.RESOURCE),
|
||||||
|
"DA_Item_PrimitiveFrame": ("primitive_frame", unreal.AgrarianItemType.STRUCTURE),
|
||||||
|
"DA_Item_PrimitiveWallPanel": ("primitive_wall_panel", unreal.AgrarianItemType.STRUCTURE),
|
||||||
|
"DA_Item_PrimitiveRoofPanel": ("primitive_roof_panel", unreal.AgrarianItemType.STRUCTURE),
|
||||||
|
"DA_Item_Campfire": ("campfire", unreal.AgrarianItemType.STRUCTURE),
|
||||||
|
"DA_Item_PrimitiveShelter": ("primitive_shelter", unreal.AgrarianItemType.STRUCTURE),
|
||||||
|
"DA_Item_BasicTool": ("basic_tool", unreal.AgrarianItemType.TOOL),
|
||||||
|
"DA_Item_Bandage": ("bandage", unreal.AgrarianItemType.MEDICINE),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
missing = []
|
||||||
|
|
||||||
|
for asset_name, (expected_item_id, expected_type) in EXPECTED_ITEMS.items():
|
||||||
|
path = f"{ITEM_FOLDER}/{asset_name}"
|
||||||
|
asset = unreal.EditorAssetLibrary.load_asset(path)
|
||||||
|
if not asset:
|
||||||
|
missing.append(f"{path} missing")
|
||||||
|
continue
|
||||||
|
|
||||||
|
definition = asset.get_editor_property("definition")
|
||||||
|
item_id = str(definition.get_editor_property("item_id"))
|
||||||
|
display_name = str(definition.get_editor_property("display_name"))
|
||||||
|
description = str(definition.get_editor_property("description"))
|
||||||
|
item_type = definition.get_editor_property("item_type")
|
||||||
|
unit_weight = definition.get_editor_property("unit_weight")
|
||||||
|
max_stack_size = definition.get_editor_property("max_stack_size")
|
||||||
|
|
||||||
|
if item_id != expected_item_id:
|
||||||
|
missing.append(f"{path} item_id expected {expected_item_id}, got {item_id}")
|
||||||
|
if item_type != expected_type:
|
||||||
|
missing.append(f"{path} type expected {expected_type}, got {item_type}")
|
||||||
|
if not display_name:
|
||||||
|
missing.append(f"{path} display_name empty")
|
||||||
|
if not description:
|
||||||
|
missing.append(f"{path} description empty")
|
||||||
|
if unit_weight <= 0.0:
|
||||||
|
missing.append(f"{path} unit_weight must be positive")
|
||||||
|
if max_stack_size < 1:
|
||||||
|
missing.append(f"{path} max_stack_size must be positive")
|
||||||
|
|
||||||
|
if missing:
|
||||||
|
raise RuntimeError("Item definition verification failed: " + "; ".join(missing))
|
||||||
|
|
||||||
|
unreal.log("Agrarian item definition verification complete.")
|
||||||
|
|
||||||
|
|
||||||
|
main()
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
import unreal
|
||||||
|
|
||||||
|
|
||||||
|
EXPECTED = {
|
||||||
|
"/Game/Agrarian/Blueprints/Resources/BP_WoodResourceNode": {
|
||||||
|
"properties": {
|
||||||
|
"remaining_harvests": 16,
|
||||||
|
"quantity_per_harvest": 2,
|
||||||
|
},
|
||||||
|
"yield_item_id": "wood",
|
||||||
|
},
|
||||||
|
"/Game/Agrarian/Blueprints/Resources/BP_FiberResourceNode": {
|
||||||
|
"properties": {
|
||||||
|
"remaining_harvests": 10,
|
||||||
|
"quantity_per_harvest": 3,
|
||||||
|
},
|
||||||
|
"yield_item_id": "fiber",
|
||||||
|
},
|
||||||
|
"/Game/Agrarian/Blueprints/Structures/BP_Campfire": {
|
||||||
|
"properties": {
|
||||||
|
"fuel_seconds": 180.0,
|
||||||
|
"warmth_radius": 650.0,
|
||||||
|
"warmth_per_second": 0.03,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"/Game/Agrarian/Blueprints/Structures/BP_PrimitiveShelter": {
|
||||||
|
"properties": {
|
||||||
|
"weather_protection": 0.7,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"/Game/Agrarian/Blueprints/Wildlife/BP_RabbitWildlife": {
|
||||||
|
"properties": {
|
||||||
|
"wildlife_id": "rabbit",
|
||||||
|
"max_health": 12.0,
|
||||||
|
"health": 12.0,
|
||||||
|
"wander_radius": 900.0,
|
||||||
|
"wander_speed": 160.0,
|
||||||
|
"flee_speed": 520.0,
|
||||||
|
"aggro_radius": 0.0,
|
||||||
|
"flee_radius": 750.0,
|
||||||
|
"decision_interval_seconds": 1.5,
|
||||||
|
},
|
||||||
|
"harvest_yield_ids": ["meat", "hide"],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def nearly_equal(left, right):
|
||||||
|
if isinstance(left, float) or isinstance(right, float):
|
||||||
|
return abs(float(left) - float(right)) < 0.001
|
||||||
|
return left == right
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
failures = []
|
||||||
|
|
||||||
|
for path, expected in EXPECTED.items():
|
||||||
|
blueprint = unreal.EditorAssetLibrary.load_asset(path)
|
||||||
|
if not blueprint:
|
||||||
|
failures.append(f"{path} missing")
|
||||||
|
continue
|
||||||
|
|
||||||
|
generated_class = unreal.EditorAssetLibrary.load_blueprint_class(path)
|
||||||
|
if not generated_class:
|
||||||
|
failures.append(f"{path} parent class mismatch")
|
||||||
|
continue
|
||||||
|
|
||||||
|
cdo = unreal.get_default_object(generated_class)
|
||||||
|
for property_name, expected_value in expected.get("properties", {}).items():
|
||||||
|
actual_value = cdo.get_editor_property(property_name)
|
||||||
|
if str(actual_value) != expected_value and not nearly_equal(actual_value, expected_value):
|
||||||
|
failures.append(f"{path} {property_name} expected {expected_value}, got {actual_value}")
|
||||||
|
|
||||||
|
expected_yield_item_id = expected.get("yield_item_id")
|
||||||
|
if expected_yield_item_id:
|
||||||
|
item_asset = cdo.get_editor_property("yield_item_definition")
|
||||||
|
definition = item_asset.get_editor_property("definition") if item_asset else None
|
||||||
|
item_id = str(definition.get_editor_property("item_id")) if definition else ""
|
||||||
|
if item_id != expected_yield_item_id:
|
||||||
|
failures.append(f"{path} yield_item_definition expected {expected_yield_item_id}, got {item_id}")
|
||||||
|
|
||||||
|
expected_harvest_ids = expected.get("harvest_yield_ids")
|
||||||
|
if expected_harvest_ids:
|
||||||
|
harvest_yields = cdo.get_editor_property("harvest_yields")
|
||||||
|
actual_ids = [str(stack.get_editor_property("item_id")) for stack in harvest_yields]
|
||||||
|
if actual_ids != expected_harvest_ids:
|
||||||
|
failures.append(f"{path} harvest_yields expected {expected_harvest_ids}, got {actual_ids}")
|
||||||
|
|
||||||
|
if failures:
|
||||||
|
raise RuntimeError("Playable Blueprint verification failed: " + "; ".join(failures))
|
||||||
|
|
||||||
|
unreal.log("Agrarian playable Blueprint verification complete.")
|
||||||
|
|
||||||
|
|
||||||
|
main()
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import unreal
|
||||||
|
|
||||||
|
|
||||||
|
MAP_PATH = "/Game/ThirdPerson/Lvl_ThirdPerson"
|
||||||
|
CHARACTER_CLASS_PATH = "/Game/ThirdPerson/Blueprints/BP_ThirdPersonCharacter"
|
||||||
|
SHELTER_RECIPE_PATH = "/Game/Agrarian/DataAssets/Recipes/DA_Recipe_PrimitiveShelter"
|
||||||
|
FRAME_RECIPE_PATH = "/Game/Agrarian/DataAssets/Recipes/DA_Recipe_PrimitiveFrame"
|
||||||
|
WALL_PANEL_RECIPE_PATH = "/Game/Agrarian/DataAssets/Recipes/DA_Recipe_PrimitiveWallPanel"
|
||||||
|
ROOF_PANEL_RECIPE_PATH = "/Game/Agrarian/DataAssets/Recipes/DA_Recipe_PrimitiveRoofPanel"
|
||||||
|
SHELTER_CLASS_PATH = "/Game/Agrarian/Blueprints/Structures/BP_PrimitiveShelter"
|
||||||
|
WOOD_NODE_LABEL = "AGR_WoodResourceNode_01"
|
||||||
|
FIBER_NODE_LABEL = "AGR_FiberResourceNode_01"
|
||||||
|
RABBIT_LABEL = "AGR_RabbitWildlife_01"
|
||||||
|
|
||||||
|
|
||||||
|
def get_actor_label(actor):
|
||||||
|
try:
|
||||||
|
return actor.get_actor_label()
|
||||||
|
except Exception:
|
||||||
|
return actor.get_name()
|
||||||
|
|
||||||
|
|
||||||
|
def load_blueprint_class(path):
|
||||||
|
generated_class = unreal.EditorAssetLibrary.load_blueprint_class(path)
|
||||||
|
if not generated_class:
|
||||||
|
raise RuntimeError(f"Could not load Blueprint class: {path}")
|
||||||
|
return generated_class
|
||||||
|
|
||||||
|
|
||||||
|
def find_actor_by_label(label):
|
||||||
|
for actor in unreal.EditorLevelLibrary.get_all_level_actors():
|
||||||
|
if get_actor_label(actor) == label:
|
||||||
|
return actor
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if not unreal.EditorLevelLibrary.load_level(MAP_PATH):
|
||||||
|
raise RuntimeError(f"Could not load map: {MAP_PATH}")
|
||||||
|
|
||||||
|
character_class = load_blueprint_class(CHARACTER_CLASS_PATH)
|
||||||
|
shelter_class = load_blueprint_class(SHELTER_CLASS_PATH)
|
||||||
|
shelter_recipe = unreal.EditorAssetLibrary.load_asset(SHELTER_RECIPE_PATH)
|
||||||
|
if not shelter_recipe:
|
||||||
|
raise RuntimeError(f"Could not load shelter recipe: {SHELTER_RECIPE_PATH}")
|
||||||
|
frame_recipe = unreal.EditorAssetLibrary.load_asset(FRAME_RECIPE_PATH)
|
||||||
|
if not frame_recipe:
|
||||||
|
raise RuntimeError(f"Could not load frame recipe: {FRAME_RECIPE_PATH}")
|
||||||
|
wall_panel_recipe = unreal.EditorAssetLibrary.load_asset(WALL_PANEL_RECIPE_PATH)
|
||||||
|
if not wall_panel_recipe:
|
||||||
|
raise RuntimeError(f"Could not load wall panel recipe: {WALL_PANEL_RECIPE_PATH}")
|
||||||
|
roof_panel_recipe = unreal.EditorAssetLibrary.load_asset(ROOF_PANEL_RECIPE_PATH)
|
||||||
|
if not roof_panel_recipe:
|
||||||
|
raise RuntimeError(f"Could not load roof panel recipe: {ROOF_PANEL_RECIPE_PATH}")
|
||||||
|
|
||||||
|
wood_node = find_actor_by_label(WOOD_NODE_LABEL)
|
||||||
|
if not wood_node:
|
||||||
|
raise RuntimeError(f"Could not find placed wood node: {WOOD_NODE_LABEL}")
|
||||||
|
fiber_node = find_actor_by_label(FIBER_NODE_LABEL)
|
||||||
|
if not fiber_node:
|
||||||
|
raise RuntimeError(f"Could not find placed fiber node: {FIBER_NODE_LABEL}")
|
||||||
|
rabbit = find_actor_by_label(RABBIT_LABEL)
|
||||||
|
if not rabbit:
|
||||||
|
raise RuntimeError(f"Could not find placed rabbit wildlife: {RABBIT_LABEL}")
|
||||||
|
|
||||||
|
result = unreal.AgrarianEditorAutomationLibrary.run_natural_shelter_loop_smoke_test(
|
||||||
|
character_class,
|
||||||
|
wood_node,
|
||||||
|
fiber_node,
|
||||||
|
rabbit,
|
||||||
|
frame_recipe,
|
||||||
|
wall_panel_recipe,
|
||||||
|
roof_panel_recipe,
|
||||||
|
shelter_recipe,
|
||||||
|
shelter_class,
|
||||||
|
)
|
||||||
|
unreal.log(result)
|
||||||
|
if not str(result).startswith("PASS:"):
|
||||||
|
raise RuntimeError(result)
|
||||||
|
|
||||||
|
unreal.log("Agrarian playable loop smoke verification complete.")
|
||||||
|
|
||||||
|
|
||||||
|
main()
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
import unreal
|
||||||
|
|
||||||
|
|
||||||
|
RECIPE_FOLDER = "/Game/Agrarian/DataAssets/Recipes"
|
||||||
|
|
||||||
|
EXPECTED_RECIPES = {
|
||||||
|
"DA_Recipe_Campfire": {
|
||||||
|
"recipe_id": "campfire",
|
||||||
|
"ingredients": {"wood": 5, "stone": 8, "fiber": 2},
|
||||||
|
"result": ("campfire", 1),
|
||||||
|
},
|
||||||
|
"DA_Recipe_PrimitiveShelter": {
|
||||||
|
"recipe_id": "primitive_shelter",
|
||||||
|
"ingredients": {
|
||||||
|
"primitive_frame": 2,
|
||||||
|
"primitive_wall_panel": 4,
|
||||||
|
"primitive_roof_panel": 2,
|
||||||
|
"hide": 2,
|
||||||
|
"fiber": 6,
|
||||||
|
},
|
||||||
|
"result": ("primitive_shelter", 1),
|
||||||
|
},
|
||||||
|
"DA_Recipe_PrimitiveFrame": {
|
||||||
|
"recipe_id": "primitive_frame",
|
||||||
|
"ingredients": {"wood": 4, "fiber": 2},
|
||||||
|
"result": ("primitive_frame", 1),
|
||||||
|
},
|
||||||
|
"DA_Recipe_PrimitiveWallPanel": {
|
||||||
|
"recipe_id": "primitive_wall_panel",
|
||||||
|
"ingredients": {"wood": 3, "fiber": 2},
|
||||||
|
"result": ("primitive_wall_panel", 1),
|
||||||
|
},
|
||||||
|
"DA_Recipe_PrimitiveRoofPanel": {
|
||||||
|
"recipe_id": "primitive_roof_panel",
|
||||||
|
"ingredients": {"wood": 3, "fiber": 3},
|
||||||
|
"result": ("primitive_roof_panel", 1),
|
||||||
|
},
|
||||||
|
"DA_Recipe_BasicTool": {
|
||||||
|
"recipe_id": "basic_tool",
|
||||||
|
"ingredients": {"wood": 1, "stone": 2, "fiber": 1},
|
||||||
|
"result": ("basic_tool", 1),
|
||||||
|
},
|
||||||
|
"DA_Recipe_Bandage": {
|
||||||
|
"recipe_id": "bandage",
|
||||||
|
"ingredients": {"fiber": 3, "hide": 1},
|
||||||
|
"result": ("bandage", 1),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def stack_item_id(stack):
|
||||||
|
return str(stack.get_editor_property("item_id"))
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
missing = []
|
||||||
|
|
||||||
|
for asset_name, expected in EXPECTED_RECIPES.items():
|
||||||
|
path = f"{RECIPE_FOLDER}/{asset_name}"
|
||||||
|
asset = unreal.EditorAssetLibrary.load_asset(path)
|
||||||
|
if not asset:
|
||||||
|
missing.append(f"{path} missing")
|
||||||
|
continue
|
||||||
|
|
||||||
|
recipe = asset.get_editor_property("recipe")
|
||||||
|
recipe_id = str(recipe.get_editor_property("recipe_id"))
|
||||||
|
display_name = str(recipe.get_editor_property("display_name"))
|
||||||
|
craft_seconds = recipe.get_editor_property("craft_seconds")
|
||||||
|
result = recipe.get_editor_property("result")
|
||||||
|
ingredients = list(recipe.get_editor_property("ingredients"))
|
||||||
|
|
||||||
|
if recipe_id != expected["recipe_id"]:
|
||||||
|
missing.append(f"{path} recipe_id expected {expected['recipe_id']}, got {recipe_id}")
|
||||||
|
if not display_name:
|
||||||
|
missing.append(f"{path} display_name empty")
|
||||||
|
if craft_seconds <= 0.0:
|
||||||
|
missing.append(f"{path} craft_seconds must be positive")
|
||||||
|
|
||||||
|
expected_result_id, expected_result_quantity = expected["result"]
|
||||||
|
if stack_item_id(result) != expected_result_id:
|
||||||
|
missing.append(f"{path} result id expected {expected_result_id}, got {stack_item_id(result)}")
|
||||||
|
if result.get_editor_property("quantity") != expected_result_quantity:
|
||||||
|
missing.append(f"{path} result quantity expected {expected_result_quantity}")
|
||||||
|
if result.get_editor_property("unit_weight") <= 0.0:
|
||||||
|
missing.append(f"{path} result unit weight must be positive")
|
||||||
|
|
||||||
|
actual_ingredients = {stack_item_id(stack): stack for stack in ingredients}
|
||||||
|
for expected_item_id, expected_quantity in expected["ingredients"].items():
|
||||||
|
stack = actual_ingredients.get(expected_item_id)
|
||||||
|
if not stack:
|
||||||
|
missing.append(f"{path} missing ingredient {expected_item_id}")
|
||||||
|
continue
|
||||||
|
if stack.get_editor_property("quantity") != expected_quantity:
|
||||||
|
missing.append(f"{path} ingredient {expected_item_id} expected quantity {expected_quantity}")
|
||||||
|
if stack.get_editor_property("unit_weight") <= 0.0:
|
||||||
|
missing.append(f"{path} ingredient {expected_item_id} unit weight must be positive")
|
||||||
|
|
||||||
|
unexpected = set(actual_ingredients.keys()) - set(expected["ingredients"].keys())
|
||||||
|
if unexpected:
|
||||||
|
missing.append(f"{path} has unexpected ingredients: {sorted(unexpected)}")
|
||||||
|
|
||||||
|
if missing:
|
||||||
|
raise RuntimeError("Recipe definition verification failed: " + "; ".join(missing))
|
||||||
|
|
||||||
|
unreal.log("Agrarian recipe definition verification complete.")
|
||||||
|
|
||||||
|
|
||||||
|
main()
|
||||||
@@ -8,10 +8,18 @@ EXPECTED_PLACEMENTS = {
|
|||||||
"class_path": "/Game/Agrarian/Blueprints/Resources/BP_WoodResourceNode",
|
"class_path": "/Game/Agrarian/Blueprints/Resources/BP_WoodResourceNode",
|
||||||
"location": unreal.Vector(650.0, -150.0, 120.0),
|
"location": unreal.Vector(650.0, -150.0, 120.0),
|
||||||
"properties": {
|
"properties": {
|
||||||
"remaining_harvests": 6,
|
"remaining_harvests": 16,
|
||||||
"quantity_per_harvest": 2,
|
"quantity_per_harvest": 2,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"AGR_FiberResourceNode_01": {
|
||||||
|
"class_path": "/Game/Agrarian/Blueprints/Resources/BP_FiberResourceNode",
|
||||||
|
"location": unreal.Vector(560.0, 140.0, 90.0),
|
||||||
|
"properties": {
|
||||||
|
"remaining_harvests": 10,
|
||||||
|
"quantity_per_harvest": 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
"AGR_Campfire_01": {
|
"AGR_Campfire_01": {
|
||||||
"class_path": "/Game/Agrarian/Blueprints/Structures/BP_Campfire",
|
"class_path": "/Game/Agrarian/Blueprints/Structures/BP_Campfire",
|
||||||
"location": unreal.Vector(900.0, 120.0, 60.0),
|
"location": unreal.Vector(900.0, 120.0, 60.0),
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
import unreal
|
||||||
|
|
||||||
|
|
||||||
|
MAP_PATH = "/Game/ThirdPerson/Lvl_ThirdPerson"
|
||||||
|
CHARACTER_CLASS_PATH = "/Game/ThirdPerson/Blueprints/BP_ThirdPersonCharacter"
|
||||||
|
RABBIT_LABEL = "AGR_RabbitWildlife_01"
|
||||||
|
|
||||||
|
|
||||||
|
def get_actor_label(actor):
|
||||||
|
try:
|
||||||
|
return actor.get_actor_label()
|
||||||
|
except Exception:
|
||||||
|
return actor.get_name()
|
||||||
|
|
||||||
|
|
||||||
|
def load_blueprint_class(path):
|
||||||
|
generated_class = unreal.EditorAssetLibrary.load_blueprint_class(path)
|
||||||
|
if not generated_class:
|
||||||
|
raise RuntimeError(f"Could not load Blueprint class: {path}")
|
||||||
|
return generated_class
|
||||||
|
|
||||||
|
|
||||||
|
def find_actor_by_label(label):
|
||||||
|
for actor in unreal.EditorLevelLibrary.get_all_level_actors():
|
||||||
|
if get_actor_label(actor) == label:
|
||||||
|
return actor
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def enum_name(value):
|
||||||
|
return str(value).split(".")[-1].lower()
|
||||||
|
|
||||||
|
|
||||||
|
def get_bool_property(actor, *property_names):
|
||||||
|
for property_name in property_names:
|
||||||
|
try:
|
||||||
|
return bool(actor.get_editor_property(property_name))
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
raise RuntimeError(f"Could not read any bool property from {property_names}")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if not unreal.EditorLevelLibrary.load_level(MAP_PATH):
|
||||||
|
raise RuntimeError(f"Could not load map: {MAP_PATH}")
|
||||||
|
|
||||||
|
character_class = load_blueprint_class(CHARACTER_CLASS_PATH)
|
||||||
|
rabbit = find_actor_by_label(RABBIT_LABEL)
|
||||||
|
if not rabbit:
|
||||||
|
raise RuntimeError(f"Could not find placed rabbit wildlife: {RABBIT_LABEL}")
|
||||||
|
|
||||||
|
character = unreal.AgrarianEditorAutomationLibrary.spawn_actor_in_editor_world(
|
||||||
|
character_class,
|
||||||
|
unreal.Vector(500.0, 320.0, 180.0),
|
||||||
|
unreal.Rotator(0.0, 0.0, 0.0),
|
||||||
|
"AGR_AutomationWildlifeCharacter",
|
||||||
|
)
|
||||||
|
if not character:
|
||||||
|
raise RuntimeError("Could not spawn automation character")
|
||||||
|
|
||||||
|
inventory = character.get_component_by_class(unreal.AgrarianInventoryComponent)
|
||||||
|
if not inventory:
|
||||||
|
raise RuntimeError("Automation character is missing AgrarianInventoryComponent")
|
||||||
|
|
||||||
|
starting_health = float(rabbit.get_editor_property("health"))
|
||||||
|
max_health = float(rabbit.get_editor_property("max_health"))
|
||||||
|
if starting_health <= 0.0 or max_health <= 0.0:
|
||||||
|
raise RuntimeError(f"Rabbit starts invalid health={starting_health}, max_health={max_health}")
|
||||||
|
|
||||||
|
rabbit.apply_wildlife_damage(1.0, character)
|
||||||
|
damaged_health = float(rabbit.get_editor_property("health"))
|
||||||
|
damaged_state = enum_name(rabbit.get_editor_property("wildlife_state"))
|
||||||
|
if not damaged_health < starting_health:
|
||||||
|
raise RuntimeError(f"Rabbit non-lethal damage did not reduce health: {starting_health} -> {damaged_health}")
|
||||||
|
if "fleeing" not in damaged_state:
|
||||||
|
raise RuntimeError(f"Rabbit expected fleeing after non-lethal damage, got {damaged_state}")
|
||||||
|
|
||||||
|
rabbit.apply_wildlife_damage(max_health + 10.0, character)
|
||||||
|
dead_health = float(rabbit.get_editor_property("health"))
|
||||||
|
dead_state = enum_name(rabbit.get_editor_property("wildlife_state"))
|
||||||
|
if dead_health != 0.0:
|
||||||
|
raise RuntimeError(f"Rabbit lethal damage expected health 0, got {dead_health}")
|
||||||
|
if "dead" not in dead_state:
|
||||||
|
raise RuntimeError(f"Rabbit expected dead after lethal damage, got {dead_state}")
|
||||||
|
if not rabbit.can_interact(character):
|
||||||
|
raise RuntimeError("Rabbit should be harvestable after death")
|
||||||
|
|
||||||
|
meat_before = inventory.get_item_count("meat")
|
||||||
|
hide_before = inventory.get_item_count("hide")
|
||||||
|
rabbit.interact(character)
|
||||||
|
meat_after = inventory.get_item_count("meat")
|
||||||
|
hide_after = inventory.get_item_count("hide")
|
||||||
|
if meat_after <= meat_before:
|
||||||
|
raise RuntimeError(f"Rabbit harvest did not add meat: {meat_before} -> {meat_after}")
|
||||||
|
if hide_after <= hide_before:
|
||||||
|
raise RuntimeError(f"Rabbit harvest did not add hide: {hide_before} -> {hide_after}")
|
||||||
|
if not get_bool_property(rabbit, "b_harvested", "harvested"):
|
||||||
|
raise RuntimeError("Rabbit harvest did not set bHarvested")
|
||||||
|
if rabbit.can_interact(character):
|
||||||
|
raise RuntimeError("Rabbit should not be harvestable twice")
|
||||||
|
|
||||||
|
unreal.EditorLevelLibrary.destroy_actor(character)
|
||||||
|
unreal.log(
|
||||||
|
"PASS: wildlife damage/death/harvest verified "
|
||||||
|
f"health {starting_health}->{damaged_health}->0, "
|
||||||
|
f"meat {meat_before}->{meat_after}, hide {hide_before}->{hide_after}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
main()
|
||||||
@@ -2,12 +2,58 @@
|
|||||||
|
|
||||||
#include "AgrarianEditorAutomationLibrary.h"
|
#include "AgrarianEditorAutomationLibrary.h"
|
||||||
|
|
||||||
|
#include "AgrarianBuildingPlacementComponent.h"
|
||||||
|
#include "AgrarianCraftingComponent.h"
|
||||||
|
#include "AgrarianGameCharacter.h"
|
||||||
|
#include "AgrarianInteractable.h"
|
||||||
|
#include "AgrarianInventoryComponent.h"
|
||||||
|
#include "AgrarianPersistentActorComponent.h"
|
||||||
|
#include "AgrarianPersistenceSubsystem.h"
|
||||||
|
#include "AgrarianRecipeDataAsset.h"
|
||||||
|
#include "AgrarianResourceNode.h"
|
||||||
|
#include "AgrarianSaveGame.h"
|
||||||
|
#include "AgrarianWildlifeBase.h"
|
||||||
#include "Engine/World.h"
|
#include "Engine/World.h"
|
||||||
|
#include "EngineUtils.h"
|
||||||
|
#include "Kismet/GameplayStatics.h"
|
||||||
|
#include "Misc/FileHelper.h"
|
||||||
|
|
||||||
#if WITH_EDITOR
|
#if WITH_EDITOR
|
||||||
#include "Editor.h"
|
#include "Editor.h"
|
||||||
|
#include "Landscape.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
int32 GetIngredientQuantity(const UAgrarianRecipeDataAsset* RecipeAsset, const FName ItemId)
|
||||||
|
{
|
||||||
|
if (!RecipeAsset)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const FAgrarianItemStack& Ingredient : RecipeAsset->Recipe.Ingredients)
|
||||||
|
{
|
||||||
|
if (Ingredient.ItemId == ItemId)
|
||||||
|
{
|
||||||
|
return Ingredient.Quantity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AddNeededIngredient(TMap<FName, int32>& NeededItems, const FName ItemId, const int32 Quantity)
|
||||||
|
{
|
||||||
|
if (ItemId == NAME_None || Quantity <= 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
NeededItems.FindOrAdd(ItemId) += Quantity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
AActor* UAgrarianEditorAutomationLibrary::SpawnActorInEditorWorld(TSubclassOf<AActor> ActorClass, const FVector& Location, const FRotator& Rotation, const FString& ActorLabel)
|
AActor* UAgrarianEditorAutomationLibrary::SpawnActorInEditorWorld(TSubclassOf<AActor> ActorClass, const FVector& Location, const FRotator& Rotation, const FString& ActorLabel)
|
||||||
{
|
{
|
||||||
#if WITH_EDITOR
|
#if WITH_EDITOR
|
||||||
@@ -44,3 +90,745 @@ AActor* UAgrarianEditorAutomationLibrary::SpawnActorInEditorWorld(TSubclassOf<AA
|
|||||||
return nullptr;
|
return nullptr;
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FString UAgrarianEditorAutomationLibrary::RunPlayableLoopSmokeTest(TSubclassOf<AAgrarianGameCharacter> CharacterClass, AAgrarianResourceNode* ResourceNode, UAgrarianRecipeDataAsset* ShelterRecipe, TSubclassOf<AActor> ShelterClass)
|
||||||
|
{
|
||||||
|
#if WITH_EDITOR
|
||||||
|
UWorld* EditorWorld = GEditor ? GEditor->GetEditorWorldContext().World() : nullptr;
|
||||||
|
if (!EditorWorld)
|
||||||
|
{
|
||||||
|
return TEXT("FAIL: no editor world");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!CharacterClass || !ResourceNode || !ShelterRecipe || !ShelterClass)
|
||||||
|
{
|
||||||
|
return FString::Printf(
|
||||||
|
TEXT("FAIL: missing input CharacterClass=%s ResourceNode=%s ShelterRecipe=%s ShelterClass=%s"),
|
||||||
|
*GetNameSafe(CharacterClass.Get()),
|
||||||
|
*GetNameSafe(ResourceNode),
|
||||||
|
*GetNameSafe(ShelterRecipe),
|
||||||
|
*GetNameSafe(ShelterClass.Get()));
|
||||||
|
}
|
||||||
|
|
||||||
|
FActorSpawnParameters CharacterSpawnParams;
|
||||||
|
CharacterSpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
|
||||||
|
|
||||||
|
AAgrarianGameCharacter* TestCharacter = EditorWorld->SpawnActor<AAgrarianGameCharacter>(
|
||||||
|
CharacterClass,
|
||||||
|
FVector(700.0f, -450.0f, 180.0f),
|
||||||
|
FRotator::ZeroRotator,
|
||||||
|
CharacterSpawnParams);
|
||||||
|
|
||||||
|
if (!TestCharacter)
|
||||||
|
{
|
||||||
|
return TEXT("FAIL: could not spawn test character");
|
||||||
|
}
|
||||||
|
|
||||||
|
TestCharacter->SetActorLabel(TEXT("AGR_AutomationLoopCharacter"), false);
|
||||||
|
|
||||||
|
UAgrarianInventoryComponent* Inventory = TestCharacter->GetInventoryComponent();
|
||||||
|
UAgrarianCraftingComponent* Crafting = TestCharacter->GetCraftingComponent();
|
||||||
|
UAgrarianBuildingPlacementComponent* Placement = TestCharacter->GetBuildingPlacementComponent();
|
||||||
|
|
||||||
|
if (!Inventory || !Crafting || !Placement)
|
||||||
|
{
|
||||||
|
TestCharacter->Destroy();
|
||||||
|
return TEXT("FAIL: spawned character is missing inventory, crafting, or placement component");
|
||||||
|
}
|
||||||
|
|
||||||
|
const int32 StartingWood = Inventory->GetItemCount(TEXT("wood"));
|
||||||
|
if (!ResourceNode->GetClass()->ImplementsInterface(UAgrarianInteractable::StaticClass()))
|
||||||
|
{
|
||||||
|
TestCharacter->Destroy();
|
||||||
|
return TEXT("FAIL: resource node does not implement AgrarianInteractable");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!IAgrarianInteractable::Execute_CanInteract(ResourceNode, TestCharacter))
|
||||||
|
{
|
||||||
|
TestCharacter->Destroy();
|
||||||
|
return TEXT("FAIL: resource node cannot be gathered");
|
||||||
|
}
|
||||||
|
|
||||||
|
IAgrarianInteractable::Execute_Interact(ResourceNode, TestCharacter);
|
||||||
|
|
||||||
|
const int32 WoodAfterGather = Inventory->GetItemCount(TEXT("wood"));
|
||||||
|
if (WoodAfterGather <= StartingWood)
|
||||||
|
{
|
||||||
|
TestCharacter->Destroy();
|
||||||
|
return FString::Printf(TEXT("FAIL: gather did not add wood, before=%d after=%d"), StartingWood, WoodAfterGather);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The current shelter recipe depends on prototype structure parts that do not yet have world acquisition.
|
||||||
|
// Seed only the missing recipe inputs so this smoke test can exercise the connected craft/place/save/load path.
|
||||||
|
for (const FAgrarianItemStack& Ingredient : ShelterRecipe->Recipe.Ingredients)
|
||||||
|
{
|
||||||
|
const int32 CurrentCount = Inventory->GetItemCount(Ingredient.ItemId);
|
||||||
|
if (CurrentCount < Ingredient.Quantity)
|
||||||
|
{
|
||||||
|
FAgrarianItemStack MissingStack = Ingredient;
|
||||||
|
MissingStack.Quantity = Ingredient.Quantity - CurrentCount;
|
||||||
|
if (!Inventory->AddItem(MissingStack))
|
||||||
|
{
|
||||||
|
TestCharacter->Destroy();
|
||||||
|
return FString::Printf(TEXT("FAIL: could not seed missing ingredient %s"), *Ingredient.ItemId.ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Crafting->AddKnownRecipe(ShelterRecipe->Recipe))
|
||||||
|
{
|
||||||
|
TestCharacter->Destroy();
|
||||||
|
return TEXT("FAIL: could not add primitive shelter recipe to test character");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Crafting->Craft(ShelterRecipe->Recipe.RecipeId))
|
||||||
|
{
|
||||||
|
TestCharacter->Destroy();
|
||||||
|
return TEXT("FAIL: primitive shelter craft failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Inventory->GetItemCount(TEXT("primitive_shelter")) < 1)
|
||||||
|
{
|
||||||
|
TestCharacter->Destroy();
|
||||||
|
return TEXT("FAIL: crafting did not add primitive_shelter to inventory");
|
||||||
|
}
|
||||||
|
|
||||||
|
TArray<FAgrarianItemStack> PlacementCost;
|
||||||
|
FAgrarianItemStack ShelterCost = ShelterRecipe->Recipe.Result;
|
||||||
|
ShelterCost.Quantity = 1;
|
||||||
|
PlacementCost.Add(ShelterCost);
|
||||||
|
Placement->SetActiveBuildable(ShelterClass, PlacementCost);
|
||||||
|
Placement->PlacementDistance = 5000.0f;
|
||||||
|
Placement->PlacementProbeRadius = 1.0f;
|
||||||
|
|
||||||
|
const int32 ShelterCountBeforePlace = Inventory->GetItemCount(TEXT("primitive_shelter"));
|
||||||
|
int32 ShelterActorsBeforePlace = 0;
|
||||||
|
for (TActorIterator<AActor> ActorIt(EditorWorld, ShelterClass); ActorIt; ++ActorIt)
|
||||||
|
{
|
||||||
|
ShelterActorsBeforePlace++;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FTransform PlacementTransform(FRotator::ZeroRotator, FVector(1500.0f, 550.0f, 300.0f));
|
||||||
|
Placement->ServerPlaceBuildable_Implementation(ShelterClass, PlacementTransform);
|
||||||
|
|
||||||
|
const int32 ShelterCountAfterPlace = Inventory->GetItemCount(TEXT("primitive_shelter"));
|
||||||
|
int32 ShelterActorsAfterPlace = 0;
|
||||||
|
AActor* PlacedShelter = nullptr;
|
||||||
|
for (TActorIterator<AActor> ActorIt(EditorWorld, ShelterClass); ActorIt; ++ActorIt)
|
||||||
|
{
|
||||||
|
ShelterActorsAfterPlace++;
|
||||||
|
PlacedShelter = *ActorIt;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ShelterActorsAfterPlace <= ShelterActorsBeforePlace)
|
||||||
|
{
|
||||||
|
TestCharacter->Destroy();
|
||||||
|
return FString::Printf(TEXT("FAIL: placement did not spawn shelter, before=%d after=%d"), ShelterActorsBeforePlace, ShelterActorsAfterPlace);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ShelterCountAfterPlace >= ShelterCountBeforePlace)
|
||||||
|
{
|
||||||
|
TestCharacter->Destroy();
|
||||||
|
return FString::Printf(TEXT("FAIL: placement did not consume shelter item, before=%d after=%d"), ShelterCountBeforePlace, ShelterCountAfterPlace);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!PlacedShelter || !PlacedShelter->FindComponentByClass<UAgrarianPersistentActorComponent>())
|
||||||
|
{
|
||||||
|
TestCharacter->Destroy();
|
||||||
|
return TEXT("FAIL: placed shelter is missing persistent actor component");
|
||||||
|
}
|
||||||
|
|
||||||
|
TArray<FAgrarianSavedWorldActor> SavedActors;
|
||||||
|
for (TActorIterator<AActor> ActorIt(EditorWorld); ActorIt; ++ActorIt)
|
||||||
|
{
|
||||||
|
if (UAgrarianPersistentActorComponent* PersistentComponent = ActorIt->FindComponentByClass<UAgrarianPersistentActorComponent>())
|
||||||
|
{
|
||||||
|
if (PersistentComponent->IsSaveable())
|
||||||
|
{
|
||||||
|
SavedActors.Add(PersistentComponent->CaptureSaveState());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (SavedActors.IsEmpty())
|
||||||
|
{
|
||||||
|
TestCharacter->Destroy();
|
||||||
|
return TEXT("FAIL: save capture did not include any persistent world actors");
|
||||||
|
}
|
||||||
|
|
||||||
|
int32 RestoredActorCount = 0;
|
||||||
|
for (const FAgrarianSavedWorldActor& SavedActor : SavedActors)
|
||||||
|
{
|
||||||
|
if (SavedActor.ActorTypeId != TEXT("primitive_shelter"))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
AActor* RestoredActor = EditorWorld->SpawnActor<AActor>(ShelterClass, SavedActor.Transform);
|
||||||
|
if (!RestoredActor)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (UAgrarianPersistentActorComponent* PersistentComponent = RestoredActor->FindComponentByClass<UAgrarianPersistentActorComponent>())
|
||||||
|
{
|
||||||
|
PersistentComponent->ApplySaveState(SavedActor);
|
||||||
|
}
|
||||||
|
|
||||||
|
RestoredActorCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (RestoredActorCount <= 0)
|
||||||
|
{
|
||||||
|
TestCharacter->Destroy();
|
||||||
|
return TEXT("FAIL: restore did not spawn any saved primitive shelter actors");
|
||||||
|
}
|
||||||
|
|
||||||
|
TestCharacter->Destroy();
|
||||||
|
return FString::Printf(
|
||||||
|
TEXT("PASS: gathered wood %d->%d, crafted primitive_shelter, placed shelter, saved %d persistent actor(s), restored %d actor(s)"),
|
||||||
|
StartingWood,
|
||||||
|
WoodAfterGather,
|
||||||
|
SavedActors.Num(),
|
||||||
|
RestoredActorCount);
|
||||||
|
#else
|
||||||
|
return TEXT("FAIL: editor automation is only available in editor builds");
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
FString UAgrarianEditorAutomationLibrary::RunNaturalShelterLoopSmokeTest(
|
||||||
|
TSubclassOf<AAgrarianGameCharacter> CharacterClass,
|
||||||
|
AAgrarianResourceNode* WoodNode,
|
||||||
|
AAgrarianResourceNode* FiberNode,
|
||||||
|
AAgrarianWildlifeBase* Wildlife,
|
||||||
|
UAgrarianRecipeDataAsset* FrameRecipe,
|
||||||
|
UAgrarianRecipeDataAsset* WallPanelRecipe,
|
||||||
|
UAgrarianRecipeDataAsset* RoofPanelRecipe,
|
||||||
|
UAgrarianRecipeDataAsset* ShelterRecipe,
|
||||||
|
TSubclassOf<AActor> ShelterClass)
|
||||||
|
{
|
||||||
|
#if WITH_EDITOR
|
||||||
|
UWorld* EditorWorld = GEditor ? GEditor->GetEditorWorldContext().World() : nullptr;
|
||||||
|
if (!EditorWorld)
|
||||||
|
{
|
||||||
|
return TEXT("FAIL: no editor world");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!CharacterClass || !WoodNode || !FiberNode || !Wildlife || !FrameRecipe || !WallPanelRecipe || !RoofPanelRecipe || !ShelterRecipe || !ShelterClass)
|
||||||
|
{
|
||||||
|
return FString::Printf(
|
||||||
|
TEXT("FAIL: missing input CharacterClass=%s WoodNode=%s FiberNode=%s Wildlife=%s FrameRecipe=%s WallPanelRecipe=%s RoofPanelRecipe=%s ShelterRecipe=%s ShelterClass=%s"),
|
||||||
|
*GetNameSafe(CharacterClass.Get()),
|
||||||
|
*GetNameSafe(WoodNode),
|
||||||
|
*GetNameSafe(FiberNode),
|
||||||
|
*GetNameSafe(Wildlife),
|
||||||
|
*GetNameSafe(FrameRecipe),
|
||||||
|
*GetNameSafe(WallPanelRecipe),
|
||||||
|
*GetNameSafe(RoofPanelRecipe),
|
||||||
|
*GetNameSafe(ShelterRecipe),
|
||||||
|
*GetNameSafe(ShelterClass.Get()));
|
||||||
|
}
|
||||||
|
|
||||||
|
FActorSpawnParameters CharacterSpawnParams;
|
||||||
|
CharacterSpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
|
||||||
|
|
||||||
|
AAgrarianGameCharacter* TestCharacter = EditorWorld->SpawnActor<AAgrarianGameCharacter>(
|
||||||
|
CharacterClass,
|
||||||
|
FVector(720.0f, -420.0f, 180.0f),
|
||||||
|
FRotator::ZeroRotator,
|
||||||
|
CharacterSpawnParams);
|
||||||
|
|
||||||
|
if (!TestCharacter)
|
||||||
|
{
|
||||||
|
return TEXT("FAIL: could not spawn test character");
|
||||||
|
}
|
||||||
|
|
||||||
|
TestCharacter->SetActorLabel(TEXT("AGR_AutomationNaturalShelterCharacter"), false);
|
||||||
|
|
||||||
|
UAgrarianInventoryComponent* Inventory = TestCharacter->GetInventoryComponent();
|
||||||
|
UAgrarianCraftingComponent* Crafting = TestCharacter->GetCraftingComponent();
|
||||||
|
UAgrarianBuildingPlacementComponent* Placement = TestCharacter->GetBuildingPlacementComponent();
|
||||||
|
|
||||||
|
if (!Inventory || !Crafting || !Placement)
|
||||||
|
{
|
||||||
|
TestCharacter->Destroy();
|
||||||
|
return TEXT("FAIL: spawned character is missing inventory, crafting, or placement component");
|
||||||
|
}
|
||||||
|
|
||||||
|
TMap<FName, int32> NeededItems;
|
||||||
|
for (const FAgrarianItemStack& ShelterIngredient : ShelterRecipe->Recipe.Ingredients)
|
||||||
|
{
|
||||||
|
if (ShelterIngredient.ItemId == TEXT("primitive_frame"))
|
||||||
|
{
|
||||||
|
for (const FAgrarianItemStack& Ingredient : FrameRecipe->Recipe.Ingredients)
|
||||||
|
{
|
||||||
|
AddNeededIngredient(NeededItems, Ingredient.ItemId, Ingredient.Quantity * ShelterIngredient.Quantity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (ShelterIngredient.ItemId == TEXT("primitive_wall_panel"))
|
||||||
|
{
|
||||||
|
for (const FAgrarianItemStack& Ingredient : WallPanelRecipe->Recipe.Ingredients)
|
||||||
|
{
|
||||||
|
AddNeededIngredient(NeededItems, Ingredient.ItemId, Ingredient.Quantity * ShelterIngredient.Quantity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (ShelterIngredient.ItemId == TEXT("primitive_roof_panel"))
|
||||||
|
{
|
||||||
|
for (const FAgrarianItemStack& Ingredient : RoofPanelRecipe->Recipe.Ingredients)
|
||||||
|
{
|
||||||
|
AddNeededIngredient(NeededItems, Ingredient.ItemId, Ingredient.Quantity * ShelterIngredient.Quantity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
AddNeededIngredient(NeededItems, ShelterIngredient.ItemId, ShelterIngredient.Quantity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const int32 NeededWood = NeededItems.FindRef(TEXT("wood"));
|
||||||
|
const int32 NeededFiber = NeededItems.FindRef(TEXT("fiber"));
|
||||||
|
const int32 NeededHide = NeededItems.FindRef(TEXT("hide"));
|
||||||
|
|
||||||
|
auto GatherUntil = [TestCharacter, Inventory](AAgrarianResourceNode* Node, const FName ItemId, const int32 NeededCount) -> bool
|
||||||
|
{
|
||||||
|
while (Inventory->GetItemCount(ItemId) < NeededCount)
|
||||||
|
{
|
||||||
|
if (!Node || !Node->GetClass()->ImplementsInterface(UAgrarianInteractable::StaticClass()) || !IAgrarianInteractable::Execute_CanInteract(Node, TestCharacter))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
IAgrarianInteractable::Execute_Interact(Node, TestCharacter);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!GatherUntil(WoodNode, TEXT("wood"), NeededWood))
|
||||||
|
{
|
||||||
|
const int32 WoodCount = Inventory->GetItemCount(TEXT("wood"));
|
||||||
|
TestCharacter->Destroy();
|
||||||
|
return FString::Printf(TEXT("FAIL: could not gather enough wood, needed=%d gathered=%d"), NeededWood, WoodCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!GatherUntil(FiberNode, TEXT("fiber"), NeededFiber))
|
||||||
|
{
|
||||||
|
const int32 FiberCount = Inventory->GetItemCount(TEXT("fiber"));
|
||||||
|
TestCharacter->Destroy();
|
||||||
|
return FString::Printf(TEXT("FAIL: could not gather enough fiber, needed=%d gathered=%d"), NeededFiber, FiberCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (NeededHide > 0)
|
||||||
|
{
|
||||||
|
Wildlife->ApplyWildlifeDamage(Wildlife->MaxHealth + 10.0f, TestCharacter);
|
||||||
|
if (!IAgrarianInteractable::Execute_CanInteract(Wildlife, TestCharacter))
|
||||||
|
{
|
||||||
|
TestCharacter->Destroy();
|
||||||
|
return TEXT("FAIL: wildlife could not be harvested after lethal damage");
|
||||||
|
}
|
||||||
|
|
||||||
|
IAgrarianInteractable::Execute_Interact(Wildlife, TestCharacter);
|
||||||
|
if (Inventory->GetItemCount(TEXT("hide")) < NeededHide)
|
||||||
|
{
|
||||||
|
const int32 HideCount = Inventory->GetItemCount(TEXT("hide"));
|
||||||
|
TestCharacter->Destroy();
|
||||||
|
return FString::Printf(TEXT("FAIL: could not harvest enough hide, needed=%d harvested=%d"), NeededHide, HideCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Crafting->AddKnownRecipe(FrameRecipe->Recipe) || !Crafting->AddKnownRecipe(WallPanelRecipe->Recipe) || !Crafting->AddKnownRecipe(RoofPanelRecipe->Recipe) || !Crafting->AddKnownRecipe(ShelterRecipe->Recipe))
|
||||||
|
{
|
||||||
|
TestCharacter->Destroy();
|
||||||
|
return TEXT("FAIL: could not add shelter recipes to test character");
|
||||||
|
}
|
||||||
|
|
||||||
|
auto CraftRepeated = [TestCharacter, Crafting](const UAgrarianRecipeDataAsset* RecipeAsset, const int32 Count) -> bool
|
||||||
|
{
|
||||||
|
for (int32 Index = 0; Index < Count; ++Index)
|
||||||
|
{
|
||||||
|
if (!RecipeAsset || !Crafting->Craft(RecipeAsset->Recipe.RecipeId))
|
||||||
|
{
|
||||||
|
TestCharacter->Destroy();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const int32 NeededFrames = GetIngredientQuantity(ShelterRecipe, TEXT("primitive_frame"));
|
||||||
|
const int32 NeededWallPanels = GetIngredientQuantity(ShelterRecipe, TEXT("primitive_wall_panel"));
|
||||||
|
const int32 NeededRoofPanels = GetIngredientQuantity(ShelterRecipe, TEXT("primitive_roof_panel"));
|
||||||
|
if (!CraftRepeated(FrameRecipe, NeededFrames) || !CraftRepeated(WallPanelRecipe, NeededWallPanels) || !CraftRepeated(RoofPanelRecipe, NeededRoofPanels))
|
||||||
|
{
|
||||||
|
return TEXT("FAIL: could not craft primitive shelter parts from gathered materials");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Crafting->Craft(ShelterRecipe->Recipe.RecipeId))
|
||||||
|
{
|
||||||
|
TestCharacter->Destroy();
|
||||||
|
return TEXT("FAIL: primitive shelter craft failed after natural gathering");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Inventory->GetItemCount(TEXT("primitive_shelter")) < 1)
|
||||||
|
{
|
||||||
|
TestCharacter->Destroy();
|
||||||
|
return TEXT("FAIL: crafting did not add primitive_shelter to inventory");
|
||||||
|
}
|
||||||
|
|
||||||
|
TArray<FAgrarianItemStack> PlacementCost;
|
||||||
|
FAgrarianItemStack ShelterCost = ShelterRecipe->Recipe.Result;
|
||||||
|
ShelterCost.Quantity = 1;
|
||||||
|
PlacementCost.Add(ShelterCost);
|
||||||
|
Placement->SetActiveBuildable(ShelterClass, PlacementCost);
|
||||||
|
Placement->PlacementDistance = 5000.0f;
|
||||||
|
Placement->PlacementProbeRadius = 1.0f;
|
||||||
|
|
||||||
|
const int32 ShelterCountBeforePlace = Inventory->GetItemCount(TEXT("primitive_shelter"));
|
||||||
|
int32 ShelterActorsBeforePlace = 0;
|
||||||
|
for (TActorIterator<AActor> ActorIt(EditorWorld, ShelterClass); ActorIt; ++ActorIt)
|
||||||
|
{
|
||||||
|
ShelterActorsBeforePlace++;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FTransform PlacementTransform(FRotator::ZeroRotator, FVector(1500.0f, 550.0f, 300.0f));
|
||||||
|
Placement->ServerPlaceBuildable_Implementation(ShelterClass, PlacementTransform);
|
||||||
|
|
||||||
|
const int32 ShelterCountAfterPlace = Inventory->GetItemCount(TEXT("primitive_shelter"));
|
||||||
|
int32 ShelterActorsAfterPlace = 0;
|
||||||
|
AActor* PlacedShelter = nullptr;
|
||||||
|
for (TActorIterator<AActor> ActorIt(EditorWorld, ShelterClass); ActorIt; ++ActorIt)
|
||||||
|
{
|
||||||
|
ShelterActorsAfterPlace++;
|
||||||
|
PlacedShelter = *ActorIt;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ShelterActorsAfterPlace <= ShelterActorsBeforePlace)
|
||||||
|
{
|
||||||
|
TestCharacter->Destroy();
|
||||||
|
return FString::Printf(TEXT("FAIL: placement did not spawn shelter, before=%d after=%d"), ShelterActorsBeforePlace, ShelterActorsAfterPlace);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ShelterCountAfterPlace >= ShelterCountBeforePlace)
|
||||||
|
{
|
||||||
|
TestCharacter->Destroy();
|
||||||
|
return FString::Printf(TEXT("FAIL: placement did not consume shelter item, before=%d after=%d"), ShelterCountBeforePlace, ShelterCountAfterPlace);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!PlacedShelter || !PlacedShelter->FindComponentByClass<UAgrarianPersistentActorComponent>())
|
||||||
|
{
|
||||||
|
TestCharacter->Destroy();
|
||||||
|
return TEXT("FAIL: placed shelter is missing persistent actor component");
|
||||||
|
}
|
||||||
|
|
||||||
|
TArray<FAgrarianSavedWorldActor> SavedActors;
|
||||||
|
for (TActorIterator<AActor> ActorIt(EditorWorld); ActorIt; ++ActorIt)
|
||||||
|
{
|
||||||
|
if (UAgrarianPersistentActorComponent* PersistentComponent = ActorIt->FindComponentByClass<UAgrarianPersistentActorComponent>())
|
||||||
|
{
|
||||||
|
if (PersistentComponent->IsSaveable())
|
||||||
|
{
|
||||||
|
SavedActors.Add(PersistentComponent->CaptureSaveState());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (SavedActors.IsEmpty())
|
||||||
|
{
|
||||||
|
TestCharacter->Destroy();
|
||||||
|
return TEXT("FAIL: save capture did not include any persistent world actors");
|
||||||
|
}
|
||||||
|
|
||||||
|
int32 RestoredActorCount = 0;
|
||||||
|
for (const FAgrarianSavedWorldActor& SavedActor : SavedActors)
|
||||||
|
{
|
||||||
|
if (SavedActor.ActorTypeId != TEXT("primitive_shelter"))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
AActor* RestoredActor = EditorWorld->SpawnActor<AActor>(ShelterClass, SavedActor.Transform);
|
||||||
|
if (!RestoredActor)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (UAgrarianPersistentActorComponent* PersistentComponent = RestoredActor->FindComponentByClass<UAgrarianPersistentActorComponent>())
|
||||||
|
{
|
||||||
|
PersistentComponent->ApplySaveState(SavedActor);
|
||||||
|
}
|
||||||
|
|
||||||
|
RestoredActorCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (RestoredActorCount <= 0)
|
||||||
|
{
|
||||||
|
TestCharacter->Destroy();
|
||||||
|
return TEXT("FAIL: restore did not spawn any saved primitive shelter actors");
|
||||||
|
}
|
||||||
|
|
||||||
|
const int32 WoodGathered = Inventory->GetItemCount(TEXT("wood")) + NeededWood;
|
||||||
|
const int32 FiberGathered = Inventory->GetItemCount(TEXT("fiber")) + NeededFiber;
|
||||||
|
const int32 HideHarvested = Inventory->GetItemCount(TEXT("hide")) + NeededHide;
|
||||||
|
TestCharacter->Destroy();
|
||||||
|
return FString::Printf(
|
||||||
|
TEXT("PASS: naturally gathered wood=%d fiber=%d hide=%d, crafted parts %d/%d/%d, crafted and placed primitive_shelter, saved %d persistent actor(s), restored %d actor(s)"),
|
||||||
|
WoodGathered,
|
||||||
|
FiberGathered,
|
||||||
|
HideHarvested,
|
||||||
|
NeededFrames,
|
||||||
|
NeededWallPanels,
|
||||||
|
NeededRoofPanels,
|
||||||
|
SavedActors.Num(),
|
||||||
|
RestoredActorCount);
|
||||||
|
#else
|
||||||
|
return TEXT("FAIL: editor automation is only available in editor builds");
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
FString UAgrarianEditorAutomationLibrary::RunPersistenceSubsystemSmokeTest(TSubclassOf<AActor> ShelterClass, const FString& SlotName)
|
||||||
|
{
|
||||||
|
#if WITH_EDITOR
|
||||||
|
UWorld* TestWorld = nullptr;
|
||||||
|
if (GEditor)
|
||||||
|
{
|
||||||
|
TestWorld = GEditor->PlayWorld ? GEditor->PlayWorld.Get() : GEditor->GetEditorWorldContext().World();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TestWorld)
|
||||||
|
{
|
||||||
|
return TEXT("FAIL: no editor or PIE world");
|
||||||
|
}
|
||||||
|
|
||||||
|
UGameInstance* GameInstance = TestWorld->GetGameInstance();
|
||||||
|
if (!GameInstance)
|
||||||
|
{
|
||||||
|
return TEXT("FAIL: test world has no live GameInstance");
|
||||||
|
}
|
||||||
|
|
||||||
|
UAgrarianPersistenceSubsystem* Persistence = GameInstance->GetSubsystem<UAgrarianPersistenceSubsystem>();
|
||||||
|
if (!Persistence)
|
||||||
|
{
|
||||||
|
return TEXT("FAIL: live GameInstance has no Agrarian persistence subsystem");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ShelterClass)
|
||||||
|
{
|
||||||
|
return TEXT("FAIL: missing shelter class");
|
||||||
|
}
|
||||||
|
|
||||||
|
const FString PreviousSlotName = Persistence->DefaultSlotName;
|
||||||
|
const int32 PreviousUserIndex = Persistence->UserIndex;
|
||||||
|
const TMap<FName, TSubclassOf<AActor>> PreviousRegistry = Persistence->WorldActorClassRegistry;
|
||||||
|
|
||||||
|
const FString EffectiveSlotName = SlotName.IsEmpty() ? TEXT("AgrarianAutomationPersistence") : SlotName;
|
||||||
|
Persistence->DefaultSlotName = EffectiveSlotName;
|
||||||
|
Persistence->UserIndex = 0;
|
||||||
|
Persistence->WorldActorClassRegistry.Reset();
|
||||||
|
Persistence->RegisterWorldActorClass(TEXT("primitive_shelter"), ShelterClass);
|
||||||
|
UGameplayStatics::DeleteGameInSlot(EffectiveSlotName, 0);
|
||||||
|
|
||||||
|
FActorSpawnParameters SpawnParams;
|
||||||
|
SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
|
||||||
|
AActor* TestShelter = TestWorld->SpawnActor<AActor>(
|
||||||
|
ShelterClass,
|
||||||
|
FTransform(FRotator::ZeroRotator, FVector(1800.0f, 750.0f, 300.0f)),
|
||||||
|
SpawnParams);
|
||||||
|
|
||||||
|
if (!TestShelter)
|
||||||
|
{
|
||||||
|
Persistence->DefaultSlotName = PreviousSlotName;
|
||||||
|
Persistence->UserIndex = PreviousUserIndex;
|
||||||
|
Persistence->WorldActorClassRegistry = PreviousRegistry;
|
||||||
|
return TEXT("FAIL: could not spawn persistence test shelter");
|
||||||
|
}
|
||||||
|
|
||||||
|
TestShelter->SetActorLabel(TEXT("AGR_AutomationPersistenceShelter"), false);
|
||||||
|
|
||||||
|
if (!TestShelter->FindComponentByClass<UAgrarianPersistentActorComponent>())
|
||||||
|
{
|
||||||
|
TestShelter->Destroy();
|
||||||
|
Persistence->DefaultSlotName = PreviousSlotName;
|
||||||
|
Persistence->UserIndex = PreviousUserIndex;
|
||||||
|
Persistence->WorldActorClassRegistry = PreviousRegistry;
|
||||||
|
return TEXT("FAIL: test shelter is missing persistent actor component");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Persistence->SaveCurrentWorld())
|
||||||
|
{
|
||||||
|
TestShelter->Destroy();
|
||||||
|
UGameplayStatics::DeleteGameInSlot(EffectiveSlotName, 0);
|
||||||
|
Persistence->DefaultSlotName = PreviousSlotName;
|
||||||
|
Persistence->UserIndex = PreviousUserIndex;
|
||||||
|
Persistence->WorldActorClassRegistry = PreviousRegistry;
|
||||||
|
return TEXT("FAIL: SaveCurrentWorld failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Persistence->DoesSaveExist())
|
||||||
|
{
|
||||||
|
TestShelter->Destroy();
|
||||||
|
UGameplayStatics::DeleteGameInSlot(EffectiveSlotName, 0);
|
||||||
|
Persistence->DefaultSlotName = PreviousSlotName;
|
||||||
|
Persistence->UserIndex = PreviousUserIndex;
|
||||||
|
Persistence->WorldActorClassRegistry = PreviousRegistry;
|
||||||
|
return TEXT("FAIL: save slot was not written");
|
||||||
|
}
|
||||||
|
|
||||||
|
const UAgrarianSaveGame* LoadedSave = Persistence->LoadOrCreateSave();
|
||||||
|
const int32 SavedActorCount = LoadedSave ? LoadedSave->WorldActors.Num() : 0;
|
||||||
|
if (SavedActorCount <= 0)
|
||||||
|
{
|
||||||
|
TestShelter->Destroy();
|
||||||
|
UGameplayStatics::DeleteGameInSlot(EffectiveSlotName, 0);
|
||||||
|
Persistence->DefaultSlotName = PreviousSlotName;
|
||||||
|
Persistence->UserIndex = PreviousUserIndex;
|
||||||
|
Persistence->WorldActorClassRegistry = PreviousRegistry;
|
||||||
|
return TEXT("FAIL: loaded save did not include persistent world actors");
|
||||||
|
}
|
||||||
|
|
||||||
|
const int32 RestoredActorCount = Persistence->RestoreWorldActors(LoadedSave, true);
|
||||||
|
if (RestoredActorCount != SavedActorCount)
|
||||||
|
{
|
||||||
|
UGameplayStatics::DeleteGameInSlot(EffectiveSlotName, 0);
|
||||||
|
Persistence->DefaultSlotName = PreviousSlotName;
|
||||||
|
Persistence->UserIndex = PreviousUserIndex;
|
||||||
|
Persistence->WorldActorClassRegistry = PreviousRegistry;
|
||||||
|
return FString::Printf(TEXT("FAIL: restored actor count mismatch, saved=%d restored=%d"), SavedActorCount, RestoredActorCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
int32 PersistentActorCountAfterRestore = 0;
|
||||||
|
for (TActorIterator<AActor> ActorIt(TestWorld); ActorIt; ++ActorIt)
|
||||||
|
{
|
||||||
|
const AActor* Actor = *ActorIt;
|
||||||
|
const UAgrarianPersistentActorComponent* PersistentComponent = Actor ? Actor->FindComponentByClass<UAgrarianPersistentActorComponent>() : nullptr;
|
||||||
|
if (PersistentComponent && PersistentComponent->IsSaveable())
|
||||||
|
{
|
||||||
|
PersistentActorCountAfterRestore++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
UGameplayStatics::DeleteGameInSlot(EffectiveSlotName, 0);
|
||||||
|
Persistence->DefaultSlotName = PreviousSlotName;
|
||||||
|
Persistence->UserIndex = PreviousUserIndex;
|
||||||
|
Persistence->WorldActorClassRegistry = PreviousRegistry;
|
||||||
|
|
||||||
|
return FString::Printf(
|
||||||
|
TEXT("PASS: live persistence subsystem saved %d actor(s), restored %d actor(s), world now has %d persistent actor(s)"),
|
||||||
|
SavedActorCount,
|
||||||
|
RestoredActorCount,
|
||||||
|
PersistentActorCountAfterRestore);
|
||||||
|
#else
|
||||||
|
return TEXT("FAIL: editor automation is only available in editor builds");
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
FString UAgrarianEditorAutomationLibrary::ImportLandscapeHeightmapIntoEditorWorld(
|
||||||
|
const FString& HeightmapFilename,
|
||||||
|
const int32 Width,
|
||||||
|
const int32 Height,
|
||||||
|
const float XScaleCm,
|
||||||
|
const float YScaleCm,
|
||||||
|
const float ZScaleCm,
|
||||||
|
const FString& ActorLabel)
|
||||||
|
{
|
||||||
|
#if WITH_EDITOR
|
||||||
|
if (Width <= 1 || Height <= 1 || Width != Height)
|
||||||
|
{
|
||||||
|
return FString::Printf(TEXT("FAIL: invalid landscape dimensions Width=%d Height=%d"), Width, Height);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (FMath::IsNearlyZero(XScaleCm) || FMath::IsNearlyZero(YScaleCm) || FMath::IsNearlyZero(ZScaleCm))
|
||||||
|
{
|
||||||
|
return FString::Printf(TEXT("FAIL: invalid landscape scale X=%f Y=%f Z=%f"), XScaleCm, YScaleCm, ZScaleCm);
|
||||||
|
}
|
||||||
|
|
||||||
|
UWorld* EditorWorld = GEditor ? GEditor->GetEditorWorldContext().World() : nullptr;
|
||||||
|
if (!EditorWorld)
|
||||||
|
{
|
||||||
|
return TEXT("FAIL: no editor world");
|
||||||
|
}
|
||||||
|
|
||||||
|
TArray<uint8> HeightmapBytes;
|
||||||
|
if (!FFileHelper::LoadFileToArray(HeightmapBytes, *HeightmapFilename))
|
||||||
|
{
|
||||||
|
return FString::Printf(TEXT("FAIL: could not read heightmap %s"), *HeightmapFilename);
|
||||||
|
}
|
||||||
|
|
||||||
|
const int64 ExpectedByteCount = static_cast<int64>(Width) * static_cast<int64>(Height) * sizeof(uint16);
|
||||||
|
if (HeightmapBytes.Num() != ExpectedByteCount)
|
||||||
|
{
|
||||||
|
return FString::Printf(
|
||||||
|
TEXT("FAIL: heightmap byte count mismatch, expected=%lld actual=%d"),
|
||||||
|
ExpectedByteCount,
|
||||||
|
HeightmapBytes.Num());
|
||||||
|
}
|
||||||
|
|
||||||
|
TArray<uint16> HeightData;
|
||||||
|
HeightData.SetNumUninitialized(Width * Height);
|
||||||
|
FMemory::Memcpy(HeightData.GetData(), HeightmapBytes.GetData(), HeightmapBytes.Num());
|
||||||
|
|
||||||
|
FActorSpawnParameters SpawnParameters;
|
||||||
|
SpawnParameters.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
|
||||||
|
SpawnParameters.ObjectFlags = RF_Transactional;
|
||||||
|
|
||||||
|
const float WorldSizeX = static_cast<float>(Width - 1) * XScaleCm;
|
||||||
|
const float WorldSizeY = static_cast<float>(Height - 1) * YScaleCm;
|
||||||
|
const FVector LandscapeLocation(-WorldSizeX * 0.5f, -WorldSizeY * 0.5f, 0.0f);
|
||||||
|
ALandscape* Landscape = EditorWorld->SpawnActor<ALandscape>(
|
||||||
|
ALandscape::StaticClass(),
|
||||||
|
LandscapeLocation,
|
||||||
|
FRotator::ZeroRotator,
|
||||||
|
SpawnParameters);
|
||||||
|
|
||||||
|
if (!Landscape)
|
||||||
|
{
|
||||||
|
return TEXT("FAIL: could not spawn landscape actor");
|
||||||
|
}
|
||||||
|
|
||||||
|
Landscape->SetActorScale3D(FVector(XScaleCm, YScaleCm, ZScaleCm));
|
||||||
|
if (!ActorLabel.IsEmpty())
|
||||||
|
{
|
||||||
|
Landscape->SetActorLabel(ActorLabel, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
const FGuid LandscapeGuid = FGuid::NewGuid();
|
||||||
|
const FGuid FinalLayerGuid;
|
||||||
|
TMap<FGuid, TArray<uint16>> ImportHeightData;
|
||||||
|
ImportHeightData.Add(FinalLayerGuid, MoveTemp(HeightData));
|
||||||
|
|
||||||
|
TMap<FGuid, TArray<FLandscapeImportLayerInfo>> ImportMaterialLayerInfos;
|
||||||
|
ImportMaterialLayerInfos.Add(FinalLayerGuid, TArray<FLandscapeImportLayerInfo>());
|
||||||
|
TArray<FLandscapeLayer> ImportLayers;
|
||||||
|
Landscape->Import(
|
||||||
|
LandscapeGuid,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
Width - 1,
|
||||||
|
Height - 1,
|
||||||
|
1,
|
||||||
|
63,
|
||||||
|
ImportHeightData,
|
||||||
|
*HeightmapFilename,
|
||||||
|
ImportMaterialLayerInfos,
|
||||||
|
ELandscapeImportAlphamapType::Additive,
|
||||||
|
MakeArrayView(ImportLayers));
|
||||||
|
|
||||||
|
Landscape->RegisterAllComponents();
|
||||||
|
Landscape->PostEditChange();
|
||||||
|
Landscape->MarkPackageDirty();
|
||||||
|
EditorWorld->MarkPackageDirty();
|
||||||
|
|
||||||
|
return FString::Printf(
|
||||||
|
TEXT("PASS: imported landscape %s size=%dx%d scale=(%.6f,%.6f,%.6f) world_size_cm=(%.2f,%.2f)"),
|
||||||
|
*GetNameSafe(Landscape),
|
||||||
|
Width,
|
||||||
|
Height,
|
||||||
|
XScaleCm,
|
||||||
|
YScaleCm,
|
||||||
|
ZScaleCm,
|
||||||
|
WorldSizeX,
|
||||||
|
WorldSizeY);
|
||||||
|
#else
|
||||||
|
return TEXT("FAIL: editor automation is only available in editor builds");
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,6 +6,11 @@
|
|||||||
#include "Kismet/BlueprintFunctionLibrary.h"
|
#include "Kismet/BlueprintFunctionLibrary.h"
|
||||||
#include "AgrarianEditorAutomationLibrary.generated.h"
|
#include "AgrarianEditorAutomationLibrary.generated.h"
|
||||||
|
|
||||||
|
class AAgrarianGameCharacter;
|
||||||
|
class AAgrarianResourceNode;
|
||||||
|
class AAgrarianWildlifeBase;
|
||||||
|
class UAgrarianRecipeDataAsset;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Editor automation helpers used by Python setup scripts.
|
* Editor automation helpers used by Python setup scripts.
|
||||||
*/
|
*/
|
||||||
@@ -17,4 +22,32 @@ class AGRARIANGAME_API UAgrarianEditorAutomationLibrary : public UBlueprintFunct
|
|||||||
public:
|
public:
|
||||||
UFUNCTION(BlueprintCallable, Category = "Agrarian|Editor Automation")
|
UFUNCTION(BlueprintCallable, Category = "Agrarian|Editor Automation")
|
||||||
static AActor* SpawnActorInEditorWorld(TSubclassOf<AActor> ActorClass, const FVector& Location, const FRotator& Rotation, const FString& ActorLabel);
|
static AActor* SpawnActorInEditorWorld(TSubclassOf<AActor> ActorClass, const FVector& Location, const FRotator& Rotation, const FString& ActorLabel);
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Agrarian|Editor Automation")
|
||||||
|
static FString RunPlayableLoopSmokeTest(TSubclassOf<AAgrarianGameCharacter> CharacterClass, AAgrarianResourceNode* ResourceNode, UAgrarianRecipeDataAsset* ShelterRecipe, TSubclassOf<AActor> ShelterClass);
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Agrarian|Editor Automation")
|
||||||
|
static FString RunNaturalShelterLoopSmokeTest(
|
||||||
|
TSubclassOf<AAgrarianGameCharacter> CharacterClass,
|
||||||
|
AAgrarianResourceNode* WoodNode,
|
||||||
|
AAgrarianResourceNode* FiberNode,
|
||||||
|
AAgrarianWildlifeBase* Wildlife,
|
||||||
|
UAgrarianRecipeDataAsset* FrameRecipe,
|
||||||
|
UAgrarianRecipeDataAsset* WallPanelRecipe,
|
||||||
|
UAgrarianRecipeDataAsset* RoofPanelRecipe,
|
||||||
|
UAgrarianRecipeDataAsset* ShelterRecipe,
|
||||||
|
TSubclassOf<AActor> ShelterClass);
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Agrarian|Editor Automation")
|
||||||
|
static FString RunPersistenceSubsystemSmokeTest(TSubclassOf<AActor> ShelterClass, const FString& SlotName);
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Agrarian|Editor Automation")
|
||||||
|
static FString ImportLandscapeHeightmapIntoEditorWorld(
|
||||||
|
const FString& HeightmapFilename,
|
||||||
|
int32 Width,
|
||||||
|
int32 Height,
|
||||||
|
float XScaleCm,
|
||||||
|
float YScaleCm,
|
||||||
|
float ZScaleCm,
|
||||||
|
const FString& ActorLabel);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ public class AgrarianGame : ModuleRules
|
|||||||
"StateTreeModule",
|
"StateTreeModule",
|
||||||
"GameplayStateTreeModule",
|
"GameplayStateTreeModule",
|
||||||
"UMG",
|
"UMG",
|
||||||
|
"Landscape",
|
||||||
"Slate"
|
"Slate"
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
// Copyright Pacificao. All Rights Reserved.
|
||||||
|
|
||||||
|
#if WITH_EDITOR && WITH_DEV_AUTOMATION_TESTS
|
||||||
|
|
||||||
|
#include "AgrarianEditorAutomationLibrary.h"
|
||||||
|
#include "HAL/PlatformTime.h"
|
||||||
|
#include "Misc/AutomationTest.h"
|
||||||
|
#include "Tests/AutomationEditorCommon.h"
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
constexpr const TCHAR* ShelterClassPath = TEXT("/Game/Agrarian/Blueprints/Structures/BP_PrimitiveShelter.BP_PrimitiveShelter_C");
|
||||||
|
constexpr const TCHAR* PersistenceTestSlot = TEXT("AgrarianAutomationPersistence");
|
||||||
|
|
||||||
|
class FAgrarianCreateBlankMapCommand final : public IAutomationLatentCommand
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit FAgrarianCreateBlankMapCommand(FAutomationTestBase* InTest)
|
||||||
|
: Test(InTest)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual bool Update() override
|
||||||
|
{
|
||||||
|
UWorld* EditorWorld = FAutomationEditorCommonUtils::CreateNewMap();
|
||||||
|
if (!EditorWorld && Test)
|
||||||
|
{
|
||||||
|
Test->AddError(TEXT("Could not create blank editor map for persistence subsystem test"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
FAutomationTestBase* Test = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
class FAgrarianWaitSecondsCommand final : public IAutomationLatentCommand
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit FAgrarianWaitSecondsCommand(const double InWaitSeconds)
|
||||||
|
: EndTime(FPlatformTime::Seconds() + InWaitSeconds)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual bool Update() override
|
||||||
|
{
|
||||||
|
return FPlatformTime::Seconds() >= EndTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
double EndTime = 0.0;
|
||||||
|
};
|
||||||
|
|
||||||
|
class FAgrarianRunPersistenceSubsystemCommand final : public IAutomationLatentCommand
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit FAgrarianRunPersistenceSubsystemCommand(FAutomationTestBase* InTest)
|
||||||
|
: Test(InTest)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual bool Update() override
|
||||||
|
{
|
||||||
|
if (!Test)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
UClass* ShelterClass = StaticLoadClass(AActor::StaticClass(), nullptr, ShelterClassPath);
|
||||||
|
if (!ShelterClass)
|
||||||
|
{
|
||||||
|
Test->AddError(FString::Printf(TEXT("Could not load shelter class: %s"), ShelterClassPath));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FString Result = UAgrarianEditorAutomationLibrary::RunPersistenceSubsystemSmokeTest(ShelterClass, PersistenceTestSlot);
|
||||||
|
if (!Result.StartsWith(TEXT("PASS:")))
|
||||||
|
{
|
||||||
|
Test->AddError(Result);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Test->AddInfo(Result);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
FAutomationTestBase* Test = nullptr;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||||
|
FAgrarianPersistenceSubsystemAutomationTest,
|
||||||
|
"Agrarian.PersistenceSubsystem.LiveGameInstance",
|
||||||
|
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
|
||||||
|
|
||||||
|
bool FAgrarianPersistenceSubsystemAutomationTest::RunTest(const FString& Parameters)
|
||||||
|
{
|
||||||
|
ADD_LATENT_AUTOMATION_COMMAND(FAgrarianCreateBlankMapCommand(this));
|
||||||
|
ADD_LATENT_AUTOMATION_COMMAND(FStartPIECommand(true));
|
||||||
|
ADD_LATENT_AUTOMATION_COMMAND(FAgrarianWaitSecondsCommand(1.0));
|
||||||
|
ADD_LATENT_AUTOMATION_COMMAND(FAgrarianRunPersistenceSubsystemCommand(this));
|
||||||
|
ADD_LATENT_AUTOMATION_COMMAND(FEndPlayMapCommand());
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
Reference in New Issue
Block a user