Compare commits

..

4 commits

174 changed files with 8935 additions and 171 deletions

View file

@ -1,4 +1,4 @@
# NETfishing Asset License 1.0
# NETfishing Asset License 1.1
Copyright © 2026 Woofmeow. All rights reserved except for the permissions
expressly granted below.
@ -7,8 +7,9 @@ expressly granted below.
This license applies to original NETfishing artwork, graphics, textures,
models, animations, music, sound recordings, narrative content, visual
designs, characters, and other creative assets owned by the NETfishing owners,
publishing as Woofmeow (the **“NETfishing Assets”**).
designs, characters, and other creative assets owned by the NETfishing owners
or licensed to them with authority to sublicense, publishing as Woofmeow (the
**“NETfishing Assets”**).
It does not apply to:
@ -35,6 +36,13 @@ NETfishing Software. A Mod may add, replace, or alter content loaded by the
Official NETfishing Software, but it may not redistribute a replacement copy
of the game or represent a fork, clone, sequel, or separate game.
**Contribution Fork** means a source-control fork or clone of an official
NETfishing source repository that is created and maintained in good faith
primarily to prepare, test, review, or submit changes for possible inclusion
in Official NETfishing Software. It does not include a fork presented or
developed as an independent game, community edition, replacement distribution,
or ongoing product separate from the official NETfishing project.
## 3. Permission to use the official game
You may reproduce NETfishing Assets as technically necessary to install, run,
@ -64,7 +72,7 @@ A distributed Mod must:
3. include the following notice in its documentation or distribution page:
> Contains or adapts NETfishing assets © 2026 Woofmeow, used under the
> NETfishing Asset License 1.0. This Mod is unofficial and is not endorsed
> NETfishing Asset License 1.1. This Mod is unofficial and is not endorsed
> by Woofmeow.
4. identify material created by the Mod author separately from NETfishing
@ -77,14 +85,57 @@ charging for access to it, licensing it commercially, or using NETfishing
Assets in paid products requires separate written permission from both
NETfishing owners.
## 5. Uses not permitted
## 5. Contribution Fork license
Except with separate written permission from both NETfishing owners, you may
not use or distribute NETfishing Assets:
Subject to this license, you may reproduce, modify, and distribute NETfishing
Assets within a Contribution Fork. You may also make the nonpublic builds
reasonably necessary to develop, test, and review its proposed contribution.
- in a fork, clone, remake, sequel, or standalone game;
This permission is worldwide, nonexclusive, royalty-free, and available for
as long as you comply with this license. Public source-control hosting of the
Contribution Fork and the repository history technically necessary for that
fork are permitted distributions.
A Contribution Fork must:
1. remain primarily directed toward preparing changes for possible inclusion
in the official NETfishing project;
2. preserve this license and the official NETfishing project's applicable
attribution, third-party license, and copyright notices;
3. remain visibly linked to or identified as a fork of the official source
repository and not be presented as official or endorsed;
4. limit test-build access to the owners, maintainers, contributors, and
testers directly participating in development or review of the proposed
contribution;
5. not publish general-audience binaries, releases, packages, or downloads,
operate a persistent public alternative service, or monetize access;
6. not distribute the NETfishing Assets as a standalone extraction, archive,
or general-purpose asset pack; and
7. comply with `TRADEMARKS.md`.
Temporary access-controlled servers may be operated when reasonably necessary
to test a proposed contribution. They may not be promoted or maintained as a
public alternative to Official NETfishing Software or its services.
Submitting work from a Contribution Fork is governed separately by
`CONTRIBUTOR-TERMS.md` and does not require the NETfishing owners to accept the
work. Declining or closing a proposed contribution does not retroactively
invalidate good-faith use under this section. A compliant Contribution Fork
may remain publicly accessible as an archival development record, but it may
not be repurposed for continued development or distribution of a separate
product containing NETfishing Assets.
## 6. Uses not permitted
Except as expressly permitted by Sections 3, 4, and 5 or by separate written
permission from both NETfishing owners, you may not use or distribute
NETfishing Assets:
- in a fork, clone, remake, sequel, or standalone game that is not a qualifying
Contribution Fork;
- with a modified distribution of the GPL-covered NETfishing code that is not
Official NETfishing Software;
Official NETfishing Software, except for nonpublic contribution-testing
builds permitted by Section 5;
- in another game, application, asset library, template, or merchandise;
- as a substantially complete asset extraction or archive;
- in a way that implies sponsorship, endorsement, or official status;
@ -92,22 +143,25 @@ not use or distribute NETfishing Assets:
identity; or
- under terms that claim ownership of the NETfishing Assets themselves.
The GPL permits forks and redistribution of the GPL-covered code. This asset
license does not permit those forks to carry NETfishing Assets. A code fork
must remove or replace the NETfishing Assets and reserved branding unless both
owners grant separate written permission.
The GPL permits forks and redistribution of the GPL-covered code. Except for a
qualifying Contribution Fork under Section 5, this asset license does not
permit those forks to carry NETfishing Assets. A code fork that is not a
Contribution Fork must remove or replace the NETfishing Assets and reserved
branding unless both owners grant separate written permission.
## 6. Ownership of Mods
## 7. Ownership of Mods and contribution work
You retain ownership of your original Mod material. The NETfishing owners
retain ownership of the NETfishing Assets and adaptations of those assets to
the extent provided by applicable law.
You retain ownership of your original Mod material and original material
created in a Contribution Fork. The NETfishing owners retain ownership of the
NETfishing Assets and adaptations of those assets to the extent provided by
applicable law.
This license does not require you to assign your original Mod material to the
NETfishing owners. It also does not grant you ownership of the underlying
NETfishing Assets.
This license does not require you to assign or submit your original material.
It also does not grant you ownership of the underlying NETfishing Assets. If
you intentionally submit material for inclusion in NETfishing,
`CONTRIBUTOR-TERMS.md` applies separately.
## 7. Termination and cure
## 8. Termination and cure
Your permissions under this license terminate automatically if you violate
its terms. They are reinstated provisionally when you stop the violation and
@ -117,7 +171,7 @@ within sixty days after it has been cured.
Termination does not limit remedies available for activity outside this
license.
## 8. No warranty
## 9. No warranty
THE NETFISHING ASSETS ARE PROVIDED “AS IS,” WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
@ -125,7 +179,7 @@ PARTICULAR PURPOSE, TITLE, OR NON-INFRINGEMENT. TO THE MAXIMUM EXTENT
PERMITTED BY LAW, THE OWNERS WILL NOT BE LIABLE FOR ANY CLAIM, DAMAGES, OR
OTHER LIABILITY ARISING FROM THIS LICENSE OR USE OF THE NETFISHING ASSETS.
## 9. Additional permission
## 10. Additional permission
Uses not granted here require written permission from both NETfishing owners.
A permission granted to one person or project does not amend this license for

View file

@ -10,6 +10,21 @@ applicable grants and representations in
affirmative record of that agreement. Contact the owners before submitting if
separate written terms are needed.
## Contribution forks
The [NETfishing Asset License](ASSET-LICENSE.md) permits a public source-control
fork to retain NETfishing assets when the fork is maintained in good faith to
prepare, test, review, or submit changes for possible inclusion in the official
project. Keep the hosting platform's fork relationship visible, preserve all
license and attribution notices, and do not publish general-audience builds or
use the fork as an independent game or distribution.
Private test builds and temporary access-controlled test servers may be shared
with people directly participating in development or review of the proposed
contribution. Submission remains subject to `CONTRIBUTOR-TERMS.md`; permission
to maintain a Contribution Fork does not guarantee that its changes will be
accepted.
## Change discipline
- Keep a change focused; avoid unrelated cleanup.

View file

@ -95,8 +95,8 @@ world and UI authoring, and testing are consolidated in
[GPL-3.0-or-later](LICENSE).
- Project-owned artwork, models, music, and other creative assets are covered
by the [NETfishing Asset License](ASSET-LICENSE.md). It permits community
Mods for official NETfishing software, but not use in forks, clones, or
unrelated products.
Mods for official NETfishing software and narrowly scoped contribution forks,
but not independent forks, clones, or unrelated products.
- The NETfishing and Woofmeow names and branding are reserved as described in
[TRADEMARKS.md](TRADEMARKS.md).
- Third-party materials retain their own terms; see the consolidated

View file

@ -24,10 +24,24 @@ An unofficial Mod may use wording such as “Example Mod for NETfishing” if it
name and presentation remain clearly distinct and it displays an unofficial
notice.
## Forks and redistributed code
## Contribution forks
A qualifying Contribution Fork under `ASSET-LICENSE.md` may retain the
NETfishing and Woofmeow names, logos, icons, and trade dress already present in
the official source repository solely as necessary to reproduce, modify, and
test the Project while preparing a proposed contribution. The source-control
host's visible fork relationship or an equivalent repository notice must make
its unofficial contribution purpose clear.
This permission does not allow a Contribution Fork to use NETfishing or
Woofmeow branding to advertise a public build, release, service, community
edition, or independent product. It does not imply acceptance, sponsorship,
endorsement, or official status.
## Other forks and redistributed code
The GNU GPL permits forks and redistribution of covered code. A fork or
modified distribution must:
modified distribution that is not a qualifying Contribution Fork must:
- use a distinct product name;
- remove NETfishing and Woofmeow logos, icons, and official trade dress;

View file

@ -53,11 +53,12 @@ alone does not relicense third-party material.
The GPL permits study, modification, redistribution, and forks of covered
code. It does not grant rights to NETfishing creative assets or branding. The
NETfishing Asset License separately permits qualifying noncommercial add-on
Mods for official NETfishing software; it does not permit those assets to
accompany code forks, clones, standalone games, unrelated products, or
general-purpose asset packs. A GPL fork must replace NETfishing assets and
reserved branding with independently licensed material and a distinct
identity.
Mods for official NETfishing software and narrowly scoped Contribution Forks
used to prepare changes for possible upstream inclusion. It does not permit
those assets to accompany independent code forks, clones, standalone games,
unrelated products, or general-purpose asset packs. A GPL fork that is not a
qualifying Contribution Fork must replace NETfishing assets and reserved
branding with independently licensed material and a distinct identity.
Project-wide code, asset, branding, or contributor-license changes require
written approval from both NETfishing owners under their private joint

View file

@ -409,6 +409,12 @@ func _start_dedicated_server() -> void:
):
_fail_dedicated_server("The server operator list is invalid.")
return
if not _apply_generated_world(config.world_seed, false):
_fail_dedicated_server("The configured world seed could not be generated.")
return
if not _network_session.set_host_world_seed(config.world_seed):
_fail_dedicated_server("The configured world seed is invalid.")
return
_configure_portable_stores()
if not _discovery.configure_dedicated_runtime(config.server_name):
_fail_dedicated_server("The server room name is invalid.")
@ -1666,14 +1672,24 @@ func _resize_native_overlays() -> void:
_shop_backdrop.size = Vector2(get_window().size)
func _on_new_game_requested() -> void:
func _on_new_game_requested(world_seed: int) -> void:
if _gameplay_started or _quit_in_progress:
return
if not _apply_generated_world(world_seed, true):
_game_ui.get_title_screen().report_network_error(
"Could not generate that world seed. Try rolling another world."
)
return
if not _prepare_host_world_seed(world_seed):
_game_ui.get_title_screen().report_network_error(
"Could not prepare the generated world for hosting."
)
return
if not _prepare_private_host():
return
if (
not _save_manager.delete_progression_save()
or not _save_manager.initialize_new_game()
or not _save_manager.initialize_new_game(world_seed)
):
_network_session.disconnect_session("New Game setup failed.")
_game_ui.get_title_screen().report_network_error(
@ -1686,14 +1702,22 @@ func _on_new_game_requested() -> void:
func _on_continue_game_requested() -> void:
if _gameplay_started or _quit_in_progress:
return
if not _prepare_private_host():
return
if not _save_manager.load_player_data():
_network_session.disconnect_session("Continue failed.")
_game_ui.get_title_screen().report_network_error(
"Failed to load save. The original was preserved."
)
return
var world_seed: int = _save_manager.get_world_seed()
if (
not _apply_generated_world(world_seed, true)
or not _prepare_host_world_seed(world_seed)
):
_game_ui.get_title_screen().report_network_error(
"The saved world could not be generated. The save was preserved."
)
return
if not _prepare_private_host():
return
_enter_gameplay()
@ -1716,6 +1740,15 @@ func _prepare_private_host() -> bool:
return true
func _prepare_host_world_seed(world_seed: int) -> bool:
if _network_session.state in [
NetworkSessionType.State.CONNECTION_FAILED,
NetworkSessionType.State.SERVER_LOST,
]:
_network_session.reset_failure()
return _network_session.set_host_world_seed(world_seed)
func _enter_gameplay() -> void:
_fade_out_title_music()
_game_ui.get_title_screen().hide()
@ -1805,10 +1838,14 @@ func _on_network_join_authenticated() -> void:
)
_join_requested_from_title = false
return
if not _apply_joined_world(server_metadata):
return
_fade_out_title_music()
_game_ui.get_title_screen().hide()
_set_gameplay_active(true)
elif _join_requested_from_pause:
if not _apply_joined_world(server_metadata):
return
_set_gameplay_active(true)
_join_requested_from_title = false
_join_requested_from_pause = false
@ -1912,7 +1949,12 @@ func _on_reset_progress_requested() -> void:
if not _save_manager.delete_progression_save():
pause_menu.report_reset_failure()
return
if not _save_manager.initialize_new_game():
var world_seed: int = PlayerSaveManager.roll_world_seed()
if not _save_manager.initialize_new_game(world_seed):
pause_menu.report_reset_failure()
return
_network_session.disconnect_session("Progress reset.")
if not _apply_generated_world(world_seed, true):
pause_menu.report_reset_failure()
return
pause_menu.close_for_title_transition()
@ -1921,6 +1963,44 @@ func _on_reset_progress_requested() -> void:
_show_title_music(true)
func _apply_joined_world(server_metadata: Dictionary) -> bool:
var world_seed: int = int(server_metadata.get("world_seed", 0))
if _apply_generated_world(world_seed, true):
return true
_network_session.disconnect_session("World generation failed.")
_join_requested_from_title = false
_join_requested_from_pause = false
_set_gameplay_active(false)
var title_screen: TitleScreenType = _game_ui.get_title_screen()
title_screen.reopen_to_menu()
title_screen.report_network_error(
"The host's generated world could not be built locally."
)
_show_title_music(true)
return false
func _apply_generated_world(world_seed: int, reposition_player: bool) -> bool:
if not _test_world.generate_world(world_seed):
return false
var spawn_transform: Transform3D = _test_world.get_player_spawn_transform()
_player_spawn_service.set_spawn_transform(spawn_transform)
if reposition_player:
_player.global_transform = spawn_transform
_player.velocity = Vector3.ZERO
if _application_initialized and not _dedicated_runtime:
_water_recovery.update_world_context(
spawn_transform,
_test_world.get_player_water_triggers(),
_test_world.get_safe_respawn_points(),
)
_shoreline_ambience.configure(
_player,
_test_world.get_saltwater_shoreline_mesh(),
)
return true
func _on_water_recovery_starting() -> void:
_local_recovery_attempt_id = (
"recovery:%s"

View file

@ -298,48 +298,11 @@ unique_name_in_owner = true
script = ExtResource("36_player_list")
[node name="TestWorld" parent="." unique_id=1334932296 instance=ExtResource("1_world")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -2.4475808, 0, -1.8959346)
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0)
[node name="Sun" parent="TestWorld/Environment" index="1"]
transform = Transform3D(0.8480481, 0.4287137, -0.31147876, 0, 0.5877853, 0.809017, 0.52991927, -0.68608534, 0.49847022, 0, 0, 0)
[node name="Shape" parent="TestWorld/Regions/StarterIslandRegion/Terrain/Collision" index="0" unique_id=674742331]
shape = SubResource("ConcavePolygonShape3D_hifdc")
[node name="Pond" parent="TestWorld/Regions/StarterIslandRegion/WaterBodies" index="0" unique_id=995477450]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -9.4, 2.51, 2.1)
[node name="VisualWater" parent="TestWorld/Regions/StarterIslandRegion/WaterBodies/Pond" index="0" unique_id=1034265960]
mesh = SubResource("PlaneMesh_7ny4s")
[node name="Shape" parent="TestWorld/Regions/StarterIslandRegion/WaterBodies/Pond/FishingRegion" index="0" unique_id=1185360438]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -2, 0)
shape = SubResource("BoxShape3D_agj3o")
[node name="Shape" parent="TestWorld/Regions/StarterIslandRegion/WaterBodies/Pond/RecoveryRegion" index="0" unique_id=1718808476]
shape = SubResource("BoxShape3D_l6210")
[node name="VisualWater" parent="TestWorld/Regions/StarterIslandRegion/WaterBodies/Ocean" index="0" unique_id=1176408937]
mesh = SubResource("PlaneMesh_6upbg")
[node name="Shape" parent="TestWorld/Regions/StarterIslandRegion/WaterBodies/Ocean/FishingRegions/OceanFishingRegion" index="0" unique_id=357406062]
shape = SubResource("BoxShape3D_umo15")
[node name="WestShape" parent="TestWorld/Regions/StarterIslandRegion/WaterBodies/Ocean/RecoveryRegions/OceanRecoveryRegion" index="0" unique_id=1183368645]
shape = SubResource("BoxShape3D_1fdkm")
[node name="EastShape" parent="TestWorld/Regions/StarterIslandRegion/WaterBodies/Ocean/RecoveryRegions/OceanRecoveryRegion" index="1" unique_id=1742419808]
shape = SubResource("BoxShape3D_y2c0j")
[node name="NorthShape" parent="TestWorld/Regions/StarterIslandRegion/WaterBodies/Ocean/RecoveryRegions/OceanRecoveryRegion" index="2" unique_id=1844920260]
shape = SubResource("BoxShape3D_uwwqy")
[node name="SouthShape" parent="TestWorld/Regions/StarterIslandRegion/WaterBodies/Ocean/RecoveryRegions/OceanRecoveryRegion" index="3" unique_id=1316495770]
shape = SubResource("BoxShape3D_hf28p")
[node name="FishingShopWorld" parent="TestWorld/Regions/StarterIslandRegion/Interactables" index="0" unique_id=1255373524]
transform = Transform3D(-0.9976046, 0, -0.06917416, 0, 1, 0, 0.06917416, 0, -0.9976046, 7.3292284, 3.4012775, 14.888193)
[node name="BelowWorldFailsafe" parent="TestWorld/Safety" index="0"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 12, -7, 0)
@ -436,4 +399,3 @@ unique_name_in_owner = true
unique_name_in_owner = true
[editable path="TestWorld"]
[editable path="TestWorld/Regions/StarterIslandRegion"]

View file

@ -1,7 +1,7 @@
class_name NetworkProtocol
extends RefCounted
const PROTOCOL_VERSION: int = 8
const PROTOCOL_VERSION: int = 9
const GAME_BUILD: String = "prealpha"
const MAX_GAME_VERSION_LENGTH: int = 64
const MAX_DISPLAY_NAME_LENGTH: int = 24
@ -28,6 +28,9 @@ const FISH_QUALITY_CAPABILITY: String = "fish_quality_v1"
const JOBS_CAPABILITY: String = "jobs_v1"
const WORLD_SPAWN_CAPABILITY: String = "world_spawn_envelope_v1"
const APPEARANCE_PREVIEW_CAPABILITY: String = "appearance_preview_v1"
const WORLD_GENERATION_CAPABILITY: String = "world_generation_v1"
const DEFAULT_WORLD_SEED: int = 13001
const MAX_WORLD_SEED: int = 2147483646
enum RejectionCode {
NONE,
@ -178,6 +181,7 @@ static func make_client_hello(
WORLD_SPAWN_CAPABILITY,
APPEARANCE_PREVIEW_CAPABILITY,
BACKPACK_SHOP_CAPABILITY,
WORLD_GENERATION_CAPABILITY,
]),
"cosmetic_snapshot": cosmetic_snapshot,
"identity_fingerprint": identity_fingerprint,
@ -281,6 +285,7 @@ static func make_server_hello(
player_count: int,
max_players: int,
server_display_name: String = "NETfishing",
world_seed: int = DEFAULT_WORLD_SEED,
) -> Dictionary:
return {
"accepted": accepted,
@ -292,6 +297,7 @@ static func make_server_hello(
"server_display_name": server_display_name,
"player_count": player_count,
"max_players": max_players,
"world_seed": world_seed,
"capability_flags": PackedStringArray([
"movement_v1",
"fishing_v1",
@ -309,6 +315,7 @@ static func make_server_hello(
JOBS_CAPABILITY,
WORLD_SPAWN_CAPABILITY,
APPEARANCE_PREVIEW_CAPABILITY,
WORLD_GENERATION_CAPABILITY,
"chat_v1",
"mail_v1",
"profile_v1",

View file

@ -85,6 +85,7 @@ var _last_server_max_players: int = DEFAULT_SESSION_MAX_PLAYERS
var _last_server_player_count: int = 0
var _last_server_display_name: String = ""
var _last_server_protocol_version: int = 0
var _last_server_world_seed: int = NetworkProtocol.DEFAULT_WORLD_SEED
var _server_capabilities: PackedStringArray = PackedStringArray()
var _profile_ready: bool = false
var _player_identity: PlayerIdentityStore
@ -111,6 +112,7 @@ var _configured_operator_fingerprints: Dictionary[String, bool] = {}
var _session_operator_fingerprints: Dictionary[String, bool] = {}
var _operator_peer_ids: Dictionary[int, bool] = {}
var _local_operator: bool = false
var _host_world_seed: int = NetworkProtocol.DEFAULT_WORLD_SEED
func _ready() -> void:
@ -272,6 +274,7 @@ func _register_player_host() -> void:
NetworkProtocol.JOBS_CAPABILITY,
NetworkProtocol.WORLD_SPAWN_CAPABILITY,
NetworkProtocol.BACKPACK_SHOP_CAPABILITY,
NetworkProtocol.WORLD_GENERATION_CAPABILITY,
]),
)
_registry.update_appearance(1, _local_appearance_snapshot)
@ -527,9 +530,25 @@ func get_last_server_metadata() -> Dictionary:
"protocol_version": _last_server_protocol_version,
"player_count": _last_server_player_count,
"max_players": _last_server_max_players,
"world_seed": _last_server_world_seed,
}
func set_host_world_seed(seed: int) -> bool:
if (
state != State.INACTIVE
or seed <= 0
or seed > NetworkProtocol.MAX_WORLD_SEED
):
return false
_host_world_seed = seed
return true
func get_authoritative_world_seed() -> int:
return _last_server_world_seed if is_joined_client() else _host_world_seed
func can_use_host_gameplay() -> bool:
return is_host()
@ -563,6 +582,7 @@ func supports_server_capability(capability: StringName) -> bool:
NetworkProtocol.WORLD_WEATHER_CAPABILITY,
NetworkProtocol.WORLD_SPAWN_CAPABILITY,
NetworkProtocol.APPEARANCE_PREVIEW_CAPABILITY,
NetworkProtocol.WORLD_GENERATION_CAPABILITY,
"chat_v1",
"mail_v1",
"profile_v1",
@ -1164,6 +1184,12 @@ func submit_client_hello(data: Dictionary) -> void:
NetworkProtocol.RejectionCode.UNSUPPORTED_CLIENT,
)
return
if NetworkProtocol.WORLD_GENERATION_CAPABILITY not in client_capabilities:
_reject_peer(
sender_id,
NetworkProtocol.RejectionCode.UNSUPPORTED_CLIENT,
)
return
var identity: Dictionary = _authenticated_identity_cache.get(sender_id, {})
if identity.is_empty():
_reject_peer(sender_id, NetworkProtocol.RejectionCode.INVALID_IDENTITY_PROOF)
@ -1251,6 +1277,7 @@ func submit_client_hello(data: Dictionary) -> void:
_registry.size(),
session_max_players,
_session_display_name,
_host_world_seed,
)
)
receive_spawn_list.rpc_id(sender_id, _build_spawn_list())
@ -1304,6 +1331,7 @@ func receive_server_hello(data: Dictionary) -> void:
or typeof(data.get("protocol_version")) != TYPE_INT
or typeof(data.get("game_version")) != TYPE_STRING
or typeof(data.get("rejection_code")) != TYPE_INT
or typeof(data.get("world_seed")) != TYPE_INT
):
_teardown_peer()
_fail("The server sent an invalid handshake response.")
@ -1329,6 +1357,11 @@ func receive_server_hello(data: Dictionary) -> void:
_teardown_peer()
_fail("The server uses a different network protocol.")
return
var received_world_seed := int(data["world_seed"])
if received_world_seed <= 0 or received_world_seed > NetworkProtocol.MAX_WORLD_SEED:
_teardown_peer()
_fail("The server sent an invalid world seed.")
return
_session_id = str(data.get("session_id", ""))
_last_server_max_players = int(data.get(
"max_players",
@ -1339,6 +1372,7 @@ func receive_server_hello(data: Dictionary) -> void:
_last_server_protocol_version = int(data.get(
"protocol_version", NetworkProtocol.PROTOCOL_VERSION
))
_last_server_world_seed = received_world_seed
_server_capabilities = PackedStringArray()
var advertised_capabilities: Variant = data.get(
"capability_flags", PackedStringArray()
@ -1353,6 +1387,10 @@ func receive_server_hello(data: Dictionary) -> void:
_teardown_peer()
_fail("This server does not support fish quality data.")
return
if NetworkProtocol.WORLD_GENERATION_CAPABILITY not in _server_capabilities:
_teardown_peer()
_fail("This server does not support generated worlds.")
return
var local_peer_id: int = multiplayer.get_unique_id()
_registry.clear()
_registry.add_peer(
@ -1368,6 +1406,7 @@ func receive_server_hello(data: Dictionary) -> void:
NetworkProtocol.WORLD_TIME_CAPABILITY,
NetworkProtocol.WORLD_WEATHER_CAPABILITY,
NetworkProtocol.BACKPACK_SHOP_CAPABILITY,
NetworkProtocol.WORLD_GENERATION_CAPABILITY,
]),
)
_registry.update_appearance(local_peer_id, _local_appearance_snapshot)

View file

@ -23,6 +23,10 @@ func setup(
_spawn_transform = spawn_transform
func set_spawn_transform(spawn_transform: Transform3D) -> void:
_spawn_transform = spawn_transform
func register_local_player(peer_id: int) -> void:
if _local_player == null:
return

View file

@ -33,9 +33,11 @@ const WorldWeatherServiceType = preload(
)
const PlayerJobServiceType = preload("res://jobs/player_job_service.gd")
const SAVE_VERSION: int = 8
const SAVE_VERSION: int = 9
const BASIC_ROD_ID: StringName = &"basic_fishing_rod"
const MAX_SAFE_BALANCE: int = 1000000000000
const DEFAULT_WORLD_SEED: int = 13001
const MAX_WORLD_SEED: int = 2147483646
class LoadSnapshot:
extends RefCounted
@ -56,6 +58,7 @@ class LoadSnapshot:
var cooler_capacity_level: int = 0
var art_unlock_mask: int = 0
var total_experience: int = 0
var world_seed: int = DEFAULT_WORLD_SEED
var world_time_hours: float = WorldTimeServiceType.DEFAULT_START_HOUR
var has_world_weather_state: bool = false
var world_weather: WorldWeatherServiceType.Weather = (
@ -93,6 +96,7 @@ var _autosave_enabled: bool = false
var _save_path := ""
var _expected_hash := ""
var _data_root: PlayerDataRoot
var _world_seed: int = DEFAULT_WORLD_SEED
func configure_storage(path: String, data_root: PlayerDataRoot) -> void:
@ -298,6 +302,7 @@ func load_player_data() -> bool:
var world_time_restored: bool = (
_world_time.restore_persistent_time_hours(snapshot.world_time_hours)
)
_world_seed = snapshot.world_seed
var world_weather_restored: bool = true
if snapshot.has_world_weather_state:
world_weather_restored = _world_weather.restore_persistent_state(
@ -403,14 +408,28 @@ func inspect_save() -> SaveInspectionType:
return result
func initialize_new_game() -> bool:
func initialize_new_game(world_seed: int = DEFAULT_WORLD_SEED) -> bool:
if not _is_configured:
return false
if world_seed <= 0 or world_seed > MAX_WORLD_SEED:
return false
_automatic_saving_blocked = false
_restore_defaults()
_world_seed = world_seed
_is_dirty = true
return true
func get_world_seed() -> int:
return _world_seed
static func roll_world_seed() -> int:
var random := RandomNumberGenerator.new()
random.randomize()
return random.randi_range(1, MAX_WORLD_SEED)
func delete_progression_save() -> bool:
if FileAccess.file_exists(_save_path) and not _remove_if_present(_save_path):
return false
@ -584,6 +603,7 @@ func _build_save_dictionary() -> Dictionary:
"art": _art_unlocks.to_save_data(),
"experience": _experience.to_save_data(),
"world": {
"seed": _world_seed,
"time_hours": _world_time.get_persistent_time_hours(),
"weather": int(_world_weather.get_persistent_weather()),
"weather_seconds_remaining": (
@ -664,6 +684,14 @@ func _build_load_snapshot(save_data: Dictionary) -> LoadSnapshot:
return null
var snapshot := LoadSnapshot.new()
if world_data.has("seed"):
snapshot.world_seed = _read_integer(
world_data["seed"],
-1,
MAX_WORLD_SEED,
)
if snapshot.world_seed <= 0:
return null
snapshot.inventory_layout_data = inventory_layout_data.duplicate(true)
snapshot.wallet_balance = balance
snapshot.total_experience = _read_integer(
@ -965,6 +993,8 @@ func _migrate_save(
migrated = _migrate_version_6_to_7(migrated)
7:
migrated = _migrate_version_7_to_8(migrated)
8:
migrated = _migrate_version_8_to_9(migrated)
_:
return {}
if migrated.is_empty():
@ -1201,6 +1231,17 @@ func _migrate_version_7_to_8(data: Dictionary) -> Dictionary:
return migrated
func _migrate_version_8_to_9(data: Dictionary) -> Dictionary:
var migrated: Dictionary = data.duplicate(true)
var world_data: Dictionary = {}
if typeof(migrated.get("world")) == TYPE_DICTIONARY:
world_data = (migrated["world"] as Dictionary).duplicate(true)
world_data["seed"] = DEFAULT_WORLD_SEED
migrated["world"] = world_data
migrated["save_version"] = 9
return migrated
func _mark_dirty() -> void:
if (
_is_restoring
@ -1319,6 +1360,7 @@ func _restore_defaults() -> void:
WorldTimeServiceType.DEFAULT_START_HOUR
)
_world_weather.reset_persistent_state()
_world_seed = DEFAULT_WORLD_SEED
_jobs.reset_to_defaults()
_is_restoring = false
_is_dirty = false

View file

@ -30,6 +30,7 @@ readonly -a QUICK_TESTS=(
"tests/fishing_audio_validation.gd"
"tests/fishing_surface_validation.gd"
"tests/fur_pattern_validation.gd"
"tests/generated_world_runtime_validation.gd"
"tests/gathering_marker_surface_validation.gd"
"tests/inventory_storage_validation.gd"
"tests/keyboard_mouse_mapping_validation.gd"
@ -42,7 +43,9 @@ readonly -a QUICK_TESTS=(
"tests/shoreline_ambience_validation.gd"
"tests/surface_drawing_validation.gd"
"tests/tackle_order_validation.gd"
"tests/terrain_biome_validation.gd"
"tests/terrain_blender_material_validation.gd"
"tests/terrain_chunk_generator_validation.gd"
"tests/texture_sampling_validation.gd"
"tests/tree_gathering_prototype_validation.gd"
"tests/unified_inventory_validation.gd"

View file

@ -5,12 +5,15 @@ const DEFAULT_NAME: String = "NETfishing Dedicated Server"
const DEFAULT_BIND_ADDRESS: String = "*"
const DEFAULT_PORT: int = 7777
const DEFAULT_MAX_PLAYERS: int = 8
const DEFAULT_WORLD_SEED: int = 13001
const MAX_WORLD_SEED: int = 2147483646
const MAX_OPERATORS: int = 64
var server_name: String = DEFAULT_NAME
var bind_address: String = DEFAULT_BIND_ADDRESS
var port: int = DEFAULT_PORT
var max_players: int = DEFAULT_MAX_PLAYERS
var world_seed: int = DEFAULT_WORLD_SEED
var public_listing: bool = false
var discovery_url: String = ""
var data_directory: String = ""
@ -58,6 +61,7 @@ func _load_file(path: String) -> bool:
max_players = int(file.get_value(
"server", "max_players", max_players
))
world_seed = int(file.get_value("world", "seed", world_seed))
public_listing = bool(file.get_value(
"server", "public", public_listing
))
@ -84,6 +88,9 @@ func _apply_environment() -> void:
max_players = _environment_int(
"NETFISHING_SERVER_MAX_PLAYERS", max_players
)
world_seed = _environment_int(
"NETFISHING_WORLD_SEED", world_seed
)
public_listing = _environment_bool(
"NETFISHING_SERVER_PUBLIC", public_listing
)
@ -111,6 +118,10 @@ func _apply_arguments(arguments: PackedStringArray) -> void:
max_players = _parse_int(
argument.trim_prefix("--max-players="), max_players
)
elif argument.begins_with("--world-seed="):
world_seed = _parse_int(
argument.trim_prefix("--world-seed="), world_seed
)
elif argument.begins_with("--data-dir="):
data_directory = argument.trim_prefix("--data-dir=")
elif argument.begins_with("--discovery-url="):
@ -141,6 +152,8 @@ func _validate() -> void:
error_message = "Server port must be between 1 and 65535."
elif max_players < 1 or max_players > 128:
error_message = "Maximum players must be between 1 and 128."
elif world_seed <= 0 or world_seed > MAX_WORLD_SEED:
error_message = "World seed must be between 1 and %d." % MAX_WORLD_SEED
elif not data_directory.is_empty() and not data_directory.is_absolute_path():
error_message = "Server data directory must be an absolute path."
elif public_listing and not (

View file

@ -28,6 +28,7 @@ func _run() -> void:
file.set_value("server", "bind_address", "127.0.0.1")
file.set_value("server", "port", 17777)
file.set_value("server", "max_players", 12)
file.set_value("world", "seed", 928374)
file.set_value("server", "public", true)
file.set_value("server", "data_directory", "/tmp/configured-server")
file.set_value("discovery", "url", "https://discovery.netfishing.org/")
@ -44,6 +45,7 @@ func _run() -> void:
assert(str(configured.get("bind_address")) == "127.0.0.1")
assert(int(configured.get("port")) == 17777)
assert(int(configured.get("max_players")) == 12)
assert(int(configured.get("world_seed")) == 928374)
assert(bool(configured.get("public_listing")))
assert(str(configured.get("discovery_url")) == "https://discovery.netfishing.org")
assert(
@ -81,6 +83,12 @@ func _run() -> void:
invalid.call("_validate")
assert(not invalid.is_valid())
var invalid_seed := ConfigType.new()
invalid_seed.set("world_seed", 0)
invalid_seed.set("data_directory", "/tmp/invalid-seed-server")
invalid_seed.call("_validate")
assert(not invalid_seed.is_valid())
var discovery := DiscoveryClient.new()
assert(
str(discovery.call("_default_room_name", "River"))

View file

@ -106,7 +106,7 @@ func _run() -> void:
var hotbar_data: Dictionary = (parsed as Dictionary)["hotbar"]
assert(typeof(hotbar_data.get("fish_slots")) == TYPE_ARRAY)
assert(str((hotbar_data["fish_slots"] as Array)[1]) == fish_catch.catch_id)
assert(int((parsed as Dictionary)["save_version"]) == 8)
assert(int((parsed as Dictionary)["save_version"]) == 9)
assert(
int((parsed as Dictionary)["experience"]["total_experience"])
== 125
@ -185,7 +185,7 @@ func _run() -> void:
var invalid_state: Dictionary = valid_state.duplicate(true)
invalid_state["display_scale"] = 1000.0
assert(not NetworkFishShowcaseProtocol.validate_state(invalid_state))
assert(NetworkProtocol.PROTOCOL_VERSION == 8)
assert(NetworkProtocol.PROTOCOL_VERSION == 9)
assert(NetworkProtocol.ENET_CHANNEL_COUNT == 10)
assert(
NetworkProtocol.FISH_QUALITY_CAPABILITY

View file

@ -25,7 +25,7 @@ func _run() -> void:
_validate_mail_round_trip()
_validate_collection_mastery()
_validate_version_four_migration()
assert(NetworkProtocol.PROTOCOL_VERSION == 8)
assert(NetworkProtocol.PROTOCOL_VERSION == 9)
assert(NetworkProtocol.ENET_CHANNEL_COUNT == 10)
assert(NetworkProtocol.FISH_QUALITY_CAPABILITY == "fish_quality_v1")
print("Fish quality validation: PASS")
@ -433,7 +433,7 @@ func _validate_version_four_migration() -> void:
version_four,
4,
)
assert(int(migrated.get("save_version", -1)) == 8)
assert(int(migrated.get("save_version", -1)) == 9)
assert(int((migrated["experience"] as Dictionary)["total_experience"]) == 0)
assert(
is_equal_approx(
@ -441,6 +441,10 @@ func _validate_version_four_migration() -> void:
8.0,
)
)
assert(
int((migrated["world"] as Dictionary)["seed"])
== PlayerSaveManager.DEFAULT_WORLD_SEED
)
var catches: Array = migrated["inventory"]["catches"]
assert(int((catches[0] as Dictionary)["quality"]) == 0)
var collection: Dictionary = migrated["collection"]

View file

@ -39,19 +39,33 @@ func _run() -> void:
assert(not session.is_open_host())
assert(service != null and fishing_spot != null and player != null)
assert(player.hotbar.get_selected_item_id() == &"basic_fishing_rod")
var pond := main.get_node(
"TestWorld/Regions/StarterIslandRegion/WaterBodies/Pond"
var fresh_root := main.get_node(
"TestWorld/Regions/GeneratedWorldRegion/WaterBodies/FreshWaterBodies"
) as Node3D
var pond: WaterBodyAuthoring
for child: Node in fresh_root.get_children():
var candidate := child as WaterBodyAuthoring
if candidate != null and &"pond" in candidate.location_tags:
pond = candidate
break
assert(pond != null)
var pond_region := pond.get_node("FishingRegion") as FishableWaterRegion
assert(pond != null and pond_region != null)
assert(pond_region != null)
var pond_surface_y: float = pond_region.get_surface_height()
player.global_position = pond.global_position + Vector3(8.9, 1.44, 0.0)
player.global_position = pond.global_transform * Vector3(
pond.surface_size.x * 0.5 + 0.8,
1.44,
0.0,
)
var pond_direction := pond.global_position - player.global_position
pond_direction.y = 0.0
pond_direction = pond_direction.normalized()
var visuals := player.get_node("Visuals") as Node3D
visuals.rotation.y = PI * 0.5
visuals.global_rotation.y = atan2(-pond_direction.x, -pond_direction.z)
for _frame: int in 4:
await physics_frame
assert(player.get_facing_direction().dot(Vector3.LEFT) > 0.99)
assert(player.get_facing_direction().dot(pond_direction) > 0.99)
fishing_spot.call("_begin_aiming", player)
assert(fishing_spot.state == FishingSpotType.FishingState.AIMING_CAST)
@ -59,7 +73,9 @@ func _run() -> void:
fishing_spot.set("_cast_charge", 0.32)
fishing_spot.call("_update_cast_charge", 0.0)
var aimed_target: Vector3 = fishing_spot.get("_cast_target")
assert(aimed_target.x < player.global_position.x - 0.85)
var aimed_direction := aimed_target - player.global_position
aimed_direction.y = 0.0
assert(aimed_direction.normalized().dot(pond_direction) > 0.99)
assert(is_equal_approx(aimed_target.y, pond_surface_y))
assert(fishing_spot.is_target_fishable(aimed_target))
fishing_spot.call("_confirm_cast")

View file

@ -0,0 +1,510 @@
extends SceneTree
const RegionScene: PackedScene = preload(
"res://world/generation/generated_world_region.tscn"
)
const FIRST_SEED := 13001
const SECOND_SEED := 13004
const EXPECTED_PROP_IDS: Array[StringName] = [
&"prop_bridge",
&"prop_dock",
&"prop_log",
&"prop_log_post_1",
&"prop_log_post_2",
&"prop_mushroom",
&"prop_palm",
&"prop_pine",
&"prop_timber_1",
&"prop_timber_2",
&"prop_tree_1",
&"prop_tree_2",
&"prop_tree_3",
]
const EXPECTED_BIOME_IDS: Array[StringName] = [
&"biome_plains",
&"biome_forest",
&"biome_pine_forest",
&"biome_coast",
]
const PROCEDURAL_PROP_IDS: Array[StringName] = [
&"prop_log",
&"prop_mushroom",
&"prop_palm",
&"prop_pine",
&"prop_tree_1",
&"prop_tree_2",
&"prop_tree_3",
]
func _initialize() -> void:
call_deferred(&"_run")
func _run() -> void:
var region := RegionScene.instantiate() as GeneratedWorldRegion
assert(region != null)
root.add_child(region)
await process_frame
await physics_frame
var generator := region.get_node(
"Terrain/TerrainChunkGenerator"
) as TerrainChunkGenerator
assert(generator != null)
assert(region.get_generation_seed() == FIRST_SEED)
_validate_generated_region(region, generator)
var first_layout := generator.placement_keys()
var first_biomes := _biome_signature(region, generator)
var first_decorations := _decoration_signature(region)
assert(region.generate_world(FIRST_SEED))
assert(generator.placement_keys() == first_layout)
assert(_biome_signature(region, generator) == first_biomes)
assert(_decoration_signature(region) == first_decorations)
assert(region.generate_world(SECOND_SEED))
await process_frame
await physics_frame
await physics_frame
_validate_generated_region(region, generator)
assert(generator.placement_keys() != first_layout)
assert(_biome_signature(region, generator) != first_biomes)
region.queue_free()
await process_frame
print("Generated world runtime validation: PASS")
quit()
func _validate_generated_region(
region: GeneratedWorldRegion,
generator: TerrainChunkGenerator,
) -> void:
var placements := generator.placement_keys()
assert(placements.size() == 49)
assert(placements[24].begins_with("chunk_spawn@"))
assert(_placement_count(placements, "chunk_spawn") == 1)
assert(_placement_count(placements, "chunk_0001") >= 1)
assert(_placement_count(placements, "chunk_0002") >= 1)
assert(_placement_count(placements, "chunk_0003") >= 1)
assert(_placement_count(placements, "chunk_0004") >= 1)
assert(_placement_count(placements, "chunk_0005") >= 1)
assert(_placement_count(placements, "chunk_0006") >= 1)
assert(_placement_count(placements, "chunk_0007") >= 1)
assert(generator.get_generated_chunks_root().get_child_count() == 49)
assert(generator.get_primary_terrain_meshes().size() == 49)
var shop := region.get_node("Interactables/FishingShopWorld") as Node3D
var storage := region.get_node("Interactables/PlayerStorageBox") as Node3D
assert(shop != null and shop.has_node("Shopkeeper"))
assert(storage != null and storage.has_node("InteractionArea"))
assert(absf(shop.position.x) < 5.0 and absf(shop.position.z) < 5.0)
assert(absf(storage.position.x) < 5.0 and absf(storage.position.z) < 5.0)
assert(region.get_fishing_shop() != null)
assert(region.get_player_storage() != null)
assert(region.get_player_spawn_transform().origin.y > 0.0)
_validate_prop_catalog(region)
_validate_biome_catalog(region, generator)
var ocean := region.get_node("WaterBodies/OceanWater") as WaterBodyAuthoring
var fresh_root := region.get_node("WaterBodies/FreshWaterBodies") as Node3D
assert(ocean != null and fresh_root != null)
assert(ocean.water_type == WaterType.Type.SALT_WATER)
var fresh_placement_count := 0
for record: Dictionary in generator.placement_records():
var tags: PackedStringArray = record.get("tags", PackedStringArray())
if "coast" in tags:
_validate_ocean_facing_record(record, generator.grid_size)
if "fresh_water" in tags:
fresh_placement_count += 1
assert(fresh_root.get_child_count() == fresh_placement_count)
for child: Node in fresh_root.get_children():
var fresh := child as WaterBodyAuthoring
assert(fresh != null)
assert(fresh.water_type == WaterType.Type.FRESH_WATER)
assert(fresh.visible)
assert(fresh.surface_size.x < 10.0 or fresh.surface_size.y < 10.0)
assert(not fresh.visual_surface_enabled)
assert(not (fresh.get_node("VisualWater") as MeshInstance3D).visible)
_validate_authored_chunk_surfaces(region, generator)
var decorations := region.get_node("Decorations") as Node3D
var anchors := region.get_node(
"GatherableAnchors/ReachableTreeTrunks"
) as GatherableAnchorSet3D
assert(decorations != null and decorations.get_child_count() > 0)
assert(anchors != null)
assert(anchors.get_spawn_positions().size() > 0)
for child: Node in decorations.get_children():
var prop_id := StringName(child.get_meta(&"terrain_prop_id", &""))
assert(PROCEDURAL_PROP_IDS.has(prop_id))
var definition := region.get_prop_catalog().definition_for_id(prop_id)
assert(definition != null)
var biome_id := StringName(
child.get_meta(&"terrain_biome_id", &"")
)
var biome := region.get_biome_catalog().definition_for_id(biome_id)
assert(biome != null)
var biome_rule := biome.prop_rule_for_group(
definition.procedural_group
)
assert(biome_rule != null and biome_rule.allows_prop(definition))
var coordinate: Vector2i = child.get_meta(
&"terrain_chunk_coordinate",
Vector2i.ZERO,
)
var center := Vector2i(
generator.grid_size.x / 2,
generator.grid_size.y / 2,
)
var spawn_distance := (
absi(coordinate.x - center.x)
+ absi(coordinate.y - center.y)
)
assert(spawn_distance >= definition.minimum_spawn_chunk_distance)
_validate_decoration_transform(
region,
child as Node3D,
definition,
)
assert(_decoration_group_count(region, &"grass_tree") > 0)
assert(_decoration_group_count(region, &"sand_tree") > 0)
assert(
_decoration_biome_group_count(
region,
&"biome_forest",
&"grass_tree",
) >= 2
)
assert(
_decoration_biome_group_count(
region,
&"biome_pine_forest",
&"grass_tree",
) >= 2
)
func _validate_ocean_facing_record(
record: Dictionary,
grid_size: Vector2i,
) -> void:
var coordinate: Vector2i = record.get("coordinate", Vector2i.ZERO)
var ocean_edges := int(record.get("ocean_facing_edges", 0))
assert(ocean_edges != 0)
for edge_value: int in TerrainChunkTopology.Edge.values():
if (ocean_edges & (1 << edge_value)) == 0:
continue
var ocean_coordinate := (
coordinate
+ TerrainChunkTopology.grid_offset(
edge_value as TerrainChunkTopology.Edge
)
)
assert(
ocean_coordinate.x < 0
or ocean_coordinate.y < 0
or ocean_coordinate.x >= grid_size.x
or ocean_coordinate.y >= grid_size.y
)
func _validate_prop_catalog(region: GeneratedWorldRegion) -> void:
var catalog := region.get_prop_catalog()
assert(catalog != null)
assert(catalog.validation_errors().is_empty())
assert(catalog.definitions.size() == EXPECTED_PROP_IDS.size())
for stable_id: StringName in EXPECTED_PROP_IDS:
var definition := catalog.definition_for_id(stable_id)
assert(definition != null and definition.packed_scene != null)
var instance := definition.packed_scene.instantiate() as Node3D
assert(instance != null)
assert(instance.position.is_zero_approx())
assert(_find_mesh_instance(instance) != null)
instance.free()
func _validate_biome_catalog(
region: GeneratedWorldRegion,
generator: TerrainChunkGenerator,
) -> void:
var catalog := region.get_biome_catalog()
assert(catalog != null)
assert(catalog.validation_errors().is_empty())
assert(catalog.definitions.size() == EXPECTED_BIOME_IDS.size())
var counts: Dictionary[StringName, int] = {}
for record: Dictionary in generator.placement_records():
var coordinate: Vector2i = record.get("coordinate", Vector2i.ZERO)
var biome_id := region.get_biome_at(coordinate)
var tags: PackedStringArray = record.get("tags", PackedStringArray())
if "grass" in tags or "sand" in tags:
assert(biome_id != &"")
var biome := catalog.definition_for_id(biome_id)
assert(biome != null and biome.supports_chunk_tags(tags))
counts[biome_id] = counts.get(biome_id, 0) + 1
else:
assert(biome_id == &"")
for biome_id: StringName in EXPECTED_BIOME_IDS:
assert(counts.get(biome_id, 0) > 0)
var center := Vector2i(
generator.grid_size.x / 2,
generator.grid_size.y / 2,
)
assert(region.get_biome_at(center) == &"biome_plains")
for child: Node in generator.get_generated_chunks_root().get_children():
var coordinate: Vector2i = child.get_meta(
&"terrain_chunk_coordinate",
Vector2i.ZERO,
)
assert(
StringName(child.get_meta(&"terrain_biome_id", &""))
== region.get_biome_at(coordinate)
)
func _validate_decoration_transform(
region: GeneratedWorldRegion,
prop: Node3D,
definition: TerrainPropDefinition,
) -> void:
assert(prop != null)
assert(prop.scale.is_equal_approx(Vector3.ONE))
var visual_root := prop.get_node_or_null("Visual") as Node3D
var visual := _find_mesh_instance(visual_root)
var collision := prop.get_node_or_null(
"TrunkCollision/CollisionShape"
) as CollisionShape3D
assert(visual_root != null and visual_root.position.is_zero_approx())
assert(visual != null and visual.mesh != null)
if definition.has_collision():
assert(collision != null)
assert(
collision.shape is CylinderShape3D
or collision.shape is BoxShape3D
)
assert(collision.global_basis.get_scale().is_equal_approx(Vector3.ONE))
else:
assert(collision == null)
var query := PhysicsRayQueryParameters3D.create(
prop.global_position + Vector3.UP * 8.0,
prop.global_position + Vector3.DOWN * 8.0,
1,
)
var prop_body := prop.get_node_or_null(
"TrunkCollision",
) as CollisionObject3D
if prop_body != null:
query.exclude = [prop_body.get_rid()]
var hit := region.get_world_3d().direct_space_state.intersect_ray(query)
assert(
not hit.is_empty(),
"No terrain collision below %s (%s) at %s."
% [prop.name, definition.stable_id, prop.global_position],
)
var ground_position: Vector3 = hit["position"]
assert(absf(ground_position.y - prop.global_position.y) <= 0.01)
func _find_mesh_instance(root_node: Node) -> MeshInstance3D:
if root_node is MeshInstance3D:
return root_node as MeshInstance3D
if root_node == null:
return null
for child: Node in root_node.get_children():
var mesh_instance := _find_mesh_instance(child)
if mesh_instance != null:
return mesh_instance
return null
func _validate_authored_chunk_surfaces(
region: GeneratedWorldRegion,
generator: TerrainChunkGenerator,
) -> void:
var generated := generator.get_generated_chunks_root()
var found_beach := false
var found_pond := false
var found_grass_ocean_edge := false
var found_grass_beach_transition := false
var found_grass_ocean_corner := false
for chunk_root: Node in generated.get_children():
if chunk_root.name.begins_with("chunk_0002r"):
var beach := chunk_root.find_child(
"chunk_0002", true, false
) as MeshInstance3D
assert(beach != null and beach.mesh != null)
var material := beach.get_active_material(0)
assert(material != null and material.resource_name == "sand")
found_beach = true
elif chunk_root.name.begins_with("chunk_0004r"):
var pond := chunk_root.find_child(
"chunk_0004", true, false
) as MeshInstance3D
assert(pond != null and pond.mesh != null)
for surface_index: int in pond.mesh.get_surface_count():
var arrays := pond.mesh.surface_get_arrays(surface_index)
var normals := arrays[Mesh.ARRAY_NORMAL] as PackedVector3Array
for normal: Vector3 in normals:
assert(normal.y >= -0.001)
_validate_pond_collision(region, pond)
found_pond = true
elif chunk_root.name.begins_with("chunk_0005r"):
var grass_ocean_edge := chunk_root.find_child(
"chunk_0005", true, false
) as MeshInstance3D
assert(grass_ocean_edge != null and grass_ocean_edge.mesh != null)
var edge_materials := _material_names(grass_ocean_edge)
assert("grass_lite" in edge_materials)
assert("cliff_wall" in edge_materials)
found_grass_ocean_edge = true
elif chunk_root.name.begins_with("chunk_0006r"):
var grass_beach_transition := chunk_root.find_child(
"chunk_0006", true, false
) as MeshInstance3D
assert(
grass_beach_transition != null
and grass_beach_transition.mesh != null
)
var transition_materials := _material_names(
grass_beach_transition
)
assert("sand" in transition_materials)
assert("grass_lite" in transition_materials)
assert("cliff_wall" in transition_materials)
found_grass_beach_transition = true
elif chunk_root.name.begins_with("chunk_0007r"):
var grass_ocean_corner := chunk_root.find_child(
"chunk_0007", true, false
) as MeshInstance3D
assert(grass_ocean_corner != null and grass_ocean_corner.mesh != null)
var corner_materials := _material_names(grass_ocean_corner)
assert("grass_lite" in corner_materials)
assert("cliff_wall" in corner_materials)
found_grass_ocean_corner = true
assert(found_beach)
assert(found_pond)
assert(found_grass_ocean_edge)
assert(found_grass_beach_transition)
assert(found_grass_ocean_corner)
func _material_names(mesh_instance: MeshInstance3D) -> PackedStringArray:
var result := PackedStringArray()
for surface_index: int in mesh_instance.mesh.get_surface_count():
var material := mesh_instance.get_active_material(surface_index)
if material != null:
result.append(material.resource_name)
return result
func _validate_pond_collision(
region: GeneratedWorldRegion,
pond: MeshInstance3D,
) -> void:
var best_centroid := Vector3.ZERO
var best_height := -INF
for surface_index: int in pond.mesh.get_surface_count():
var arrays := pond.mesh.surface_get_arrays(surface_index)
var vertices := arrays[Mesh.ARRAY_VERTEX] as PackedVector3Array
var normals := arrays[Mesh.ARRAY_NORMAL] as PackedVector3Array
var indices := arrays[Mesh.ARRAY_INDEX] as PackedInt32Array
var triangle_count := (
indices.size() / 3 if not indices.is_empty() else vertices.size() / 3
)
for triangle_index: int in triangle_count:
var offset := triangle_index * 3
var index_a: int = indices[offset] if not indices.is_empty() else offset
var index_b: int = (
indices[offset + 1] if not indices.is_empty() else offset + 1
)
var index_c: int = (
indices[offset + 2] if not indices.is_empty() else offset + 2
)
var a: Vector3 = vertices[index_a]
var b: Vector3 = vertices[index_b]
var c: Vector3 = vertices[index_c]
var normal := (
(normals[index_a] + normals[index_b] + normals[index_c]) / 3.0
).normalized()
var centroid := (a + b + c) / 3.0
if normal.y > 0.5 and centroid.y > best_height:
best_height = centroid.y
best_centroid = centroid
assert(best_height > -INF)
var target := pond.global_transform * best_centroid
var query := PhysicsRayQueryParameters3D.create(
target + Vector3.UP * 2.0,
target + Vector3.DOWN * 2.0,
1,
)
var hit := region.get_world_3d().direct_space_state.intersect_ray(query)
assert(not hit.is_empty())
var collider := hit.get("collider") as Node
assert(collider != null and collider.get_parent() == pond)
func _decoration_group_count(
region: GeneratedWorldRegion,
group: StringName,
) -> int:
var count := 0
var decorations := region.get_node("Decorations") as Node3D
for child: Node in decorations.get_children():
var stable_id := StringName(child.get_meta(&"terrain_prop_id", &""))
var definition := region.get_prop_catalog().definition_for_id(stable_id)
if definition != null and definition.procedural_group == group:
count += 1
return count
func _decoration_biome_group_count(
region: GeneratedWorldRegion,
biome_id: StringName,
group: StringName,
) -> int:
var count := 0
var decorations := region.get_node("Decorations") as Node3D
for child: Node in decorations.get_children():
if StringName(child.get_meta(&"terrain_biome_id", &"")) != biome_id:
continue
var prop_id := StringName(child.get_meta(&"terrain_prop_id", &""))
var definition := region.get_prop_catalog().definition_for_id(prop_id)
if definition != null and definition.procedural_group == group:
count += 1
return count
func _biome_signature(
region: GeneratedWorldRegion,
generator: TerrainChunkGenerator,
) -> PackedStringArray:
var result := PackedStringArray()
for row: int in generator.grid_size.y:
for column: int in generator.grid_size.x:
result.append(
String(region.get_biome_at(Vector2i(column, row)))
)
return result
func _placement_count(keys: PackedStringArray, stable_id: String) -> int:
var count := 0
for key: String in keys:
if key.begins_with(stable_id + "@"):
count += 1
return count
func _decoration_signature(region: GeneratedWorldRegion) -> PackedStringArray:
var result := PackedStringArray()
var decorations := region.get_node("Decorations") as Node3D
for child: Node in decorations.get_children():
var prop := child as Node3D
result.append("%s:%s:%s" % [
prop.name,
prop.position,
prop.rotation,
])
return result

View file

@ -0,0 +1 @@
uid://opamwsr0t7px

View file

@ -22,7 +22,8 @@ func _run() -> void:
var save_manager := main.get("_save_manager") as PlayerSaveManager
var player := main.get("_player") as Player
assert(save_manager != null and player != null)
assert(save_manager.initialize_new_game())
const TEST_WORLD_SEED := 918273
assert(save_manager.initialize_new_game(TEST_WORLD_SEED))
save_manager.set_autosave_enabled(true)
assert(player.bag.add_item(&"art_kit"))
assert(player.bag.add_item(&"coffee"))
@ -39,12 +40,14 @@ func _run() -> void:
save_file.close()
assert(typeof(parsed) == TYPE_DICTIONARY)
var records: Array = (parsed as Dictionary)["bag"]["items"]
assert(int((parsed as Dictionary)["world"]["seed"]) == TEST_WORLD_SEED)
assert(_saved_slot(records, &"basic_fishing_rod") == 14)
assert(_saved_slot(records, &"coffee") == 12)
assert(player.bag.move_item_to_storage_slot(&"basic_fishing_rod", 0))
assert(player.bag.move_item_to_storage_slot(&"coffee", 0))
assert(save_manager.load_player_data())
assert(save_manager.get_world_seed() == TEST_WORLD_SEED)
assert(player.bag.get_storage_slot(&"basic_fishing_rod") == 14)
assert(player.bag.get_storage_slot(&"coffee") == 12)

View file

@ -203,7 +203,7 @@ func _run() -> void:
save_file.close()
assert(typeof(parsed) == TYPE_DICTIONARY)
var save_data: Dictionary = parsed
assert(int(save_data.get("save_version", -1)) == 8)
assert(int(save_data.get("save_version", -1)) == 9)
assert(PlayerJobService.validate_save_data(save_data.get("jobs", {})))
_validate_pause_session_switch(main, session)

View file

@ -4,11 +4,6 @@ const MainScene: PackedScene = preload("res://main/main.tscn")
const RuntimePerformanceProfileType = preload(
"res://main/runtime_performance_profile.gd"
)
const FOLIAGE_WIND_SHADER: Shader = preload(
"res://world/materials/foliage_wind.gdshader"
)
func _initialize() -> void:
call_deferred("_run")
@ -77,27 +72,19 @@ func _validate_main_profile(main: Node) -> void:
var screen_grid := pixelation.get_node("ScreenGrid") as ColorRect
assert(screen_grid != null and not screen_grid.visible)
var pond := main.get_node(
"TestWorld/Regions/StarterIslandRegion/WaterBodies/Pond/VisualWater"
) as MeshInstance3D
var pond := _first_fresh_water_visual(main)
var ocean := main.get_node(
"TestWorld/Regions/StarterIslandRegion/WaterBodies/Ocean/VisualWater"
"TestWorld/Regions/GeneratedWorldRegion/WaterBodies/OceanWater/VisualWater"
) as MeshInstance3D
assert(pond.material_override is StandardMaterial3D)
assert(not pond.visible)
assert(ocean.material_override is StandardMaterial3D)
assert(not pond.material_override is ShaderMaterial)
assert(not ocean.material_override is ShaderMaterial)
assert(ocean is WaterSurfaceMotion)
assert(not ocean.is_processing())
assert(is_zero_approx(ocean.position.y))
_validate_ocean_fishing_coverage(main, ocean)
var island := main.get_node(
"TestWorld/Regions/StarterIslandRegion"
) as StarterIslandRegion
assert(island != null and not island.is_foliage_wind_enabled())
assert(_count_foliage_wind_overrides(
island.get_node("Terrain/Visual")
) == 0)
var region := main.get_node(
"TestWorld/Regions/GeneratedWorldRegion"
) as GeneratedWorldRegion
assert(region != null)
assert(region.get_node("Decorations").get_child_count() > 0)
var title_background := main.get_node("%TitleBackground") as ColorRect
var title_material := title_background.material as ShaderMaterial
@ -143,24 +130,18 @@ func _validate_normal_profile(main: Node) -> void:
assert(is_equal_approx(root.scaling_3d_scale, 1.0))
var pixelation: Node = main.get_node("%WorldPixelationPostprocess")
assert(not bool(pixelation.call("is_light_performance_profile")))
var pond := main.get_node(
"TestWorld/Regions/StarterIslandRegion/WaterBodies/Pond/VisualWater"
) as MeshInstance3D
var pond := _first_fresh_water_visual(main)
var ocean := main.get_node(
"TestWorld/Regions/StarterIslandRegion/WaterBodies/Ocean/VisualWater"
"TestWorld/Regions/GeneratedWorldRegion/WaterBodies/OceanWater/VisualWater"
) as MeshInstance3D
assert(pond.material_override is ShaderMaterial)
assert(not pond.visible)
assert(ocean.material_override is ShaderMaterial)
assert(ocean is WaterSurfaceMotion)
assert(ocean.is_processing())
_validate_ocean_fishing_coverage(main, ocean)
var island := main.get_node(
"TestWorld/Regions/StarterIslandRegion"
) as StarterIslandRegion
assert(island != null and island.is_foliage_wind_enabled())
assert(_count_foliage_wind_overrides(
island.get_node("Terrain/Visual")
) > 0)
var region := main.get_node(
"TestWorld/Regions/GeneratedWorldRegion"
) as GeneratedWorldRegion
assert(region != null)
assert(region.get_node("Decorations").get_child_count() > 0)
var game_ui := main.get_node("%GameUI") as GameUI
var title_screen := game_ui.get_title_screen()
assert(title_screen != null)
@ -211,6 +192,14 @@ func _validate_normal_profile(main: Node) -> void:
)
func _first_fresh_water_visual(main: Node) -> MeshInstance3D:
var root := main.get_node(
"TestWorld/Regions/GeneratedWorldRegion/WaterBodies/FreshWaterBodies"
) as Node3D
assert(root != null and root.get_child_count() > 0)
return root.get_child(0).get_node("VisualWater") as MeshInstance3D
func _validate_ocean_fishing_coverage(
main: Node,
ocean: MeshInstance3D,
@ -218,34 +207,19 @@ func _validate_ocean_fishing_coverage(
var ocean_mesh := ocean.mesh as PlaneMesh
assert(ocean_mesh != null)
var shape_node := main.get_node(
"TestWorld/Regions/StarterIslandRegion/WaterBodies/Ocean/"
+ "FishingRegions/OceanFishingRegion/Shape"
"TestWorld/Regions/GeneratedWorldRegion/WaterBodies/OceanWater/"
+ "FishingRegion/Shape"
) as CollisionShape3D
assert(shape_node != null)
var fishing_shape := shape_node.shape as BoxShape3D
assert(fishing_shape != null)
assert(fishing_shape.size.is_equal_approx(Vector3(
ocean_mesh.size.x,
2.0,
4.0,
ocean_mesh.size.y,
)))
func _count_foliage_wind_overrides(root_node: Node) -> int:
var count: int = 0
var mesh_instance := root_node as MeshInstance3D
if mesh_instance != null and mesh_instance.mesh != null:
for surface_index: int in mesh_instance.mesh.get_surface_count():
var material := mesh_instance.get_surface_override_material(
surface_index
) as ShaderMaterial
if material != null and material.shader == FOLIAGE_WIND_SHADER:
count += 1
for child: Node in root_node.get_children():
count += _count_foliage_wind_overrides(child)
return count
func _stop_audio_players(root_node: Node) -> void:
if root_node is AudioStreamPlayer:
(root_node as AudioStreamPlayer).stop()

View file

@ -0,0 +1,124 @@
extends SceneTree
const CATALOG: TerrainChunkCatalog = preload(
"res://world/generation/chunks/terrain_chunk_catalog.tres"
)
const BIOME_CATALOG: TerrainBiomeCatalog = preload(
"res://world/generation/biomes/terrain_biome_catalog.tres"
)
func _initialize() -> void:
call_deferred(&"_run")
func _run() -> void:
var generator := _make_generator()
root.add_child(generator)
if not generator._prepare_catalog():
generator.free()
quit(1)
return
var variants: Array[TerrainChunkVariant] = []
variants.assign(generator._solver_variants)
print(
"Terrain chunk rotations: %d authored, %d distinct constraints"
% [generator._variants.size(), variants.size()]
)
for variant: TerrainChunkVariant in variants:
print("\n", variant.stable_key())
var ocean_edges := variant.rotated_edge_mask(
variant.definition.ocean_facing_edges
)
for edge_value: int in TerrainChunkTopology.Edge.values():
var edge := edge_value as TerrainChunkTopology.Edge
var matches := PackedStringArray()
for other: TerrainChunkVariant in variants:
var opposite := TerrainChunkTopology.opposite_edge(edge)
if generator._edges_are_compatible(
variant,
edge,
other,
opposite,
):
matches.append(
"%s.%s"
% [
other.stable_key(),
TerrainChunkTopology.edge_name(opposite),
]
)
print(
" %s%s [%d points]: %s"
% [
TerrainChunkTopology.edge_name(edge),
(
" [ocean]"
if (ocean_edges & (1 << edge_value)) != 0
else ""
),
variant.profile(edge).points.size(),
", ".join(matches) if not matches.is_empty() else "NONE",
]
)
if generator.generate():
var summary := generator._build_summary()
print(
(
"\nSeeded diagnostic layout %s "
+ "(%d backtracks, %d repeated edges):"
)
% [
str(summary["layout_fingerprint"]).substr(0, 12),
int(summary["backtracks"]),
int(summary["adjacent_repeat_edges"]),
]
)
var keys := generator.placement_keys()
for row: int in generator.grid_size.y:
var cells := PackedStringArray()
for column: int in generator.grid_size.x:
cells.append(keys[row * generator.grid_size.x + column])
print(" ", " ".join(cells))
var biomes := TerrainBiomeAssigner.assign(
BIOME_CATALOG,
generator.placement_records(),
generator.generation_seed,
)
print("\nBiome layout:")
for row: int in generator.grid_size.y:
var cells := PackedStringArray()
for column: int in generator.grid_size.x:
var biome_id: StringName = biomes.get(
Vector2i(column, row),
&"",
)
cells.append(String(biome_id).trim_prefix("biome_"))
print(" ", " ".join(cells))
generator.free()
quit(0)
func _make_generator() -> TerrainChunkGenerator:
var generator := TerrainChunkGenerator.new()
generator.catalog = CATALOG
generator.grid_size = Vector2i(7, 7)
generator.generation_seed = 13001
generator.generate_on_ready = false
generator.build_collision = false
generator.force_center_chunk_id = &"chunk_spawn"
generator.required_chunk_ids = PackedStringArray(
[
"chunk_spawn",
"chunk_0001",
"chunk_0002",
"chunk_0003",
"chunk_0004",
"chunk_0005",
"chunk_0006",
"chunk_0007",
]
)
return generator

View file

@ -0,0 +1 @@
uid://di4yaor4f3s4e

View file

@ -0,0 +1,37 @@
extends Node3D
@onready var _generator: TerrainChunkGenerator = $TerrainChunkGenerator
@onready var _status: Label = $Instructions/Status
func _unhandled_input(event: InputEvent) -> void:
if event.is_action_pressed(&"ui_cancel"):
get_viewport().set_input_as_handled()
get_tree().quit()
return
var key_event := event as InputEventKey
if (
key_event != null
and key_event.pressed
and not key_event.echo
and key_event.physical_keycode == KEY_R
):
get_viewport().set_input_as_handled()
_generator.generation_seed += 1
_generator.generate()
func _on_generation_completed(summary: Dictionary) -> void:
_status.text = (
"Terrain generator diagnostic • seed %d%d chunks • "
+ "%d rotations / %d constraints • %d backtracks • "
+ "%d repeated edges\nlayout %s • R: regenerate • Esc/B: close"
) % [
int(summary["seed"]),
int(summary["chunk_count"]),
int(summary["variant_count"]),
int(summary["solver_variant_count"]),
int(summary["backtracks"]),
int(summary["adjacent_repeat_edges"]),
str(summary["layout_fingerprint"]).substr(0, 12),
]

View file

@ -0,0 +1 @@
uid://0dmjswsm038h

View file

@ -0,0 +1,46 @@
[gd_scene load_steps=7 format=3]
[ext_resource type="Script" path="res://tests/manual/terrain_chunk_generator/terrain_chunk_generator_test.gd" id="1_test"]
[ext_resource type="Script" path="res://world/generation/terrain_chunk_generator.gd" id="2_generator"]
[ext_resource type="Resource" path="res://world/generation/chunks/terrain_chunk_catalog.tres" id="3_catalog"]
[ext_resource type="PackedScene" path="res://player/player.tscn" id="4_player"]
[ext_resource type="Environment" path="res://world/environment/netfishing_environment.tres" id="5_environment"]
[node name="TerrainChunkGeneratorTest" type="Node3D"]
script = ExtResource("1_test")
[node name="WorldEnvironment" type="WorldEnvironment" parent="."]
environment = ExtResource("5_environment")
[node name="Sun" type="DirectionalLight3D" parent="."]
rotation_degrees = Vector3(-54, -32, 0)
light_color = Color(0.88, 0.94, 0.96, 1)
light_energy = 1.0
shadow_enabled = true
[node name="TerrainChunkGenerator" type="Node3D" parent="."]
script = ExtResource("2_generator")
catalog = ExtResource("3_catalog")
grid_size = Vector2i(7, 7)
generation_seed = 13001
build_collision = true
show_chunk_labels = false
force_center_chunk_id = &"chunk_spawn"
required_chunk_ids = PackedStringArray("chunk_spawn", "chunk_0001", "chunk_0002", "chunk_0003", "chunk_0004", "chunk_0005", "chunk_0006", "chunk_0007")
[node name="Player" parent="." instance=ExtResource("4_player")]
position = Vector3(0, 0.3, 0)
[node name="Instructions" type="CanvasLayer" parent="."]
layer = 20
[node name="Status" type="Label" parent="Instructions"]
offset_left = 18.0
offset_top = 18.0
offset_right = 930.0
offset_bottom = 74.0
text = "Terrain generator diagnostic\nR: regenerate • Esc/B: close"
theme_override_colors/font_color = Color(0.76, 0.88, 0.9, 1)
theme_override_font_sizes/font_size = 18
[connection signal="generation_completed" from="TerrainChunkGenerator" to="." method="_on_generation_completed"]

View file

@ -143,11 +143,15 @@ func _validate_save_migration() -> void:
version_five,
5,
)
assert(int(migrated.get("save_version", -1)) == 8)
assert(int(migrated.get("save_version", -1)) == 9)
var experience_data: Dictionary = migrated.get("experience", {})
assert(int(experience_data.get("total_experience", -1)) == 0)
var world_data: Dictionary = migrated.get("world", {})
assert(is_equal_approx(float(world_data.get("time_hours", -1.0)), 8.0))
assert(
int(world_data.get("seed", 0))
== PlayerSaveManager.DEFAULT_WORLD_SEED
)
assert(
PlayerJobService.validate_save_data(migrated.get("jobs", {}))
)

View file

@ -0,0 +1,135 @@
extends SceneTree
const BIOME_CATALOG: TerrainBiomeCatalog = preload(
"res://world/generation/biomes/terrain_biome_catalog.tres"
)
const PROP_CATALOG: TerrainPropCatalog = preload(
"res://world/generation/props/terrain_prop_catalog.tres"
)
const EXPECTED_BIOMES: Array[StringName] = [
&"biome_plains",
&"biome_forest",
&"biome_pine_forest",
&"biome_coast",
]
const TEST_SEED := 13001
var _failures: Array[String] = []
func _initialize() -> void:
call_deferred(&"_run")
func _run() -> void:
_validate_catalog()
_validate_assignment()
_finish()
func _validate_catalog() -> void:
for error: String in BIOME_CATALOG.validation_errors():
_check(false, error)
_check(
BIOME_CATALOG.definitions.size() == EXPECTED_BIOMES.size(),
"The biome catalog must contain all initial authored profiles.",
)
for biome_id: StringName in EXPECTED_BIOMES:
var biome := BIOME_CATALOG.definition_for_id(biome_id)
_check(biome != null, "The biome catalog is missing %s." % biome_id)
if biome == null:
continue
for rule: TerrainBiomePropRule in biome.prop_rules:
for prop_id_value: String in rule.allowed_prop_ids:
var prop := PROP_CATALOG.definition_for_id(
StringName(prop_id_value)
)
_check(
prop != null and rule.allows_prop(prop),
"%s has an invalid %s prop rule."
% [biome_id, prop_id_value],
)
func _validate_assignment() -> void:
var records := _synthetic_records()
var first := TerrainBiomeAssigner.assign(
BIOME_CATALOG,
records,
TEST_SEED,
)
var second := TerrainBiomeAssigner.assign(
BIOME_CATALOG,
records,
TEST_SEED,
)
_check(first == second, "Biome assignment must be deterministic by seed.")
_check(
TerrainBiomeAssigner.fingerprint(first).length() == 64,
"Biome assignments must expose a SHA-256 fingerprint.",
)
var counts := TerrainBiomeAssigner.counts(first)
for biome_id: StringName in EXPECTED_BIOMES:
_check(
counts.get(biome_id, 0) > 0,
"The synthetic map must contain %s." % biome_id,
)
_check(
first.get(Vector2i(3, 3), &"") == &"biome_plains",
"The spawn-tagged center must remain plains.",
)
for record: Dictionary in records:
var coordinate: Vector2i = record["coordinate"]
var tags: PackedStringArray = record["tags"]
var biome_id: StringName = first.get(coordinate, &"")
if "fresh_water" in tags:
_check(
biome_id == &"",
"Freshwater-only chunks must not inherit a land-prop biome.",
)
continue
var biome := BIOME_CATALOG.definition_for_id(biome_id)
_check(
biome != null and biome.supports_chunk_tags(tags),
"Biome %s does not support chunk %s."
% [biome_id, coordinate],
)
func _synthetic_records() -> Array[Dictionary]:
var result: Array[Dictionary] = []
for row: int in 7:
for column: int in 7:
var coordinate := Vector2i(column, row)
var tags := PackedStringArray(["land", "walkable", "grass"])
if row == 0:
tags = PackedStringArray(["land", "walkable", "sand"])
elif coordinate == Vector2i(0, 6):
tags = PackedStringArray([
"land",
"walkable",
"fresh_water",
"pond",
])
elif coordinate == Vector2i(3, 3):
tags.append("spawn")
result.append({
"coordinate": coordinate,
"tags": tags,
})
return result
func _check(condition: bool, message: String) -> void:
if not condition:
_failures.append(message)
func _finish() -> void:
if _failures.is_empty():
print("Terrain biome validation: PASS")
quit(0)
return
for failure: String in _failures:
printerr("Terrain biome validation: ", failure)
quit(1)

View file

@ -0,0 +1 @@
uid://cxioi62f6oap0

View file

@ -0,0 +1,850 @@
extends SceneTree
const CATALOG: TerrainChunkCatalog = preload(
"res://world/generation/chunks/terrain_chunk_catalog.tres"
)
const EXPECTED_IDS: Array[String] = [
"chunk_0000",
"chunk_0001",
"chunk_0002",
"chunk_0003",
"chunk_0004",
"chunk_0005",
"chunk_0006",
"chunk_0007",
"chunk_spawn",
]
const REQUIRED_IDS: Array[String] = [
"chunk_spawn",
"chunk_0001",
"chunk_0002",
"chunk_0003",
"chunk_0004",
"chunk_0005",
"chunk_0006",
"chunk_0007",
]
const TEST_SEED := 13001
const CONNECTOR_STRESS_SEED := 287
const EXPECTED_SOLVER_VARIANT_COUNT := 27
const EXPECTED_VARIANT_COUNTS: Dictionary[StringName, int] = {
&"chunk_0000": 4,
&"chunk_0001": 4,
&"chunk_0002": 4,
&"chunk_0003": 4,
&"chunk_0004": 4,
&"chunk_0005": 4,
&"chunk_0006": 4,
&"chunk_0007": 4,
&"chunk_spawn": 1,
}
var _failures: Array[String] = []
func _initialize() -> void:
call_deferred(&"_run")
func _run() -> void:
_validate_catalog()
_validate_profiles()
_validate_generation()
_finish()
func _validate_catalog() -> void:
for error: String in CATALOG.validation_errors():
_check(false, error)
_check(
CATALOG.definitions.size() == EXPECTED_IDS.size(),
"Catalog must contain the authored chunks and spawn definition.",
)
for stable_id: String in EXPECTED_IDS:
var definition := CATALOG.definition_for_id(StringName(stable_id))
_check(definition != null, "Catalog is missing %s." % stable_id)
if definition == null:
continue
_check(
definition.packed_scene != null,
"%s must reference an imported GLB." % stable_id,
)
var grass := CATALOG.definition_for_id(&"chunk_0000")
var sand := CATALOG.definition_for_id(&"chunk_0001")
var beach := CATALOG.definition_for_id(&"chunk_0002")
var grass_ocean_edge := CATALOG.definition_for_id(&"chunk_0005")
var grass_beach_transition := CATALOG.definition_for_id(&"chunk_0006")
var grass_ocean_corner := CATALOG.definition_for_id(&"chunk_0007")
var spawn := CATALOG.definition_for_id(&"chunk_spawn")
_check(
(
grass != null
and sand != null
and beach != null
and grass_ocean_edge != null
and grass_beach_transition != null
and grass_ocean_corner != null
and spawn != null
),
"The authored biome-rule definitions must be available.",
)
if (
grass != null
and sand != null
and beach != null
and grass_ocean_edge != null
and grass_beach_transition != null
and grass_ocean_corner != null
and spawn != null
):
_check(
sand.allows_non_water_neighbor(beach),
"Flat sand must permit an adjacent authored beach.",
)
_check(
sand.allows_non_water_neighbor(grass),
"Flat sand must permit the inland transition to grass.",
)
_check(
grass.allows_non_water_neighbor(sand),
"Grass must permit the inland side of a flat-sand transition.",
)
_check(
"coast" in sand.required_neighbor_tags,
"Every flat-sand chunk must remain attached to an authored beach.",
)
_check(
spawn.minimum_required_neighbors == 3,
"Spawn must retain its three-sided safe-grass requirement.",
)
_check(
beach.ocean_facing_edges
== (1 << int(TerrainChunkTopology.Edge.EAST)),
"The authored beach must descend eastward into ocean before rotation.",
)
_check(
grass_ocean_edge.ocean_facing_edges
== (1 << int(TerrainChunkTopology.Edge.EAST)),
"The authored grass cliff must face eastward ocean before rotation.",
)
_check(
grass_beach_transition.ocean_facing_edges
== (1 << int(TerrainChunkTopology.Edge.EAST)),
"The authored grass/beach join must face eastward ocean before rotation.",
)
_check(
grass_beach_transition.minimum_required_neighbors == 2
and "coast" in grass_beach_transition.required_neighbor_tags,
"The grass/beach join must remain between two coastline chunks.",
)
_check(
grass_beach_transition.allows_non_water_neighbor_on_edge(
grass,
TerrainChunkTopology.Edge.WEST,
)
and not grass_beach_transition.allows_non_water_neighbor_on_edge(
sand,
TerrainChunkTopology.Edge.WEST,
),
"The grass-backed transition edge must accept grass and reject sand.",
)
_check(
grass_beach_transition.allows_non_water_neighbor_on_edge(
beach,
TerrainChunkTopology.Edge.NORTH,
)
and grass_beach_transition.allows_non_water_neighbor_on_edge(
grass_ocean_edge,
TerrainChunkTopology.Edge.SOUTH,
),
"The transition's coastline seams must retain their authored roles.",
)
_check(
not grass_ocean_edge.allows_non_water_neighbor_on_edge(
sand,
TerrainChunkTopology.Edge.WEST,
),
"The straight grass coast's inland edge must reject sand.",
)
_check(
grass_ocean_corner.ocean_facing_edges
== (
(1 << int(TerrainChunkTopology.Edge.EAST))
| (1 << int(TerrainChunkTopology.Edge.SOUTH))
),
"The authored grass corner must expose east and south to ocean.",
)
_check(
grass_ocean_corner.minimum_required_neighbors == 2
and (
"grass_ocean_edge"
in grass_ocean_corner.required_neighbor_tags
),
"The grass corner must join two straight grass ocean edges.",
)
_check(
grass_ocean_corner.allows_non_water_neighbor_on_edge(
grass_ocean_edge,
TerrainChunkTopology.Edge.NORTH,
)
and grass_ocean_corner.allows_non_water_neighbor_on_edge(
grass_ocean_edge,
TerrainChunkTopology.Edge.WEST,
),
"Both corner land seams must accept straight grass ocean edges.",
)
func _validate_profiles() -> void:
for definition: TerrainChunkDefinition in CATALOG.definitions:
var variants := TerrainChunkAnalyzer.create_variants(
definition,
CATALOG.chunk_size,
)
_check(
not variants.is_empty(),
"%s must produce at least one rotation variant."
% definition.stable_id,
)
for variant: TerrainChunkVariant in variants:
_check(
variant.edge_profiles.size() == 4,
"%s must expose four edge profiles." % variant.stable_key(),
)
var ocean_edges := variant.rotated_edge_mask(
definition.ocean_facing_edges
)
for edge_index: int in variant.edge_profiles.size():
var profile := variant.edge_profiles[edge_index]
_check(
(
not profile.points.is_empty()
or (ocean_edges & (1 << edge_index)) != 0
),
(
"%s has an empty non-ocean edge profile."
% variant.stable_key()
),
)
func _validate_generation() -> void:
var first := _make_generator()
root.add_child(first)
var first_solved := first.generate()
_check(first_solved, "Generator must solve the prototype 7x7 grid.")
if not first_solved:
first.free()
return
var first_keys := first.placement_keys()
_check(first_keys.size() == 49, "Generator must place exactly 49 chunks.")
for stable_id: String in REQUIRED_IDS:
_check(
_placements_contain(first_keys, stable_id),
"Generated diagnostic must contain %s." % stable_id,
)
_check(
_all_neighbor_edges_match(first),
"Every generated neighboring edge must match.",
)
_check(
_count_placements(first_keys, "chunk_spawn") == 1,
"Generated layouts must contain exactly one spawn chunk.",
)
_check(
first_keys[24].begins_with("chunk_spawn@"),
"The spawn chunk must occupy the center cell.",
)
_validate_layout_rules(first)
_validate_authored_rotations(first)
_validate_generation_summary(first)
var first_generated_root := first.get_generated_chunks_root()
_check(
first.generate(),
"Repeated generation on one generator must solve.",
)
_check(
first.placement_keys() == first_keys,
"Repeated generation on one generator must remain deterministic.",
)
_check(
first.get_generated_chunks_root() != first_generated_root,
"Successful regeneration must replace the generated scene atomically.",
)
var second := _make_generator()
root.add_child(second)
var second_solved := second.generate()
_check(second_solved, "Repeated deterministic generation must solve.")
if not second_solved:
first.free()
second.free()
return
_check(
second.placement_keys() == first_keys,
"The same seed and catalog must produce the same layout.",
)
var restored := _make_generator()
restored.generation_seed = TEST_SEED + 999
root.add_child(restored)
_check(
restored.generate_from_placement_keys(first_keys),
"A resolved placement manifest must rebuild successfully.",
)
_check(
restored.placement_keys() == first_keys,
"A resolved placement manifest must preserve exact rotations.",
)
first.free()
second.free()
restored.free()
for seed_offset: int in range(1, 6):
var generator := _make_generator()
generator.generation_seed = TEST_SEED + seed_offset
root.add_child(generator)
var solved := generator.generate()
_check(
solved,
"Generator must solve seed %d." % generator.generation_seed,
)
if solved:
var keys := generator.placement_keys()
for stable_id: String in REQUIRED_IDS:
_check(
_placements_contain(keys, stable_id),
"Seed %d must contain %s."
% [generator.generation_seed, stable_id],
)
_check(
_all_neighbor_edges_match(generator),
"Seed %d has a mismatched neighboring edge."
% generator.generation_seed,
)
_validate_layout_rules(generator)
generator.free()
var connector_stress := _make_generator()
connector_stress.generation_seed = CONNECTOR_STRESS_SEED
connector_stress.maximum_backtracks = 100
root.add_child(connector_stress)
_check(
connector_stress.generate(),
"Connector propagation must solve the known stress seed.",
)
_check(
connector_stress._backtrack_count < 100,
"Connector propagation must reject unsupported branches early.",
)
connector_stress.free()
func _validate_authored_rotations(generator: TerrainChunkGenerator) -> void:
var counts: Dictionary[StringName, int] = {}
for variant: TerrainChunkVariant in generator._variants:
var stable_id := variant.definition.stable_id
counts[stable_id] = counts.get(stable_id, 0) + 1
for stable_id: StringName in EXPECTED_VARIANT_COUNTS:
_check(
counts.get(stable_id, 0) == EXPECTED_VARIANT_COUNTS[stable_id],
"%s must preserve every explicitly allowed rotation." % stable_id,
)
var stream := CATALOG.definition_for_id(&"chunk_0003")
_check(stream != null, "The stream definition must be available.")
if stream == null:
return
for quarter_turns: int in 4:
var stream_variant := _find_variant(
generator,
&"chunk_0003",
quarter_turns,
)
_check(
stream_variant != null,
"The stream must support rotation %d." % quarter_turns,
)
if stream_variant == null:
continue
var expected_inlet := 1 << int(TerrainChunkTopology.rotated_edge(
TerrainChunkTopology.Edge.NORTH,
quarter_turns,
))
var expected_outlet := 1 << int(TerrainChunkTopology.rotated_edge(
TerrainChunkTopology.Edge.SOUTH,
quarter_turns,
))
_check(
stream_variant.rotated_edge_mask(stream.water_inlet_edges)
== expected_inlet,
"The rotated stream inlet must follow its authored orientation.",
)
_check(
stream_variant.rotated_edge_mask(stream.water_outlet_edges)
== expected_outlet,
"The rotated stream outlet must follow its authored orientation.",
)
var beach := CATALOG.definition_for_id(&"chunk_0002")
_check(beach != null, "The beach definition must be available.")
if beach == null:
return
for quarter_turns: int in 4:
var beach_variant := _find_variant(
generator,
&"chunk_0002",
quarter_turns,
)
_check(
beach_variant != null,
"The beach must support rotation %d." % quarter_turns,
)
if beach_variant == null:
continue
var expected_ocean_edge := 1 << int(
TerrainChunkTopology.rotated_edge(
TerrainChunkTopology.Edge.EAST,
quarter_turns,
)
)
_check(
beach_variant.rotated_edge_mask(beach.ocean_facing_edges)
== expected_ocean_edge,
"The rotated beach must keep its low edge facing the ocean.",
)
_check(
not generator._variant_respects_ocean_boundary(
beach_variant,
Vector2i(3, 3),
),
"A beach slope must never direct its low edge into the map.",
)
var boundary_coordinate := Vector2i(3, 3)
match TerrainChunkTopology.rotated_edge(
TerrainChunkTopology.Edge.EAST,
quarter_turns,
):
TerrainChunkTopology.Edge.NORTH:
boundary_coordinate.y = 0
TerrainChunkTopology.Edge.EAST:
boundary_coordinate.x = generator.grid_size.x - 1
TerrainChunkTopology.Edge.SOUTH:
boundary_coordinate.y = generator.grid_size.y - 1
TerrainChunkTopology.Edge.WEST:
boundary_coordinate.x = 0
_check(
generator._variant_respects_ocean_boundary(
beach_variant,
boundary_coordinate,
),
"A beach slope must be legal when its low edge faces open ocean.",
)
var grass_ocean_edge := CATALOG.definition_for_id(&"chunk_0005")
var grass_beach_transition := CATALOG.definition_for_id(&"chunk_0006")
var grass_ocean_corner := CATALOG.definition_for_id(&"chunk_0007")
_check(
(
grass_ocean_edge != null
and grass_beach_transition != null
and grass_ocean_corner != null
),
"The authored coastline additions must be available.",
)
if (
grass_ocean_edge == null
or grass_beach_transition == null
or grass_ocean_corner == null
):
return
var beach_zero := _find_variant(generator, &"chunk_0002", 0)
var grass_edge_zero := _find_variant(generator, &"chunk_0005", 0)
var grass_edge_south := _find_variant(generator, &"chunk_0005", 3)
var transition_zero := _find_variant(generator, &"chunk_0006", 0)
var corner_zero := _find_variant(generator, &"chunk_0007", 0)
_check(
beach_zero != null
and grass_edge_zero != null
and grass_edge_south != null
and transition_zero != null
and corner_zero != null,
"The unrotated coastline variants must be available.",
)
if (
beach_zero != null
and grass_edge_zero != null
and grass_edge_south != null
and transition_zero != null
and corner_zero != null
):
_check(
grass_edge_zero.profile(
TerrainChunkTopology.Edge.EAST
).points.is_empty(),
"The grass cliff's authored east edge must remain open to ocean.",
)
_check(
transition_zero.profile(
TerrainChunkTopology.Edge.NORTH
).matches(
beach_zero.profile(TerrainChunkTopology.Edge.SOUTH),
generator.edge_match_tolerance,
),
"The transition's north side must match the authored beach.",
)
_check(
transition_zero.profile(
TerrainChunkTopology.Edge.SOUTH
).matches(
grass_edge_zero.profile(TerrainChunkTopology.Edge.NORTH),
generator.edge_match_tolerance,
),
"The transition's south side must match the grass cliff.",
)
_check(
corner_zero.profile(TerrainChunkTopology.Edge.EAST).points.is_empty()
and corner_zero.profile(
TerrainChunkTopology.Edge.SOUTH
).points.is_empty(),
"The corner's authored east and south edges must remain open to ocean.",
)
_check(
corner_zero.profile(TerrainChunkTopology.Edge.NORTH).matches(
grass_edge_zero.profile(TerrainChunkTopology.Edge.SOUTH),
generator.edge_match_tolerance,
),
"The corner's north side must join a straight grass ocean edge.",
)
_check(
corner_zero.profile(TerrainChunkTopology.Edge.WEST).matches(
grass_edge_south.profile(TerrainChunkTopology.Edge.EAST),
generator.edge_match_tolerance,
),
"The corner's west side must join a straight grass ocean edge.",
)
var flat_grass := _find_variant(generator, &"chunk_0000", 0)
var flat_sand := _find_variant(generator, &"chunk_0001", 0)
_check(
flat_grass != null and flat_sand != null,
"Flat grass and sand variants must be available for seam validation.",
)
if flat_grass != null and flat_sand != null:
for quarter_turns: int in 4:
var transition := _find_variant(
generator,
&"chunk_0006",
quarter_turns,
)
if transition == null:
continue
var inland_edge := TerrainChunkTopology.rotated_edge(
TerrainChunkTopology.Edge.WEST,
quarter_turns,
)
var neighbor_edge := TerrainChunkTopology.opposite_edge(inland_edge)
_check(
generator._edges_are_compatible(
transition,
inland_edge,
flat_grass,
neighbor_edge,
),
"Every transition rotation must accept grass behind it.",
)
_check(
not generator._edges_are_compatible(
transition,
inland_edge,
flat_sand,
neighbor_edge,
),
"No transition rotation may accept sand behind it.",
)
for stable_id: StringName in [&"chunk_0005", &"chunk_0006", &"chunk_0007"]:
for quarter_turns: int in 4:
var coast_variant := _find_variant(
generator,
stable_id,
quarter_turns,
)
_check(
coast_variant != null,
"%s must support rotation %d." % [stable_id, quarter_turns],
)
if coast_variant == null:
continue
_check(
not generator._variant_respects_ocean_boundary(
coast_variant,
Vector2i(3, 3),
),
"%s must never place its ocean edge inside the map." % stable_id,
)
if stable_id != &"chunk_0007":
continue
var boundary_coordinate := Vector2i(3, 3)
var ocean_edges := coast_variant.rotated_edge_mask(
grass_ocean_corner.ocean_facing_edges
)
for edge_value: int in TerrainChunkTopology.Edge.values():
if (ocean_edges & (1 << edge_value)) == 0:
continue
match edge_value as TerrainChunkTopology.Edge:
TerrainChunkTopology.Edge.NORTH:
boundary_coordinate.y = 0
TerrainChunkTopology.Edge.EAST:
boundary_coordinate.x = generator.grid_size.x - 1
TerrainChunkTopology.Edge.SOUTH:
boundary_coordinate.y = generator.grid_size.y - 1
TerrainChunkTopology.Edge.WEST:
boundary_coordinate.x = 0
_check(
generator._variant_respects_ocean_boundary(
coast_variant,
boundary_coordinate,
),
"Each grass corner rotation must fit its matching map corner.",
)
func _validate_generation_summary(generator: TerrainChunkGenerator) -> void:
var summary := generator._build_summary()
_check(
int(summary.get("variant_count", 0)) == 33,
"The current catalog must expose all 33 authored rotations.",
)
_check(
int(summary.get("solver_variant_count", 0))
== EXPECTED_SOLVER_VARIANT_COUNT,
"Equivalent visual rotations must share one solver constraint.",
)
_check(
int(summary.get("adjacent_repeat_edges", -1))
== generator._count_adjacent_repeat_edges(),
"The summary must report adjacent repeated chunk definitions.",
)
_check(
str(summary.get("layout_fingerprint", ""))
== generator.placement_fingerprint(),
"The summary must identify the exact resolved layout.",
)
_check(
generator.placement_fingerprint().length() == 64,
"The resolved layout fingerprint must be SHA-256.",
)
var variant_counts: Dictionary = summary.get("variant_counts", {})
for stable_id: StringName in EXPECTED_VARIANT_COUNTS:
_check(
int(variant_counts.get(stable_id, 0))
== EXPECTED_VARIANT_COUNTS[stable_id],
"The summary must report %s rotation candidates." % stable_id,
)
func _find_variant(
generator: TerrainChunkGenerator,
stable_id: StringName,
quarter_turns: int,
) -> TerrainChunkVariant:
for variant: TerrainChunkVariant in generator._variants:
if (
variant.definition.stable_id == stable_id
and variant.quarter_turns == quarter_turns
):
return variant
return null
func _make_generator() -> TerrainChunkGenerator:
var generator := TerrainChunkGenerator.new()
generator.catalog = CATALOG
generator.grid_size = Vector2i(7, 7)
generator.generation_seed = TEST_SEED
generator.generate_on_ready = false
generator.build_collision = false
generator.force_center_chunk_id = &"chunk_spawn"
generator.required_chunk_ids = PackedStringArray(REQUIRED_IDS)
generator.maximum_backtracks = 100000
return generator
func _placements_contain(keys: PackedStringArray, stable_id: String) -> bool:
for key: String in keys:
if key.begins_with(stable_id + "@"):
return true
return false
func _count_placements(keys: PackedStringArray, stable_id: String) -> int:
var count := 0
for key: String in keys:
if key.begins_with(stable_id + "@"):
count += 1
return count
func _all_neighbor_edges_match(generator: TerrainChunkGenerator) -> bool:
for index: int in generator._placements.size():
var coordinate := Vector2i(
index % generator.grid_size.x,
index / generator.grid_size.x,
)
var current: TerrainChunkVariant = generator._placements[index]
if coordinate.x > 0:
var west: TerrainChunkVariant = generator._placements[index - 1]
if not generator._edges_are_compatible(
current,
TerrainChunkTopology.Edge.WEST,
west,
TerrainChunkTopology.Edge.EAST,
):
return false
if coordinate.y > 0:
var north: TerrainChunkVariant = (
generator._placements[index - generator.grid_size.x]
)
if not generator._edges_are_compatible(
current,
TerrainChunkTopology.Edge.NORTH,
north,
TerrainChunkTopology.Edge.SOUTH,
):
return false
return true
func _validate_layout_rules(generator: TerrainChunkGenerator) -> void:
_check(
generator._neighbor_requirement_validation_error(
generator._placements
).is_empty(),
"Every generated chunk must satisfy its authored neighbor count.",
)
_check(
generator._walkable_connectivity_validation_error(
generator._placements
).is_empty(),
"All walkable generated terrain must remain connected to spawn.",
)
var center := Vector2i(
generator.grid_size.x / 2,
generator.grid_size.y / 2,
)
var safe_spawn_neighbors := 0
for edge_value: int in TerrainChunkTopology.Edge.values():
var neighbor_coordinate := (
center
+ TerrainChunkTopology.grid_offset(
edge_value as TerrainChunkTopology.Edge
)
)
var neighbor := generator._placements[
neighbor_coordinate.y * generator.grid_size.x
+ neighbor_coordinate.x
]
if "spawn_safe" in neighbor.definition.tags:
safe_spawn_neighbors += 1
_check(
safe_spawn_neighbors >= 3,
"At least three cardinal spawn neighbors must be safe flat grass.",
)
for index: int in generator._placements.size():
var placement := generator._placements[index]
var coordinate := Vector2i(
index % generator.grid_size.x,
index / generator.grid_size.x,
)
if "coast" in placement.definition.tags:
_check(
generator._variant_respects_ocean_boundary(
placement,
coordinate,
),
"Every coastline chunk must direct its ocean edge outside the grid.",
)
if placement.definition.stable_id == &"chunk_0006":
var touches_beach := false
var touches_grass_edge := false
for edge_value: int in TerrainChunkTopology.Edge.values():
var neighbor_coordinate := (
coordinate
+ TerrainChunkTopology.grid_offset(
edge_value as TerrainChunkTopology.Edge
)
)
if not generator._coordinate_is_inside_grid(neighbor_coordinate):
continue
var neighbor := generator._placements[
neighbor_coordinate.y * generator.grid_size.x
+ neighbor_coordinate.x
]
touches_beach = (
touches_beach
or neighbor.definition.stable_id == &"chunk_0002"
)
touches_grass_edge = (
touches_grass_edge
or neighbor.definition.stable_id == &"chunk_0005"
)
_check(
touches_beach and touches_grass_edge,
"Every grass/beach transition must join both authored edge types.",
)
if placement.definition.stable_id == &"chunk_0007":
var straight_edge_neighbors := 0
for edge_value: int in TerrainChunkTopology.Edge.values():
var neighbor_coordinate := (
coordinate
+ TerrainChunkTopology.grid_offset(
edge_value as TerrainChunkTopology.Edge
)
)
if not generator._coordinate_is_inside_grid(neighbor_coordinate):
continue
var neighbor := generator._placements[
neighbor_coordinate.y * generator.grid_size.x
+ neighbor_coordinate.x
]
if neighbor.definition.stable_id == &"chunk_0005":
straight_edge_neighbors += 1
_check(
straight_edge_neighbors == 2,
"Every grass corner must join two straight grass ocean edges.",
)
if placement.definition.stable_id != &"chunk_0001":
continue
var touches_coast := false
for edge_value: int in TerrainChunkTopology.Edge.values():
var neighbor_coordinate := (
coordinate
+ TerrainChunkTopology.grid_offset(
edge_value as TerrainChunkTopology.Edge
)
)
if not generator._coordinate_is_inside_grid(neighbor_coordinate):
continue
var neighbor := generator._placements[
neighbor_coordinate.y * generator.grid_size.x
+ neighbor_coordinate.x
]
if "coast" in neighbor.definition.tags:
touches_coast = true
_check(
touches_coast,
"Every flat-sand chunk must remain directly attached to a beach.",
)
func _check(condition: bool, message: String) -> void:
if not condition:
_failures.append(message)
func _finish() -> void:
if _failures.is_empty():
print("Terrain chunk generator validation: PASS")
quit(0)
return
for failure: String in _failures:
printerr("Terrain chunk generator validation: ", failure)
quit(1)

View file

@ -0,0 +1 @@
uid://bt6pjgkomx76s

View file

@ -16,7 +16,7 @@ const CalendarSeasonType = preload("res://world/calendar_season.gd")
func _initialize() -> void:
assert(NetworkProtocol.PROTOCOL_VERSION == 8)
assert(NetworkProtocol.PROTOCOL_VERSION == 9)
assert(
NetworkWorldSpawnProtocol.CAPABILITY
== NetworkProtocol.WORLD_SPAWN_CAPABILITY

View file

@ -0,0 +1,805 @@
#!/usr/bin/env python3
"""Export convention-named terrain chunk collections to individual GLBs.
Run through Blender rather than a standalone Python interpreter:
blender --background terrain_chunks.blend \
--python tools/blender/export_terrain_chunks.py -- \
--output /path/to/terrain_chunks
Collections use ``chunk_####_description`` and contain one primary terrain
mesh named ``chunk_####``. Additional production objects may live in the same
collection. Reusable procedural props use individual ``prop_description``
mesh objects. They may be arranged anywhere in the source file because each
object's authored origin becomes the exported runtime anchor. A same-named
``prop_description`` collection remains supported for multi-object props.
Unrelated objects and collections are ignored.
"""
from __future__ import annotations
import argparse
import hashlib
import json
import math
import re
import struct
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import Iterable
import bpy
from mathutils import Vector
CHUNK_SIZE_METERS = 10.0
# Match the runtime boundary analyzer's five-millimeter authoring tolerance.
DEFAULT_TOLERANCE = 0.005
COLLECTION_PATTERN = re.compile(
r"^chunk_(?P<number>\d{4})(?:_(?P<label>[a-z0-9]+(?:_[a-z0-9]+)*))?$"
)
PROP_COLLECTION_PATTERN = re.compile(
r"^prop_(?P<label>[a-z0-9]+(?:_[a-z0-9]+)*)$"
)
ALLOWED_OBJECT_TYPES = {"EMPTY", "MESH"}
class ExportValidationError(RuntimeError):
"""Raised when the source file does not satisfy the chunk convention."""
@dataclass(frozen=True)
class Bounds:
minimum: tuple[float, float, float]
maximum: tuple[float, float, float]
@property
def size(self) -> tuple[float, float, float]:
return tuple(
self.maximum[axis] - self.minimum[axis] for axis in range(3)
)
@property
def center(self) -> tuple[float, float, float]:
return tuple(
(self.minimum[axis] + self.maximum[axis]) * 0.5
for axis in range(3)
)
@dataclass(frozen=True)
class ChunkSource:
number: int
stable_id: str
label: str
collection: bpy.types.Collection
primary_mesh: bpy.types.Object
objects: tuple[bpy.types.Object, ...]
bounds: Bounds
@dataclass(frozen=True)
class PropSource:
stable_id: str
label: str
source_kind: str
source_name: str
collection: bpy.types.Collection
primary_mesh: bpy.types.Object
objects: tuple[bpy.types.Object, ...]
bounds: Bounds
anchor_world: tuple[float, float, float]
ExportSource = ChunkSource | PropSource
def _parse_arguments() -> argparse.Namespace:
script_arguments: list[str] = []
if "--" in sys.argv:
script_arguments = sys.argv[sys.argv.index("--") + 1 :]
parser = argparse.ArgumentParser(
description="Export NETfishing terrain chunk collections to GLB files."
)
parser.add_argument(
"--output",
type=Path,
required=True,
help=(
"Directory that will receive chunk_####.glb, prop_*.glb, and "
"chunks.json."
),
)
parser.add_argument(
"--tolerance",
type=float,
default=DEFAULT_TOLERANCE,
help="World-space validation tolerance in meters.",
)
parser.add_argument(
"--dry-run",
action="store_true",
help="Validate and report chunks without writing output files.",
)
return parser.parse_args(script_arguments)
def _close_enough(actual: float, expected: float, tolerance: float) -> bool:
return math.isclose(actual, expected, rel_tol=0.0, abs_tol=tolerance)
def _format_vector(values: Iterable[float]) -> str:
return "(" + ", ".join(f"{value:.4f}" for value in values) + ")"
def _world_bounds(mesh_object: bpy.types.Object) -> Bounds:
evaluated_object = mesh_object.evaluated_get(
bpy.context.evaluated_depsgraph_get()
)
world_corners = [
evaluated_object.matrix_world @ Vector(corner)
for corner in evaluated_object.bound_box
]
minimum = tuple(
min(corner[axis] for corner in world_corners) for axis in range(3)
)
maximum = tuple(
max(corner[axis] for corner in world_corners) for axis in range(3)
)
return Bounds(minimum=minimum, maximum=maximum)
def _validate_transform(
mesh_object: bpy.types.Object,
tolerance: float,
) -> list[str]:
problems: list[str] = []
for axis, value in zip("XYZ", mesh_object.location):
if not _close_enough(value, 0.0, tolerance):
problems.append(f"location {axis} is {value:.6f}, expected 0")
for axis, value in zip("XYZ", mesh_object.rotation_euler):
if not _close_enough(value, 0.0, tolerance):
problems.append(f"rotation {axis} is {value:.6f}, expected 0")
for axis, value in zip("XYZ", mesh_object.scale):
if not _close_enough(value, 1.0, tolerance):
problems.append(f"scale {axis} is {value:.6f}, expected 1")
return problems
def _validate_bounds(
bounds: Bounds,
tolerance: float,
allow_open_east_edge: bool = False,
allow_open_south_edge: bool = False,
) -> list[str]:
problems: list[str] = []
expected_half_size = CHUNK_SIZE_METERS * 0.5
expected_values = {
"minimum X": (bounds.minimum[0], -expected_half_size),
"maximum Y": (bounds.maximum[1], expected_half_size),
}
if not allow_open_east_edge:
expected_values["maximum X"] = (
bounds.maximum[0],
expected_half_size,
)
if not allow_open_south_edge:
expected_values["minimum Y"] = (
bounds.minimum[1],
-expected_half_size,
)
for label, (actual, expected) in expected_values.items():
if not _close_enough(actual, expected, tolerance):
problems.append(f"{label} is {actual:.6f}, expected {expected:.6f}")
if (
allow_open_east_edge
and bounds.maximum[0] > expected_half_size + tolerance
):
problems.append(
f"maximum X is {bounds.maximum[0]:.6f}, expected no greater than "
f"{expected_half_size:.6f}"
)
if (
allow_open_south_edge
and bounds.minimum[1] < -expected_half_size - tolerance
):
problems.append(
f"minimum Y is {bounds.minimum[1]:.6f}, expected no less than "
f"{-expected_half_size:.6f}"
)
return problems
def _discover_chunks(tolerance: float) -> list[ChunkSource]:
chunks: list[ChunkSource] = []
errors: list[str] = []
claimed_objects: dict[str, str] = {}
claimed_numbers: dict[int, str] = {}
for collection in sorted(bpy.data.collections, key=lambda item: item.name):
match = COLLECTION_PATTERN.fullmatch(collection.name)
if match is None:
continue
number = int(match.group("number"))
stable_id = f"chunk_{number:04d}"
label = match.group("label") or ""
if number in claimed_numbers:
errors.append(
f"{collection.name}: duplicates numeric ID already used by "
f"{claimed_numbers[number]}"
)
continue
claimed_numbers[number] = collection.name
objects = tuple(sorted(collection.all_objects, key=lambda item: item.name))
if not objects:
errors.append(f"{collection.name}: collection is empty")
continue
invalid_objects = [
item for item in objects if item.type not in ALLOWED_OBJECT_TYPES
]
if invalid_objects:
details = ", ".join(
f"{item.name} ({item.type})" for item in invalid_objects
)
errors.append(
f"{collection.name}: contains unsupported objects: {details}"
)
primary_matches = [
item
for item in objects
if item.name == stable_id and item.type == "MESH"
]
if len(primary_matches) != 1:
errors.append(
f"{collection.name}: expected exactly one primary mesh named "
f"{stable_id}, found {len(primary_matches)}"
)
continue
primary_mesh = primary_matches[0]
for item in objects:
previous_collection = claimed_objects.get(item.name)
if previous_collection is not None:
errors.append(
f"{collection.name}: object {item.name} is also exported by "
f"{previous_collection}"
)
else:
claimed_objects[item.name] = collection.name
transform_problems = _validate_transform(primary_mesh, tolerance)
errors.extend(
f"{collection.name}/{primary_mesh.name}: {problem}"
for problem in transform_problems
)
bounds = _world_bounds(primary_mesh)
# Coastline meshes may intentionally stop before the positive-X edge,
# leaving the surrounding ocean visible. Positive X is the canonical
# authored ocean direction; Godot supplies the quarter-turn variants.
allow_open_east_edge = "ocean_edge" in label
# Corner pieces additionally leave canonical negative Y open. Their
# two marked ocean edges rotate together at runtime.
allow_open_south_edge = "ocean_edge_corner" in label
bound_problems = _validate_bounds(
bounds,
tolerance,
allow_open_east_edge=allow_open_east_edge,
allow_open_south_edge=allow_open_south_edge,
)
errors.extend(
f"{collection.name}/{primary_mesh.name}: {problem}"
for problem in bound_problems
)
if len(primary_mesh.data.vertices) < 3:
errors.append(
f"{collection.name}/{primary_mesh.name}: mesh has fewer than "
"three vertices"
)
if len(primary_mesh.data.materials) == 0:
errors.append(
f"{collection.name}/{primary_mesh.name}: mesh has no material"
)
chunks.append(
ChunkSource(
number=number,
stable_id=stable_id,
label=label,
collection=collection,
primary_mesh=primary_mesh,
objects=objects,
bounds=bounds,
)
)
if not chunks:
errors.append(
"No collections matched chunk_####_description (for example, "
"chunk_0000_grass)."
)
if errors:
raise ExportValidationError("\n".join(errors))
return sorted(chunks, key=lambda item: item.number)
def _validate_prop_bounds(bounds: Bounds, tolerance: float) -> list[str]:
problems: list[str] = []
for axis, size in zip("XYZ", bounds.size):
if size <= tolerance:
problems.append(f"bounds size {axis} is {size:.6f}, expected positive")
return problems
def _rebased_prop_bounds(
mesh_objects: list[bpy.types.Object],
anchor_world: Vector,
) -> Bounds:
corners: list[Vector] = []
dependency_graph = bpy.context.evaluated_depsgraph_get()
for mesh_object in mesh_objects:
evaluated_object = mesh_object.evaluated_get(dependency_graph)
corners.extend(
evaluated_object.matrix_world @ Vector(corner) - anchor_world
for corner in evaluated_object.bound_box
)
minimum = tuple(
min(corner[axis] for corner in corners) for axis in range(3)
)
maximum = tuple(
max(corner[axis] for corner in corners) for axis in range(3)
)
return Bounds(minimum=minimum, maximum=maximum)
def _prop_transform_problems(
source_objects: tuple[bpy.types.Object, ...],
tolerance: float,
) -> list[str]:
problems: list[str] = []
source_set = set(source_objects)
for item in source_objects:
if item.parent is not None and item.parent not in source_set:
problems.append(
f"{item.name} is parented outside its exported prop source"
)
for label, values in (
("location", item.location),
("rotation", item.rotation_euler),
("scale", item.scale),
):
if not all(math.isfinite(value) for value in values):
problems.append(f"{item.name} has a non-finite {label}")
for axis, value in zip("XYZ", item.scale):
if abs(value) <= tolerance:
problems.append(
f"{item.name} scale {axis} is {value:.6f}, expected nonzero"
)
return problems
def _make_prop_source(
stable_id: str,
label: str,
source_kind: str,
source_name: str,
collection: bpy.types.Collection,
objects: tuple[bpy.types.Object, ...],
tolerance: float,
) -> tuple[PropSource | None, list[str]]:
errors: list[str] = []
invalid_objects = [
item for item in objects if item.type not in ALLOWED_OBJECT_TYPES
]
if invalid_objects:
details = ", ".join(
f"{item.name} ({item.type})" for item in invalid_objects
)
errors.append(f"{source_name}: contains unsupported objects: {details}")
mesh_objects = [item for item in objects if item.type == "MESH"]
named_meshes = [item for item in mesh_objects if item.name == stable_id]
if len(named_meshes) == 1:
primary_mesh = named_meshes[0]
elif len(mesh_objects) == 1:
primary_mesh = mesh_objects[0]
else:
errors.append(
f"{source_name}: expected one mesh (preferably named "
f"{stable_id}), found {len(mesh_objects)}"
)
return None, errors
errors.extend(
f"{source_name}: {problem}"
for problem in _prop_transform_problems(objects, tolerance)
)
anchor_vector = primary_mesh.matrix_world.translation.copy()
bounds = _rebased_prop_bounds(mesh_objects, anchor_vector)
errors.extend(
f"{source_name}/{primary_mesh.name}: {problem}"
for problem in _validate_prop_bounds(bounds, tolerance)
)
for mesh_object in mesh_objects:
if len(mesh_object.data.vertices) < 3:
errors.append(
f"{source_name}/{mesh_object.name}: has fewer than three vertices"
)
if len(mesh_object.data.materials) == 0:
errors.append(
f"{source_name}/{mesh_object.name}: has no material"
)
return (
PropSource(
stable_id=stable_id,
label=label,
source_kind=source_kind,
source_name=source_name,
collection=collection,
primary_mesh=primary_mesh,
objects=objects,
bounds=bounds,
anchor_world=tuple(anchor_vector),
),
errors,
)
def _discover_props(tolerance: float) -> list[PropSource]:
props: list[PropSource] = []
errors: list[str] = []
claimed_objects: dict[int, str] = {}
claimed_ids: dict[str, str] = {}
for collection in sorted(bpy.data.collections, key=lambda item: item.name):
match = PROP_COLLECTION_PATTERN.fullmatch(collection.name)
if match is None:
continue
stable_id = collection.name
label = match.group("label")
objects = tuple(sorted(collection.all_objects, key=lambda item: item.name))
if not objects:
errors.append(f"{collection.name}: collection is empty")
continue
for item in objects:
object_key = item.as_pointer()
previous_source = claimed_objects.get(object_key)
if previous_source is not None:
errors.append(
f"{collection.name}: object {item.name} is also exported by "
f"{previous_source}"
)
else:
claimed_objects[object_key] = collection.name
prop, source_errors = _make_prop_source(
stable_id,
label,
"collection",
collection.name,
collection,
objects,
tolerance,
)
errors.extend(source_errors)
if prop is not None:
props.append(prop)
claimed_ids[stable_id] = collection.name
for item in sorted(bpy.data.objects, key=lambda value: value.name):
match = PROP_COLLECTION_PATTERN.fullmatch(item.name)
if match is None or item.as_pointer() in claimed_objects:
continue
stable_id = item.name
if stable_id in claimed_ids:
errors.append(
f"{item.name}: duplicates prop ID already used by "
f"{claimed_ids[stable_id]}"
)
continue
if item.type != "MESH":
errors.append(f"{item.name}: prop object must be a mesh")
continue
collections = sorted(item.users_collection, key=lambda value: value.name)
if not collections:
errors.append(f"{item.name}: prop object belongs to no collection")
continue
prop, source_errors = _make_prop_source(
stable_id,
match.group("label"),
"object",
item.name,
collections[0],
(item,),
tolerance,
)
errors.extend(source_errors)
if prop is not None:
props.append(prop)
claimed_ids[stable_id] = item.name
claimed_objects[item.as_pointer()] = item.name
if errors:
raise ExportValidationError("\n".join(errors))
return sorted(props, key=lambda item: item.stable_id)
def _validate_distinct_source_membership(
chunks: list[ChunkSource],
props: list[PropSource],
) -> None:
claimed_objects: dict[int, str] = {}
errors: list[str] = []
for source in [*chunks, *props]:
source_name = (
source.collection.name
if isinstance(source, ChunkSource)
else source.source_name
)
for item in source.objects:
object_key = item.as_pointer()
previous_source = claimed_objects.get(object_key)
if previous_source is not None:
errors.append(
f"{source_name}: object {item.name} is also exported by "
f"{previous_source}"
)
else:
claimed_objects[object_key] = source_name
if errors:
raise ExportValidationError("\n".join(errors))
def _select_source(source: ExportSource) -> None:
if bpy.context.object is not None and bpy.context.object.mode != "OBJECT":
bpy.ops.object.mode_set(mode="OBJECT")
# Blender's selection operator can miss objects after a glTF export has
# temporarily changed visibility/context. Set every object explicitly so
# one chunk can never leak into a later chunk's selected-only export.
selected_objects = set(source.objects)
for item in bpy.context.view_layer.objects:
item.select_set(item in selected_objects)
for item in source.objects:
item.hide_select = False
item.hide_viewport = False
item.hide_set(False)
item.select_set(True)
bpy.context.view_layer.objects.active = source.primary_mesh
def _export_source(source: ExportSource, output_path: Path) -> None:
_select_source(source)
saved_root_matrices: dict[bpy.types.Object, object] = {}
if isinstance(source, PropSource):
source_objects = set(source.objects)
anchor = Vector(source.anchor_world)
for item in source.objects:
if item.parent in source_objects:
continue
saved_root_matrices[item] = item.matrix_world.copy()
rebased_matrix = item.matrix_world.copy()
rebased_matrix.translation -= anchor
item.matrix_world = rebased_matrix
bpy.context.view_layer.update()
try:
result = bpy.ops.export_scene.gltf(
filepath=str(output_path),
check_existing=False,
export_format="GLB",
use_selection=True,
export_apply=True,
export_animations=False,
export_cameras=False,
export_lights=False,
export_materials="EXPORT",
export_morph=False,
export_normals=True,
export_skins=False,
export_tangents=False,
export_texcoords=True,
export_yup=True,
export_extras=True,
will_save_settings=False,
)
finally:
for item, matrix in saved_root_matrices.items():
item.matrix_world = matrix
if saved_root_matrices:
bpy.context.view_layer.update()
if result != {"FINISHED"}:
source_name = (
source.collection.name
if isinstance(source, ChunkSource)
else source.source_name
)
raise RuntimeError(
f"Blender failed to export {source_name}: {result}"
)
_validate_exported_object_membership(source, output_path)
def _validate_exported_object_membership(
source: ExportSource,
output_path: Path,
) -> None:
"""Confirm selected-only export contains exactly the intended objects."""
with output_path.open("rb") as binary_file:
header = binary_file.read(20)
if len(header) != 20 or header[:4] != b"glTF":
raise RuntimeError(f"{output_path.name} is not a valid GLB file")
json_length, json_kind = struct.unpack_from("<II", header, 12)
if json_kind != 0x4E4F534A:
raise RuntimeError(
f"{output_path.name} does not begin with a GLB JSON chunk"
)
document = json.loads(binary_file.read(json_length))
exported_names = {
str(node.get("name", ""))
for node in document.get("nodes", [])
if str(node.get("name", ""))
}
expected_names = {item.name for item in source.objects}
if exported_names != expected_names:
raise RuntimeError(
f"{output_path.name} object membership mismatch: expected "
f"{sorted(expected_names)}, exported {sorted(exported_names)}"
)
def _sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as source:
for block in iter(lambda: source.read(1024 * 1024), b""):
digest.update(block)
return digest.hexdigest()
def _round_vector(values: Iterable[float]) -> list[float]:
return [round(value, 6) for value in values]
def _manifest_entry(chunk: ChunkSource, output_path: Path) -> dict[str, object]:
return {
"id": chunk.stable_id,
"number": chunk.number,
"label": chunk.label,
"collection": chunk.collection.name,
"primary_mesh": chunk.primary_mesh.name,
"objects": [item.name for item in chunk.objects],
"file": output_path.name,
"sha256": _sha256(output_path),
"bounds": {
"minimum": _round_vector(chunk.bounds.minimum),
"maximum": _round_vector(chunk.bounds.maximum),
"size": _round_vector(chunk.bounds.size),
"center": _round_vector(chunk.bounds.center),
},
}
def _prop_manifest_entry(
prop: PropSource,
output_path: Path,
) -> dict[str, object]:
return {
"id": prop.stable_id,
"label": prop.label,
"source_kind": prop.source_kind,
"source_name": prop.source_name,
"collection": prop.collection.name,
"primary_mesh": prop.primary_mesh.name,
"objects": [item.name for item in prop.objects],
"source_anchor_world": _round_vector(prop.anchor_world),
"file": output_path.name,
"sha256": _sha256(output_path),
"bounds": {
"minimum": _round_vector(prop.bounds.minimum),
"maximum": _round_vector(prop.bounds.maximum),
"size": _round_vector(prop.bounds.size),
"center": _round_vector(prop.bounds.center),
},
}
def _run() -> int:
arguments = _parse_arguments()
if arguments.tolerance <= 0.0:
raise ExportValidationError("--tolerance must be greater than zero")
chunks = _discover_chunks(arguments.tolerance)
props = _discover_props(arguments.tolerance)
_validate_distinct_source_membership(chunks, props)
print(
f"Validated {len(chunks)} terrain chunks and {len(props)} reusable "
f"props from "
f"{Path(bpy.data.filepath).resolve()}"
)
for chunk in chunks:
print(
f" {chunk.stable_id}: {chunk.label or '(no label)'} | "
f"bounds {_format_vector(chunk.bounds.minimum)} to "
f"{_format_vector(chunk.bounds.maximum)} | "
f"{len(chunk.objects)} object(s)"
)
for prop in props:
print(
f" {prop.stable_id}: reusable {prop.source_kind} prop | bounds "
f"{_format_vector(prop.bounds.minimum)} to "
f"{_format_vector(prop.bounds.maximum)} | "
f"{len(prop.objects)} object(s)"
)
if arguments.dry_run:
print("Dry run complete; no files written.")
return 0
output_directory = arguments.output.expanduser().resolve()
output_directory.mkdir(parents=True, exist_ok=True)
entries: list[dict[str, object]] = []
prop_entries: list[dict[str, object]] = []
expected_files: set[str] = set()
for chunk in chunks:
output_path = output_directory / f"{chunk.stable_id}.glb"
_export_source(chunk, output_path)
expected_files.add(output_path.name)
entry = _manifest_entry(chunk, output_path)
entries.append(entry)
print(
f"Exported {output_path.name} | {output_path.stat().st_size} bytes | "
f"{entry['sha256']}"
)
for prop in props:
output_path = output_directory / f"{prop.stable_id}.glb"
_export_source(prop, output_path)
expected_files.add(output_path.name)
entry = _prop_manifest_entry(prop, output_path)
prop_entries.append(entry)
print(
f"Exported {output_path.name} | {output_path.stat().st_size} bytes | "
f"{entry['sha256']}"
)
manifest = {
"format_version": 2,
"chunk_size_meters": CHUNK_SIZE_METERS,
"source_blend": str(Path(bpy.data.filepath).resolve()),
"blender_version": bpy.app.version_string,
"chunks": entries,
"props": prop_entries,
}
manifest_path = output_directory / "chunks.json"
manifest_path.write_text(
json.dumps(manifest, indent=2, sort_keys=True) + "\n",
encoding="utf-8",
)
expected_files.add(manifest_path.name)
stale_files = sorted({
path.name
for pattern in ("chunk_*.glb", "prop_*.glb")
for path in output_directory.glob(pattern)
if path.name not in expected_files
})
if stale_files:
print(
"WARNING: stale generated-world outputs were retained: "
+ ", ".join(stale_files)
)
print(f"Wrote manifest {manifest_path}")
return 0
if __name__ == "__main__":
try:
raise SystemExit(_run())
except ExportValidationError as error:
print(f"EXPORT VALIDATION FAILED:\n{error}", file=sys.stderr)
raise SystemExit(2) from error

View file

@ -94,7 +94,7 @@ const INTRO_BRANDING_TRAVEL_DURATION: float = 2.0
const INTRO_BRANDING_START_DELAY: float = 0.35
const HOST_CLUSTER_SAFE_MARGIN: float = 24.0
signal new_game_requested
signal new_game_requested(world_seed: int)
signal continue_game_requested
signal quit_requested
signal join_game_requested(endpoint: String)
@ -147,6 +147,7 @@ enum ConfirmationAction {
var _save_manager: SaveManagerType
var _settings_manager: SettingsManagerType
var _inspection: SaveInspectionType
var _pending_new_game_seed: int = PlayerSaveManager.DEFAULT_WORLD_SEED
var _confirmation_action: ConfirmationAction = ConfirmationAction.NONE
var _action_in_progress: bool = false
var _decorative_rng := RandomNumberGenerator.new()
@ -1019,9 +1020,6 @@ func _on_new_game_pressed() -> void:
):
return
_hide_continue_stats_context()
if _inspection.status == SaveInspectionType.Status.MISSING:
new_game_requested.emit()
return
if _inspection.status == SaveInspectionType.Status.UNSUPPORTED_VERSION:
_feedback_label.text = _center_feedback_text(
"this save is from a newer game version. "
@ -1033,9 +1031,14 @@ func _on_new_game_pressed() -> void:
"the existing save cannot be accessed safely."
)
return
_pending_new_game_seed = PlayerSaveManager.roll_world_seed()
var warning := "start a new world?"
if _inspection.status != SaveInspectionType.Status.MISSING:
warning += " existing progression will be deleted."
warning += "\nworld seed: %d" % _pending_new_game_seed
_open_confirmation(
ConfirmationAction.NEW_GAME,
"start a new game? existing progression will be deleted.",
warning,
"start\nnew game",
true
)
@ -1073,7 +1076,7 @@ func _on_confirmation_accepted() -> void:
_confirmation_transition_generation += 1
_cancel_confirmation_transition()
_confirmation_page.hide_page()
new_game_requested.emit()
new_game_requested.emit(_pending_new_game_seed)
_action_in_progress = false
return
if not _save_manager.delete_progression_save():

View file

@ -0,0 +1,20 @@
[gd_resource type="Resource" script_class="TerrainBiomeDefinition" load_steps=4 format=3]
[ext_resource type="Script" path="res://world/generation/terrain_biome_definition.gd" id="1_definition"]
[ext_resource type="Script" path="res://world/generation/terrain_biome_prop_rule.gd" id="2_rule"]
[sub_resource type="Resource" id="Rule_sand_tree"]
script = ExtResource("2_rule")
procedural_group = &"sand_tree"
placement_chance = 4200
adjacency_bonus = 1200
maximum_density = 5000
minimum_placements = 1
allowed_prop_ids = PackedStringArray("prop_palm")
[resource]
script = ExtResource("1_definition")
stable_id = &"biome_coast"
label = "coast"
required_chunk_tags = PackedStringArray("sand")
prop_rules = Array[ExtResource("2_rule")]([SubResource("Rule_sand_tree")])

View file

@ -0,0 +1,31 @@
[gd_resource type="Resource" script_class="TerrainBiomeDefinition" load_steps=5 format=3]
[ext_resource type="Script" path="res://world/generation/terrain_biome_definition.gd" id="1_definition"]
[ext_resource type="Script" path="res://world/generation/terrain_biome_prop_rule.gd" id="2_rule"]
[sub_resource type="Resource" id="Rule_grass_tree"]
script = ExtResource("2_rule")
procedural_group = &"grass_tree"
placement_chance = 9000
adjacency_bonus = 1000
maximum_density = 10000
minimum_placements = 2
allowed_prop_ids = PackedStringArray("prop_tree_1", "prop_tree_2", "prop_tree_3")
[sub_resource type="Resource" id="Rule_grass_detail"]
script = ExtResource("2_rule")
procedural_group = &"grass_detail"
placement_chance = 3500
adjacency_bonus = 1200
maximum_density = 5000
minimum_placements = 1
allowed_prop_ids = PackedStringArray("prop_log", "prop_mushroom")
[resource]
script = ExtResource("1_definition")
stable_id = &"biome_forest"
label = "forest"
required_chunk_tags = PackedStringArray("grass")
forbidden_chunk_tags = PackedStringArray("spawn")
region_weight = 1.4
prop_rules = Array[ExtResource("2_rule")]([SubResource("Rule_grass_tree"), SubResource("Rule_grass_detail")])

View file

@ -0,0 +1,31 @@
[gd_resource type="Resource" script_class="TerrainBiomeDefinition" load_steps=5 format=3]
[ext_resource type="Script" path="res://world/generation/terrain_biome_definition.gd" id="1_definition"]
[ext_resource type="Script" path="res://world/generation/terrain_biome_prop_rule.gd" id="2_rule"]
[sub_resource type="Resource" id="Rule_grass_tree"]
script = ExtResource("2_rule")
procedural_group = &"grass_tree"
placement_chance = 9000
adjacency_bonus = 1000
maximum_density = 10000
minimum_placements = 2
allowed_prop_ids = PackedStringArray("prop_pine")
[sub_resource type="Resource" id="Rule_grass_detail"]
script = ExtResource("2_rule")
procedural_group = &"grass_detail"
placement_chance = 2500
adjacency_bonus = 1000
maximum_density = 4000
minimum_placements = 1
allowed_prop_ids = PackedStringArray("prop_log", "prop_mushroom")
[resource]
script = ExtResource("1_definition")
stable_id = &"biome_pine_forest"
label = "pine forest"
required_chunk_tags = PackedStringArray("grass")
forbidden_chunk_tags = PackedStringArray("spawn")
region_weight = 1.2
prop_rules = Array[ExtResource("2_rule")]([SubResource("Rule_grass_tree"), SubResource("Rule_grass_detail")])

View file

@ -0,0 +1,28 @@
[gd_resource type="Resource" script_class="TerrainBiomeDefinition" load_steps=5 format=3]
[ext_resource type="Script" path="res://world/generation/terrain_biome_definition.gd" id="1_definition"]
[ext_resource type="Script" path="res://world/generation/terrain_biome_prop_rule.gd" id="2_rule"]
[sub_resource type="Resource" id="Rule_grass_tree"]
script = ExtResource("2_rule")
procedural_group = &"grass_tree"
placement_chance = 1600
adjacency_bonus = 900
maximum_density = 2200
allowed_prop_ids = PackedStringArray("prop_tree_1", "prop_tree_2", "prop_tree_3")
[sub_resource type="Resource" id="Rule_grass_detail"]
script = ExtResource("2_rule")
procedural_group = &"grass_detail"
placement_chance = 700
adjacency_bonus = 400
maximum_density = 1200
allowed_prop_ids = PackedStringArray("prop_log", "prop_mushroom")
[resource]
script = ExtResource("1_definition")
stable_id = &"biome_plains"
label = "plains"
required_chunk_tags = PackedStringArray("grass")
region_weight = 1.0
prop_rules = Array[ExtResource("2_rule")]([SubResource("Rule_grass_tree"), SubResource("Rule_grass_detail")])

View file

@ -0,0 +1,12 @@
[gd_resource type="Resource" script_class="TerrainBiomeCatalog" load_steps=7 format=3]
[ext_resource type="Script" path="res://world/generation/terrain_biome_catalog.gd" id="1_catalog"]
[ext_resource type="Resource" path="res://world/generation/biomes/definitions/biome_plains.tres" id="2_plains"]
[ext_resource type="Resource" path="res://world/generation/biomes/definitions/biome_forest.tres" id="3_forest"]
[ext_resource type="Resource" path="res://world/generation/biomes/definitions/biome_pine_forest.tres" id="4_pine"]
[ext_resource type="Resource" path="res://world/generation/biomes/definitions/biome_coast.tres" id="5_coast"]
[ext_resource type="Script" path="res://world/generation/terrain_biome_definition.gd" id="6_definition_script"]
[resource]
script = ExtResource("1_catalog")
definitions = Array[ExtResource("6_definition_script")]([ExtResource("2_plains"), ExtResource("3_forest"), ExtResource("4_pine"), ExtResource("5_coast")])

Binary file not shown.

View file

@ -0,0 +1,45 @@
[remap]
importer="scene"
importer_version=1
type="PackedScene"
uid="uid://jv50rtyjn3j1"
path="res://.godot/imported/chunk_0000.glb-8221861eeebadbe0547c1162b0dc4018.scn"
[deps]
source_file="res://world/generation/chunks/assets/chunk_0000.glb"
dest_files=["res://.godot/imported/chunk_0000.glb-8221861eeebadbe0547c1162b0dc4018.scn"]
[params]
nodes/root_type=""
nodes/root_name=""
nodes/root_script=null
mesh_library/use_node_names_as_mesh_names=false
array_mesh/deduplicate_surfaces=true
nodes/apply_root_scale=true
nodes/root_scale=1.0
nodes/import_as_skeleton_bones=false
nodes/use_name_suffixes=true
nodes/use_node_type_suffixes=true
meshes/ensure_tangents=true
meshes/generate_lods=true
meshes/create_shadow_meshes=true
meshes/light_baking=1
meshes/lightmap_texel_size=0.2
meshes/force_disable_compression=false
skins/use_named_skins=true
animation/import=true
animation/fps=30
animation/trimming=false
animation/remove_immutable_tracks=true
animation/import_rest_as_RESET=false
import_script/path=""
materials/extract=0
materials/extract_format=0
materials/extract_path=""
_subresources={}
gltf/naming_version=2
gltf/embedded_image_handling=1
gltf/texture_map_mode=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

View file

@ -0,0 +1,45 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://doju2lbj2ghwa"
path.s3tc="res://.godot/imported/chunk_0000_grass_lite.png-12306f832f5f55ac4ec98276bdf209b9.s3tc.ctex"
path.etc2="res://.godot/imported/chunk_0000_grass_lite.png-12306f832f5f55ac4ec98276bdf209b9.etc2.ctex"
metadata={
"imported_formats": ["s3tc_bptc", "etc2_astc"],
"vram_texture": true
}
generator_parameters={
"md5": "4dc1ac400d85d9b3e4f43a5af0a38ed4"
}
[deps]
source_file="res://world/generation/chunks/assets/chunk_0000_grass_lite.png"
dest_files=["res://.godot/imported/chunk_0000_grass_lite.png-12306f832f5f55ac4ec98276bdf209b9.s3tc.ctex", "res://.godot/imported/chunk_0000_grass_lite.png-12306f832f5f55ac4ec98276bdf209b9.etc2.ctex"]
[params]
compress/mode=2
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=0

Binary file not shown.

View file

@ -0,0 +1,45 @@
[remap]
importer="scene"
importer_version=1
type="PackedScene"
uid="uid://uxb30e8bfw5v"
path="res://.godot/imported/chunk_0001.glb-a498b960fb79406faee2341d1edecb1a.scn"
[deps]
source_file="res://world/generation/chunks/assets/chunk_0001.glb"
dest_files=["res://.godot/imported/chunk_0001.glb-a498b960fb79406faee2341d1edecb1a.scn"]
[params]
nodes/root_type=""
nodes/root_name=""
nodes/root_script=null
mesh_library/use_node_names_as_mesh_names=false
array_mesh/deduplicate_surfaces=true
nodes/apply_root_scale=true
nodes/root_scale=1.0
nodes/import_as_skeleton_bones=false
nodes/use_name_suffixes=true
nodes/use_node_type_suffixes=true
meshes/ensure_tangents=true
meshes/generate_lods=true
meshes/create_shadow_meshes=true
meshes/light_baking=1
meshes/lightmap_texel_size=0.2
meshes/force_disable_compression=false
skins/use_named_skins=true
animation/import=true
animation/fps=30
animation/trimming=false
animation/remove_immutable_tracks=true
animation/import_rest_as_RESET=false
import_script/path=""
materials/extract=0
materials/extract_format=0
materials/extract_path=""
_subresources={}
gltf/naming_version=2
gltf/embedded_image_handling=1
gltf/texture_map_mode=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

View file

@ -0,0 +1,45 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://c3s1ebrqrsylj"
path.s3tc="res://.godot/imported/chunk_0001_sand.png-67653175c1ccdaa529250778f806d3f8.s3tc.ctex"
path.etc2="res://.godot/imported/chunk_0001_sand.png-67653175c1ccdaa529250778f806d3f8.etc2.ctex"
metadata={
"imported_formats": ["s3tc_bptc", "etc2_astc"],
"vram_texture": true
}
generator_parameters={
"md5": "adfa66b8fe9da7f3450d3ddfd432b86a"
}
[deps]
source_file="res://world/generation/chunks/assets/chunk_0001_sand.png"
dest_files=["res://.godot/imported/chunk_0001_sand.png-67653175c1ccdaa529250778f806d3f8.s3tc.ctex", "res://.godot/imported/chunk_0001_sand.png-67653175c1ccdaa529250778f806d3f8.etc2.ctex"]
[params]
compress/mode=2
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=0

Binary file not shown.

View file

@ -0,0 +1,45 @@
[remap]
importer="scene"
importer_version=1
type="PackedScene"
uid="uid://dgd530dol7mfd"
path="res://.godot/imported/chunk_0002.glb-0a0d64db0049184f39957f77063387f4.scn"
[deps]
source_file="res://world/generation/chunks/assets/chunk_0002.glb"
dest_files=["res://.godot/imported/chunk_0002.glb-0a0d64db0049184f39957f77063387f4.scn"]
[params]
nodes/root_type=""
nodes/root_name=""
nodes/root_script=null
mesh_library/use_node_names_as_mesh_names=false
array_mesh/deduplicate_surfaces=true
nodes/apply_root_scale=true
nodes/root_scale=1.0
nodes/import_as_skeleton_bones=false
nodes/use_name_suffixes=true
nodes/use_node_type_suffixes=true
meshes/ensure_tangents=true
meshes/generate_lods=true
meshes/create_shadow_meshes=true
meshes/light_baking=1
meshes/lightmap_texel_size=0.2
meshes/force_disable_compression=false
skins/use_named_skins=true
animation/import=true
animation/fps=30
animation/trimming=false
animation/remove_immutable_tracks=true
animation/import_rest_as_RESET=false
import_script/path=""
materials/extract=0
materials/extract_format=0
materials/extract_path=""
_subresources={}
gltf/naming_version=2
gltf/embedded_image_handling=1
gltf/texture_map_mode=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

View file

@ -0,0 +1,45 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://delam1fb8dqwx"
path.s3tc="res://.godot/imported/chunk_0002_grass_lite.png-c1692a06eb751734c5b996f92a9ca1af.s3tc.ctex"
path.etc2="res://.godot/imported/chunk_0002_grass_lite.png-c1692a06eb751734c5b996f92a9ca1af.etc2.ctex"
metadata={
"imported_formats": ["s3tc_bptc", "etc2_astc"],
"vram_texture": true
}
generator_parameters={
"md5": "4dc1ac400d85d9b3e4f43a5af0a38ed4"
}
[deps]
source_file="res://world/generation/chunks/assets/chunk_0002_grass_lite.png"
dest_files=["res://.godot/imported/chunk_0002_grass_lite.png-c1692a06eb751734c5b996f92a9ca1af.s3tc.ctex", "res://.godot/imported/chunk_0002_grass_lite.png-c1692a06eb751734c5b996f92a9ca1af.etc2.ctex"]
[params]
compress/mode=2
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=0

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

View file

@ -0,0 +1,45 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bjsjdf3lknj1g"
path.s3tc="res://.godot/imported/chunk_0002_sand.png-044d6a274c3b25b166e2b43f026482ed.s3tc.ctex"
path.etc2="res://.godot/imported/chunk_0002_sand.png-044d6a274c3b25b166e2b43f026482ed.etc2.ctex"
metadata={
"imported_formats": ["s3tc_bptc", "etc2_astc"],
"vram_texture": true
}
generator_parameters={
"md5": "adfa66b8fe9da7f3450d3ddfd432b86a"
}
[deps]
source_file="res://world/generation/chunks/assets/chunk_0002_sand.png"
dest_files=["res://.godot/imported/chunk_0002_sand.png-044d6a274c3b25b166e2b43f026482ed.s3tc.ctex", "res://.godot/imported/chunk_0002_sand.png-044d6a274c3b25b166e2b43f026482ed.etc2.ctex"]
[params]
compress/mode=2
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=0

Binary file not shown.

View file

@ -0,0 +1,45 @@
[remap]
importer="scene"
importer_version=1
type="PackedScene"
uid="uid://2wl647bi18v5"
path="res://.godot/imported/chunk_0003.glb-913e99fdd0f30f24832fe5a7c1b7b865.scn"
[deps]
source_file="res://world/generation/chunks/assets/chunk_0003.glb"
dest_files=["res://.godot/imported/chunk_0003.glb-913e99fdd0f30f24832fe5a7c1b7b865.scn"]
[params]
nodes/root_type=""
nodes/root_name=""
nodes/root_script=null
mesh_library/use_node_names_as_mesh_names=false
array_mesh/deduplicate_surfaces=true
nodes/apply_root_scale=true
nodes/root_scale=1.0
nodes/import_as_skeleton_bones=false
nodes/use_name_suffixes=true
nodes/use_node_type_suffixes=true
meshes/ensure_tangents=true
meshes/generate_lods=true
meshes/create_shadow_meshes=true
meshes/light_baking=1
meshes/lightmap_texel_size=0.2
meshes/force_disable_compression=false
skins/use_named_skins=true
animation/import=true
animation/fps=30
animation/trimming=false
animation/remove_immutable_tracks=true
animation/import_rest_as_RESET=false
import_script/path=""
materials/extract=0
materials/extract_format=0
materials/extract_path=""
_subresources={}
gltf/naming_version=2
gltf/embedded_image_handling=1
gltf/texture_map_mode=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

View file

@ -0,0 +1,45 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://614ano6tj7ny"
path.s3tc="res://.godot/imported/chunk_0003_dirt.png-aec7f6dc456d2c2fe0bb22bdba6b692f.s3tc.ctex"
path.etc2="res://.godot/imported/chunk_0003_dirt.png-aec7f6dc456d2c2fe0bb22bdba6b692f.etc2.ctex"
metadata={
"imported_formats": ["s3tc_bptc", "etc2_astc"],
"vram_texture": true
}
generator_parameters={
"md5": "07d90b3c46a4c3d0c172bed143dc9436"
}
[deps]
source_file="res://world/generation/chunks/assets/chunk_0003_dirt.png"
dest_files=["res://.godot/imported/chunk_0003_dirt.png-aec7f6dc456d2c2fe0bb22bdba6b692f.s3tc.ctex", "res://.godot/imported/chunk_0003_dirt.png-aec7f6dc456d2c2fe0bb22bdba6b692f.etc2.ctex"]
[params]
compress/mode=2
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=0

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

View file

@ -0,0 +1,45 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://c2votr034k2g3"
path.s3tc="res://.godot/imported/chunk_0003_dirt_wall.png-19ae8be57bf9c33db0df849cb088b83c.s3tc.ctex"
path.etc2="res://.godot/imported/chunk_0003_dirt_wall.png-19ae8be57bf9c33db0df849cb088b83c.etc2.ctex"
metadata={
"imported_formats": ["s3tc_bptc", "etc2_astc"],
"vram_texture": true
}
generator_parameters={
"md5": "b5430dba0b52db87d30bae85ae640b09"
}
[deps]
source_file="res://world/generation/chunks/assets/chunk_0003_dirt_wall.png"
dest_files=["res://.godot/imported/chunk_0003_dirt_wall.png-19ae8be57bf9c33db0df849cb088b83c.s3tc.ctex", "res://.godot/imported/chunk_0003_dirt_wall.png-19ae8be57bf9c33db0df849cb088b83c.etc2.ctex"]
[params]
compress/mode=2
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=0

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

View file

@ -0,0 +1,45 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cynuisx0jdsq5"
path.s3tc="res://.godot/imported/chunk_0003_grass_lite.png-4a0a167fc9e6752477003141049a1d21.s3tc.ctex"
path.etc2="res://.godot/imported/chunk_0003_grass_lite.png-4a0a167fc9e6752477003141049a1d21.etc2.ctex"
metadata={
"imported_formats": ["s3tc_bptc", "etc2_astc"],
"vram_texture": true
}
generator_parameters={
"md5": "4dc1ac400d85d9b3e4f43a5af0a38ed4"
}
[deps]
source_file="res://world/generation/chunks/assets/chunk_0003_grass_lite.png"
dest_files=["res://.godot/imported/chunk_0003_grass_lite.png-4a0a167fc9e6752477003141049a1d21.s3tc.ctex", "res://.godot/imported/chunk_0003_grass_lite.png-4a0a167fc9e6752477003141049a1d21.etc2.ctex"]
[params]
compress/mode=2
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=0

Binary file not shown.

View file

@ -0,0 +1,45 @@
[remap]
importer="scene"
importer_version=1
type="PackedScene"
uid="uid://codcyn5ta6aj"
path="res://.godot/imported/chunk_0004.glb-0ea01a6391edb5f687fee1991ba71993.scn"
[deps]
source_file="res://world/generation/chunks/assets/chunk_0004.glb"
dest_files=["res://.godot/imported/chunk_0004.glb-0ea01a6391edb5f687fee1991ba71993.scn"]
[params]
nodes/root_type=""
nodes/root_name=""
nodes/root_script=null
mesh_library/use_node_names_as_mesh_names=false
array_mesh/deduplicate_surfaces=true
nodes/apply_root_scale=true
nodes/root_scale=1.0
nodes/import_as_skeleton_bones=false
nodes/use_name_suffixes=true
nodes/use_node_type_suffixes=true
meshes/ensure_tangents=true
meshes/generate_lods=true
meshes/create_shadow_meshes=true
meshes/light_baking=1
meshes/lightmap_texel_size=0.2
meshes/force_disable_compression=false
skins/use_named_skins=true
animation/import=true
animation/fps=30
animation/trimming=false
animation/remove_immutable_tracks=true
animation/import_rest_as_RESET=false
import_script/path=""
materials/extract=0
materials/extract_format=0
materials/extract_path=""
_subresources={}
gltf/naming_version=2
gltf/embedded_image_handling=1
gltf/texture_map_mode=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

View file

@ -0,0 +1,45 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cdpfqe4vn7235"
path.s3tc="res://.godot/imported/chunk_0004_dirt.png-9c64a7ac5c9e75161623be94b3197f43.s3tc.ctex"
path.etc2="res://.godot/imported/chunk_0004_dirt.png-9c64a7ac5c9e75161623be94b3197f43.etc2.ctex"
metadata={
"imported_formats": ["s3tc_bptc", "etc2_astc"],
"vram_texture": true
}
generator_parameters={
"md5": "07d90b3c46a4c3d0c172bed143dc9436"
}
[deps]
source_file="res://world/generation/chunks/assets/chunk_0004_dirt.png"
dest_files=["res://.godot/imported/chunk_0004_dirt.png-9c64a7ac5c9e75161623be94b3197f43.s3tc.ctex", "res://.godot/imported/chunk_0004_dirt.png-9c64a7ac5c9e75161623be94b3197f43.etc2.ctex"]
[params]
compress/mode=2
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=0

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

View file

@ -0,0 +1,45 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cm42r4jmqis7h"
path.s3tc="res://.godot/imported/chunk_0004_dirt_wall.png-8cbb5921c02a9dc6be9e3fff14c91409.s3tc.ctex"
path.etc2="res://.godot/imported/chunk_0004_dirt_wall.png-8cbb5921c02a9dc6be9e3fff14c91409.etc2.ctex"
metadata={
"imported_formats": ["s3tc_bptc", "etc2_astc"],
"vram_texture": true
}
generator_parameters={
"md5": "b5430dba0b52db87d30bae85ae640b09"
}
[deps]
source_file="res://world/generation/chunks/assets/chunk_0004_dirt_wall.png"
dest_files=["res://.godot/imported/chunk_0004_dirt_wall.png-8cbb5921c02a9dc6be9e3fff14c91409.s3tc.ctex", "res://.godot/imported/chunk_0004_dirt_wall.png-8cbb5921c02a9dc6be9e3fff14c91409.etc2.ctex"]
[params]
compress/mode=2
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=0

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

View file

@ -0,0 +1,45 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://kvyeovkwx72h"
path.s3tc="res://.godot/imported/chunk_0004_grass_lite.png-152b0775592d1fbccc26c10057d15004.s3tc.ctex"
path.etc2="res://.godot/imported/chunk_0004_grass_lite.png-152b0775592d1fbccc26c10057d15004.etc2.ctex"
metadata={
"imported_formats": ["s3tc_bptc", "etc2_astc"],
"vram_texture": true
}
generator_parameters={
"md5": "4dc1ac400d85d9b3e4f43a5af0a38ed4"
}
[deps]
source_file="res://world/generation/chunks/assets/chunk_0004_grass_lite.png"
dest_files=["res://.godot/imported/chunk_0004_grass_lite.png-152b0775592d1fbccc26c10057d15004.s3tc.ctex", "res://.godot/imported/chunk_0004_grass_lite.png-152b0775592d1fbccc26c10057d15004.etc2.ctex"]
[params]
compress/mode=2
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=0

Binary file not shown.

View file

@ -0,0 +1,45 @@
[remap]
importer="scene"
importer_version=1
type="PackedScene"
uid="uid://cg6jtwiq41mbr"
path="res://.godot/imported/chunk_0005.glb-fccbd2231305ec7b7f5a0903feee3737.scn"
[deps]
source_file="res://world/generation/chunks/assets/chunk_0005.glb"
dest_files=["res://.godot/imported/chunk_0005.glb-fccbd2231305ec7b7f5a0903feee3737.scn"]
[params]
nodes/root_type=""
nodes/root_name=""
nodes/root_script=null
mesh_library/use_node_names_as_mesh_names=false
array_mesh/deduplicate_surfaces=true
nodes/apply_root_scale=true
nodes/root_scale=1.0
nodes/import_as_skeleton_bones=false
nodes/use_name_suffixes=true
nodes/use_node_type_suffixes=true
meshes/ensure_tangents=true
meshes/generate_lods=true
meshes/create_shadow_meshes=true
meshes/light_baking=1
meshes/lightmap_texel_size=0.2
meshes/force_disable_compression=false
skins/use_named_skins=true
animation/import=true
animation/fps=30
animation/trimming=false
animation/remove_immutable_tracks=true
animation/import_rest_as_RESET=false
import_script/path=""
materials/extract=0
materials/extract_format=0
materials/extract_path=""
_subresources={}
gltf/naming_version=2
gltf/embedded_image_handling=1
gltf/texture_map_mode=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

View file

@ -0,0 +1,45 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://diybhc4ciplal"
path.s3tc="res://.godot/imported/chunk_0005_cliff_wall.png-ce0994f179fdecaed785c59a34d7b735.s3tc.ctex"
path.etc2="res://.godot/imported/chunk_0005_cliff_wall.png-ce0994f179fdecaed785c59a34d7b735.etc2.ctex"
metadata={
"imported_formats": ["s3tc_bptc", "etc2_astc"],
"vram_texture": true
}
generator_parameters={
"md5": "fc97184f537cc23f83a731327e10c836"
}
[deps]
source_file="res://world/generation/chunks/assets/chunk_0005_cliff_wall.png"
dest_files=["res://.godot/imported/chunk_0005_cliff_wall.png-ce0994f179fdecaed785c59a34d7b735.s3tc.ctex", "res://.godot/imported/chunk_0005_cliff_wall.png-ce0994f179fdecaed785c59a34d7b735.etc2.ctex"]
[params]
compress/mode=2
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=0

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

View file

@ -0,0 +1,45 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://i6vbptsv8276"
path.s3tc="res://.godot/imported/chunk_0005_grass_lite.png-ed24123dd98452d08e5de6e6b0eaad0d.s3tc.ctex"
path.etc2="res://.godot/imported/chunk_0005_grass_lite.png-ed24123dd98452d08e5de6e6b0eaad0d.etc2.ctex"
metadata={
"imported_formats": ["s3tc_bptc", "etc2_astc"],
"vram_texture": true
}
generator_parameters={
"md5": "4dc1ac400d85d9b3e4f43a5af0a38ed4"
}
[deps]
source_file="res://world/generation/chunks/assets/chunk_0005_grass_lite.png"
dest_files=["res://.godot/imported/chunk_0005_grass_lite.png-ed24123dd98452d08e5de6e6b0eaad0d.s3tc.ctex", "res://.godot/imported/chunk_0005_grass_lite.png-ed24123dd98452d08e5de6e6b0eaad0d.etc2.ctex"]
[params]
compress/mode=2
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=0

Binary file not shown.

View file

@ -0,0 +1,45 @@
[remap]
importer="scene"
importer_version=1
type="PackedScene"
uid="uid://ddjqd4kl4tkhg"
path="res://.godot/imported/chunk_0006.glb-3c19b65d4a38bd1731728ce1ea2dda2b.scn"
[deps]
source_file="res://world/generation/chunks/assets/chunk_0006.glb"
dest_files=["res://.godot/imported/chunk_0006.glb-3c19b65d4a38bd1731728ce1ea2dda2b.scn"]
[params]
nodes/root_type=""
nodes/root_name=""
nodes/root_script=null
mesh_library/use_node_names_as_mesh_names=false
array_mesh/deduplicate_surfaces=true
nodes/apply_root_scale=true
nodes/root_scale=1.0
nodes/import_as_skeleton_bones=false
nodes/use_name_suffixes=true
nodes/use_node_type_suffixes=true
meshes/ensure_tangents=true
meshes/generate_lods=true
meshes/create_shadow_meshes=true
meshes/light_baking=1
meshes/lightmap_texel_size=0.2
meshes/force_disable_compression=false
skins/use_named_skins=true
animation/import=true
animation/fps=30
animation/trimming=false
animation/remove_immutable_tracks=true
animation/import_rest_as_RESET=false
import_script/path=""
materials/extract=0
materials/extract_format=0
materials/extract_path=""
_subresources={}
gltf/naming_version=2
gltf/embedded_image_handling=1
gltf/texture_map_mode=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

View file

@ -0,0 +1,45 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://beiaoue0vy8c7"
path.s3tc="res://.godot/imported/chunk_0006_cliff_wall.png-ac5bf5875f430089a760275aac53f7c5.s3tc.ctex"
path.etc2="res://.godot/imported/chunk_0006_cliff_wall.png-ac5bf5875f430089a760275aac53f7c5.etc2.ctex"
metadata={
"imported_formats": ["s3tc_bptc", "etc2_astc"],
"vram_texture": true
}
generator_parameters={
"md5": "fc97184f537cc23f83a731327e10c836"
}
[deps]
source_file="res://world/generation/chunks/assets/chunk_0006_cliff_wall.png"
dest_files=["res://.godot/imported/chunk_0006_cliff_wall.png-ac5bf5875f430089a760275aac53f7c5.s3tc.ctex", "res://.godot/imported/chunk_0006_cliff_wall.png-ac5bf5875f430089a760275aac53f7c5.etc2.ctex"]
[params]
compress/mode=2
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=0

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

View file

@ -0,0 +1,45 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://djrg33nkvpnih"
path.s3tc="res://.godot/imported/chunk_0006_grass_lite.png-965daad85495f67f8a702417724c30a5.s3tc.ctex"
path.etc2="res://.godot/imported/chunk_0006_grass_lite.png-965daad85495f67f8a702417724c30a5.etc2.ctex"
metadata={
"imported_formats": ["s3tc_bptc", "etc2_astc"],
"vram_texture": true
}
generator_parameters={
"md5": "4dc1ac400d85d9b3e4f43a5af0a38ed4"
}
[deps]
source_file="res://world/generation/chunks/assets/chunk_0006_grass_lite.png"
dest_files=["res://.godot/imported/chunk_0006_grass_lite.png-965daad85495f67f8a702417724c30a5.s3tc.ctex", "res://.godot/imported/chunk_0006_grass_lite.png-965daad85495f67f8a702417724c30a5.etc2.ctex"]
[params]
compress/mode=2
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=0

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

View file

@ -0,0 +1,45 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://vxomtbcs2mc0"
path.s3tc="res://.godot/imported/chunk_0006_sand.png-b81070e87c8688cdfae71416692c7e7b.s3tc.ctex"
path.etc2="res://.godot/imported/chunk_0006_sand.png-b81070e87c8688cdfae71416692c7e7b.etc2.ctex"
metadata={
"imported_formats": ["s3tc_bptc", "etc2_astc"],
"vram_texture": true
}
generator_parameters={
"md5": "adfa66b8fe9da7f3450d3ddfd432b86a"
}
[deps]
source_file="res://world/generation/chunks/assets/chunk_0006_sand.png"
dest_files=["res://.godot/imported/chunk_0006_sand.png-b81070e87c8688cdfae71416692c7e7b.s3tc.ctex", "res://.godot/imported/chunk_0006_sand.png-b81070e87c8688cdfae71416692c7e7b.etc2.ctex"]
[params]
compress/mode=2
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=0

Binary file not shown.

View file

@ -0,0 +1,45 @@
[remap]
importer="scene"
importer_version=1
type="PackedScene"
uid="uid://cniughsb3gacd"
path="res://.godot/imported/chunk_0007.glb-ba9f86d0e228adfa3c9dcc6c2c6d2bea.scn"
[deps]
source_file="res://world/generation/chunks/assets/chunk_0007.glb"
dest_files=["res://.godot/imported/chunk_0007.glb-ba9f86d0e228adfa3c9dcc6c2c6d2bea.scn"]
[params]
nodes/root_type=""
nodes/root_name=""
nodes/root_script=null
mesh_library/use_node_names_as_mesh_names=false
array_mesh/deduplicate_surfaces=true
nodes/apply_root_scale=true
nodes/root_scale=1.0
nodes/import_as_skeleton_bones=false
nodes/use_name_suffixes=true
nodes/use_node_type_suffixes=true
meshes/ensure_tangents=true
meshes/generate_lods=true
meshes/create_shadow_meshes=true
meshes/light_baking=1
meshes/lightmap_texel_size=0.2
meshes/force_disable_compression=false
skins/use_named_skins=true
animation/import=true
animation/fps=30
animation/trimming=false
animation/remove_immutable_tracks=true
animation/import_rest_as_RESET=false
import_script/path=""
materials/extract=0
materials/extract_format=0
materials/extract_path=""
_subresources={}
gltf/naming_version=2
gltf/embedded_image_handling=1
gltf/texture_map_mode=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

View file

@ -0,0 +1,45 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://b2xro1br3b8hr"
path.s3tc="res://.godot/imported/chunk_0007_cliff_wall.png-b91883984675adf65a5448c549fa5414.s3tc.ctex"
path.etc2="res://.godot/imported/chunk_0007_cliff_wall.png-b91883984675adf65a5448c549fa5414.etc2.ctex"
metadata={
"imported_formats": ["s3tc_bptc", "etc2_astc"],
"vram_texture": true
}
generator_parameters={
"md5": "fc97184f537cc23f83a731327e10c836"
}
[deps]
source_file="res://world/generation/chunks/assets/chunk_0007_cliff_wall.png"
dest_files=["res://.godot/imported/chunk_0007_cliff_wall.png-b91883984675adf65a5448c549fa5414.s3tc.ctex", "res://.godot/imported/chunk_0007_cliff_wall.png-b91883984675adf65a5448c549fa5414.etc2.ctex"]
[params]
compress/mode=2
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=0

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

View file

@ -0,0 +1,45 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://3skvv046r4cn"
path.s3tc="res://.godot/imported/chunk_0007_grass_lite.png-2d878613a1a7bd71881adeafa310116a.s3tc.ctex"
path.etc2="res://.godot/imported/chunk_0007_grass_lite.png-2d878613a1a7bd71881adeafa310116a.etc2.ctex"
metadata={
"imported_formats": ["s3tc_bptc", "etc2_astc"],
"vram_texture": true
}
generator_parameters={
"md5": "4dc1ac400d85d9b3e4f43a5af0a38ed4"
}
[deps]
source_file="res://world/generation/chunks/assets/chunk_0007_grass_lite.png"
dest_files=["res://.godot/imported/chunk_0007_grass_lite.png-2d878613a1a7bd71881adeafa310116a.s3tc.ctex", "res://.godot/imported/chunk_0007_grass_lite.png-2d878613a1a7bd71881adeafa310116a.etc2.ctex"]
[params]
compress/mode=2
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=0

View file

@ -0,0 +1,16 @@
[gd_resource type="Resource" script_class="TerrainChunkDefinition" load_steps=3 format=3]
[ext_resource type="Script" path="res://world/generation/terrain_chunk_definition.gd" id="1_definition"]
[ext_resource type="PackedScene" path="res://world/generation/chunks/assets/chunk_0000.glb" id="2_scene"]
[resource]
script = ExtResource("1_definition")
stable_id = &"chunk_0000"
label = "grass"
packed_scene = ExtResource("2_scene")
primary_mesh_name = &"chunk_0000"
allowed_rotation_mask = 15
selection_weight = 4.0
tags = PackedStringArray("land", "walkable", "grass", "flat", "spawn_safe")
allowed_neighbor_tags = PackedStringArray("grass", "sand", "coast", "fresh_water")
preferred_neighbor_tags = PackedStringArray("grass")

View file

@ -0,0 +1,18 @@
[gd_resource type="Resource" script_class="TerrainChunkDefinition" load_steps=3 format=3]
[ext_resource type="Script" path="res://world/generation/terrain_chunk_definition.gd" id="1_definition"]
[ext_resource type="PackedScene" path="res://world/generation/chunks/assets/chunk_0001.glb" id="2_scene"]
[resource]
script = ExtResource("1_definition")
stable_id = &"chunk_0001"
label = "sand"
packed_scene = ExtResource("2_scene")
primary_mesh_name = &"chunk_0001"
allowed_rotation_mask = 15
selection_weight = 1.5
tags = PackedStringArray("land", "walkable", "sand", "flat")
allowed_neighbor_tags = PackedStringArray("sand", "grass")
required_neighbor_tags = PackedStringArray("coast")
minimum_required_neighbors = 1
preferred_neighbor_tags = PackedStringArray("coast", "sand")

View file

@ -0,0 +1,19 @@
[gd_resource type="Resource" script_class="TerrainChunkDefinition" format=3 uid="uid://b2ha4ekenoiaj"]
[ext_resource type="Script" uid="uid://ck6m4auf5epde" path="res://world/generation/terrain_chunk_definition.gd" id="1_definition"]
[ext_resource type="PackedScene" uid="uid://dgd530dol7mfd" path="res://world/generation/chunks/assets/chunk_0002.glb" id="2_scene"]
[resource]
script = ExtResource("1_definition")
stable_id = &"chunk_0002"
label = "beach"
packed_scene = ExtResource("2_scene")
primary_mesh_name = &"chunk_0002"
selection_weight = 0.75
maximum_placements = 20
tags = PackedStringArray("land", "walkable", "sand", "coast", "slope")
allowed_neighbor_tags = PackedStringArray("sand", "grass")
required_neighbor_tags = PackedStringArray("sand")
minimum_required_neighbors = 1
preferred_neighbor_tags = PackedStringArray("sand", "grass")
ocean_facing_edges = 2

View file

@ -0,0 +1,19 @@
[gd_resource type="Resource" script_class="TerrainChunkDefinition" format=3 uid="uid://dl7tsxml6g6na"]
[ext_resource type="Script" uid="uid://ck6m4auf5epde" path="res://world/generation/terrain_chunk_definition.gd" id="1_definition"]
[ext_resource type="PackedScene" uid="uid://2wl647bi18v5" path="res://world/generation/chunks/assets/chunk_0003.glb" id="2_scene"]
[resource]
script = ExtResource("1_definition")
stable_id = &"chunk_0003"
label = "stream"
packed_scene = ExtResource("2_scene")
primary_mesh_name = &"chunk_0003"
selection_weight = 0.6
tags = PackedStringArray("land", "walkable", "fresh_water", "river", "flowing")
allowed_neighbor_tags = PackedStringArray("grass")
preferred_neighbor_tags = PackedStringArray("grass")
water_inlet_edges = 1
water_outlet_edges = 4
water_surface_size = Vector2(2.1, 10)
water_surface_offset = Vector2(-0.315, 0)

View file

@ -0,0 +1,18 @@
[gd_resource type="Resource" script_class="TerrainChunkDefinition" format=3 uid="uid://duotyi05ua5gp"]
[ext_resource type="Script" uid="uid://ck6m4auf5epde" path="res://world/generation/terrain_chunk_definition.gd" id="1_definition"]
[ext_resource type="PackedScene" uid="uid://codcyn5ta6aj" path="res://world/generation/chunks/assets/chunk_0004.glb" id="2_scene"]
[resource]
script = ExtResource("1_definition")
stable_id = &"chunk_0004"
label = "pond"
packed_scene = ExtResource("2_scene")
primary_mesh_name = &"chunk_0004"
selection_weight = 0.35
tags = PackedStringArray("land", "walkable", "fresh_water", "pond")
allowed_neighbor_tags = PackedStringArray("grass")
preferred_neighbor_tags = PackedStringArray("grass")
water_inlet_edges = 1
water_surface_size = Vector2(6.7, 7.75)
water_surface_offset = Vector2(-0.32, -1.03)

View file

@ -0,0 +1,21 @@
[gd_resource type="Resource" script_class="TerrainChunkDefinition" load_steps=3 format=3]
[ext_resource type="Script" path="res://world/generation/terrain_chunk_definition.gd" id="1_definition"]
[ext_resource type="PackedScene" uid="uid://cg6jtwiq41mbr" path="res://world/generation/chunks/assets/chunk_0005.glb" id="2_scene"]
[resource]
script = ExtResource("1_definition")
stable_id = &"chunk_0005"
label = "grass ocean edge"
packed_scene = ExtResource("2_scene")
primary_mesh_name = &"chunk_0005"
selection_weight = 1.0
maximum_placements = 20
tags = PackedStringArray("land", "walkable", "grass", "coast", "cliff", "grass_ocean_edge")
allowed_neighbor_tags = PackedStringArray("grass", "sand", "coast")
north_allowed_neighbor_tags = PackedStringArray("coast")
south_allowed_neighbor_tags = PackedStringArray("coast")
west_allowed_neighbor_tags = PackedStringArray("grass")
preferred_neighbor_tags = PackedStringArray("grass", "coast")
prefers_map_boundary = true
ocean_facing_edges = 2

View file

@ -0,0 +1,23 @@
[gd_resource type="Resource" script_class="TerrainChunkDefinition" load_steps=3 format=3]
[ext_resource type="Script" path="res://world/generation/terrain_chunk_definition.gd" id="1_definition"]
[ext_resource type="PackedScene" uid="uid://ddjqd4kl4tkhg" path="res://world/generation/chunks/assets/chunk_0006.glb" id="2_scene"]
[resource]
script = ExtResource("1_definition")
stable_id = &"chunk_0006"
label = "grass to beach ocean edge"
packed_scene = ExtResource("2_scene")
primary_mesh_name = &"chunk_0006"
selection_weight = 0.35
maximum_placements = 8
tags = PackedStringArray("land", "walkable", "sand", "coast", "transition", "slope")
allowed_neighbor_tags = PackedStringArray("grass", "sand", "coast")
north_allowed_neighbor_tags = PackedStringArray("coast")
south_allowed_neighbor_tags = PackedStringArray("grass_ocean_edge")
west_allowed_neighbor_tags = PackedStringArray("grass")
required_neighbor_tags = PackedStringArray("coast")
minimum_required_neighbors = 2
preferred_neighbor_tags = PackedStringArray("coast")
prefers_map_boundary = true
ocean_facing_edges = 2

View file

@ -0,0 +1,22 @@
[gd_resource type="Resource" script_class="TerrainChunkDefinition" load_steps=3 format=3]
[ext_resource type="Script" path="res://world/generation/terrain_chunk_definition.gd" id="1_definition"]
[ext_resource type="PackedScene" uid="uid://cniughsb3gacd" path="res://world/generation/chunks/assets/chunk_0007.glb" id="2_scene"]
[resource]
script = ExtResource("1_definition")
stable_id = &"chunk_0007"
label = "grass ocean corner"
packed_scene = ExtResource("2_scene")
primary_mesh_name = &"chunk_0007"
selection_weight = 0.2
maximum_placements = 4
tags = PackedStringArray("land", "walkable", "grass", "coast", "cliff", "corner")
allowed_neighbor_tags = PackedStringArray("grass", "coast")
north_allowed_neighbor_tags = PackedStringArray("grass_ocean_edge")
west_allowed_neighbor_tags = PackedStringArray("grass_ocean_edge")
required_neighbor_tags = PackedStringArray("grass_ocean_edge")
minimum_required_neighbors = 2
preferred_neighbor_tags = PackedStringArray("coast")
prefers_map_boundary = true
ocean_facing_edges = 6

View file

@ -0,0 +1,19 @@
[gd_resource type="Resource" script_class="TerrainChunkDefinition" load_steps=3 format=3]
[ext_resource type="Script" path="res://world/generation/terrain_chunk_definition.gd" id="1_definition"]
[ext_resource type="PackedScene" path="res://world/generation/chunks/assets/chunk_0000.glb" id="2_scene"]
[resource]
script = ExtResource("1_definition")
stable_id = &"chunk_spawn"
label = "spawn"
packed_scene = ExtResource("2_scene")
primary_mesh_name = &"chunk_0000"
allowed_rotation_mask = 1
selection_weight = 0.01
maximum_placements = 1
tags = PackedStringArray("land", "walkable", "grass", "flat", "spawn")
allowed_neighbor_tags = PackedStringArray("grass", "coast")
required_neighbor_tags = PackedStringArray("spawn_safe")
minimum_required_neighbors = 3
preferred_neighbor_tags = PackedStringArray("spawn_safe")

View file

@ -0,0 +1,18 @@
[gd_resource type="Resource" script_class="TerrainChunkCatalog" load_steps=12 format=3]
[ext_resource type="Script" path="res://world/generation/terrain_chunk_catalog.gd" id="1_catalog"]
[ext_resource type="Resource" path="res://world/generation/chunks/definitions/chunk_0000.tres" id="2_grass"]
[ext_resource type="Resource" path="res://world/generation/chunks/definitions/chunk_0001.tres" id="3_sand"]
[ext_resource type="Resource" path="res://world/generation/chunks/definitions/chunk_0002.tres" id="4_beach"]
[ext_resource type="Resource" path="res://world/generation/chunks/definitions/chunk_0003.tres" id="5_stream"]
[ext_resource type="Resource" path="res://world/generation/chunks/definitions/chunk_0004.tres" id="6_pond"]
[ext_resource type="Script" path="res://world/generation/terrain_chunk_definition.gd" id="7_definition_script"]
[ext_resource type="Resource" path="res://world/generation/chunks/definitions/chunk_spawn.tres" id="8_spawn"]
[ext_resource type="Resource" path="res://world/generation/chunks/definitions/chunk_0005.tres" id="9_grass_ocean_edge"]
[ext_resource type="Resource" path="res://world/generation/chunks/definitions/chunk_0006.tres" id="10_grass_beach_transition"]
[ext_resource type="Resource" path="res://world/generation/chunks/definitions/chunk_0007.tres" id="11_grass_ocean_corner"]
[resource]
script = ExtResource("1_catalog")
chunk_size = 10.0
definitions = Array[ExtResource("7_definition_script")]([ExtResource("2_grass"), ExtResource("3_sand"), ExtResource("4_beach"), ExtResource("5_stream"), ExtResource("6_pond"), ExtResource("9_grass_ocean_edge"), ExtResource("10_grass_beach_transition"), ExtResource("11_grass_ocean_corner"), ExtResource("8_spawn")])

Some files were not shown because too many files have changed in this diff Show more