Improve PortMaster performance and controls

This commit is contained in:
Alexander Sellite 2026-08-15 09:53:30 -04:00
parent 9afa60f6a5
commit 57df54d82d
14 changed files with 328 additions and 126 deletions

View file

@ -15,13 +15,17 @@ directory. The templates in `scripts/portmaster/` are authoritative.
## Release contract ## Release contract
- `port.json` uses schema version 4 and names `netfishing.zip`. - `port.json` uses schema version 4, names `netfishing.zip`, and marks the
self-contained package ready to run.
- `NETfishing.sh` starts with `# PORTMASTER: netfishing.zip, NETfishing.sh`. - `NETfishing.sh` starts with `# PORTMASTER: netfishing.zip, NETfishing.sh`.
- The executable is `netfishing/NETfishing.aarch64`. - The executable is `netfishing/NETfishing.aarch64`.
- The package declares AArch64, two analog sticks, GLIBC 2.28, and - The package declares AArch64, two analog sticks, GLIBC 2.28, and
`weston_pkg_0.2.squashfs`. `weston_pkg_0.2.squashfs`.
- The launcher must not start GPTOKEYB. Godot and NETfishing's controller - The launcher starts GPTOKEYB in exit-only mode without a `-c` mapping file.
mapping manager handle controller input directly. PortMaster therefore owns only its device-specific force-quit chord while
Godot and NETfishing's controller mapping manager handle gameplay input.
- The launcher calls `pm_platform_helper` for the game executable after
starting GPTOKEYB and calls `pm_finish` after the game exits.
- Persistent device data remains under `netfishing/conf/data`, - Persistent device data remains under `netfishing/conf/data`,
`netfishing/conf/config`, and `netfishing/conf/cache`. `netfishing/conf/config`, and `netfishing/conf/cache`.
- The archive must not contain `conf/`, saves, identities, logs, source files, - The archive must not contain `conf/`, saves, identities, logs, source files,
@ -69,11 +73,14 @@ Before upgrading an existing installation, preserve
`/mnt/mmc/ports/netfishing/conf/` on the device. After installation: `/mnt/mmc/ports/netfishing/conf/` on the device. After installation:
1. Confirm the installed executable and PCK hashes match the staged release. 1. Confirm the installed executable and PCK hashes match the staged release.
2. Confirm the installed launcher contains the canonical PortMaster header and 2. Confirm the installed launcher contains the canonical PortMaster header,
does not contain `GPTOKEYB`. starts GPTOKEYB without a `-c` mapping file, and calls
`pm_platform_helper` for the game executable.
3. Confirm `conf/` was not replaced or removed. 3. Confirm `conf/` was not replaced or removed.
4. Launch the installed port through the normal muOS menu. 4. Launch the installed port through the normal muOS menu.
5. Verify the displayed game version and controller face-button mapping. 5. Verify the displayed game version and controller face-button mapping.
6. Verify PortMaster's force-quit chord exits the game. This is Start+Select on
most devices.
Installing the public PortMaster catalog entry can still install an older Installing the public PortMaster catalog entry can still install an older
catalog build until the upstream `netfishing.zip` is updated. A local release catalog build until the upstream `netfishing.zip` is updated. A local release
@ -127,6 +134,11 @@ as `SDL_GAMECONTROLLERCONFIG` in the game command after Weston initializes;
Weston sources PortMaster's control file internally and otherwise restores the Weston sources PortMaster's control file internally and otherwise restores the
SDL2 value. SDL2 value.
Do not pass a `.gptk` controller mapping to GPTOKEYB. Its exit-only process
must retain PortMaster's controller configuration, while the verified Godot
mapping remains scoped to the game command. This prevents duplicate or
translated gameplay events while preserving the system force-quit chord.
Do not call `Input.add_joy_mapping(..., true)` for a recognized connected muOS Do not call `Input.add_joy_mapping(..., true)` for a recognized connected muOS
controller. Updating this virtual controller after connection can stop Godot controller. Updating this virtual controller after connection can stop Godot
from delivering its standardized controller events until restart. from delivering its standardized controller events until restart.
@ -162,7 +174,9 @@ also:
- disables the additional full-screen world-pixelation pass; - disables the additional full-screen world-pixelation pass;
- replaces animated depth-aware water shaders with opaque, per-vertex water; - replaces animated depth-aware water shaders with opaque, per-vertex water;
- disables ocean surface motion; - disables ocean surface motion;
- reduces rain to 256 particles simulated at 15 FPS; and - disables foliage wind;
- keeps the title water and full-window menu patterns static;
- reduces rain to 128 particles simulated at 12 FPS; and
- replaces procedural sky clouds and 81 moving local cloud patches with one - replaces procedural sky clouds and 81 moving local cloud patches with one
flat cloud ceiling. flat cloud ceiling.

View file

@ -259,6 +259,9 @@ func _ready() -> void:
_test_world.set_light_performance_profile( _test_world.set_light_performance_profile(
_performance_profile.is_light() _performance_profile.is_light()
) )
_configure_presentation_performance_profile(
_performance_profile.is_light()
)
DisplayServer.window_set_title("NETfishing") DisplayServer.window_set_title("NETfishing")
_rain_ambience = RainAmbienceType.new() _rain_ambience = RainAmbienceType.new()
_rain_ambience.name = "RainAmbience" _rain_ambience.name = "RainAmbience"
@ -268,28 +271,6 @@ func _ready() -> void:
_player, _player,
_test_world.get_saltwater_shoreline_mesh(), _test_world.get_saltwater_shoreline_mesh(),
) )
var menu_pattern_material := (
_player_menu_backdrop.material as ShaderMaterial
)
if menu_pattern_material != null:
menu_pattern_material.set_shader_parameter(
"display_scale",
PLAYER_MENU_PATTERN_SCALE,
)
menu_pattern_material.set_shader_parameter(
"scroll_velocity_pixels",
PLAYER_MENU_PATTERN_SCROLL_VELOCITY,
)
var shop_pattern_material := _shop_backdrop.material as ShaderMaterial
if shop_pattern_material != null:
shop_pattern_material.set_shader_parameter(
"display_scale",
SHOP_PATTERN_SCALE,
)
shop_pattern_material.set_shader_parameter(
"scroll_velocity_pixels",
PLAYER_MENU_PATTERN_SCROLL_VELOCITY,
)
get_window().size_changed.connect(_resize_native_overlays) get_window().size_changed.connect(_resize_native_overlays)
_resize_native_overlays() _resize_native_overlays()
if not _settings_manager.settings_changed.is_connected( if not _settings_manager.settings_changed.is_connected(
@ -305,6 +286,53 @@ func _ready() -> void:
_show_data_root_setup() _show_data_root_setup()
func _configure_presentation_performance_profile(light_profile: bool) -> void:
var title_water_material := _title_background.material as ShaderMaterial
if title_water_material != null:
title_water_material.set_shader_parameter(
"animation_enabled",
not light_profile,
)
var menu_pattern_material := (
_player_menu_backdrop.material as ShaderMaterial
)
if menu_pattern_material != null:
menu_pattern_material.set_shader_parameter(
"animation_enabled",
not light_profile,
)
menu_pattern_material.set_shader_parameter(
"display_scale",
PLAYER_MENU_PATTERN_SCALE,
)
menu_pattern_material.set_shader_parameter(
"scroll_velocity_pixels",
(
Vector2.ZERO
if light_profile
else PLAYER_MENU_PATTERN_SCROLL_VELOCITY
),
)
var shop_pattern_material := _shop_backdrop.material as ShaderMaterial
if shop_pattern_material != null:
shop_pattern_material.set_shader_parameter(
"animation_enabled",
not light_profile,
)
shop_pattern_material.set_shader_parameter(
"display_scale",
SHOP_PATTERN_SCALE,
)
shop_pattern_material.set_shader_parameter(
"scroll_velocity_pixels",
(
Vector2.ZERO
if light_profile
else PLAYER_MENU_PATTERN_SCROLL_VELOCITY
),
)
func _is_dedicated_server_runtime() -> bool: func _is_dedicated_server_runtime() -> bool:
if OS.has_feature("dedicated_server"): if OS.has_feature("dedicated_server"):
return true return true

View file

@ -144,6 +144,11 @@ PortMaster launches use the light performance profile by default. Put the word
\`normal\` in \`netfishing/conf/performance_profile\` to opt a capable device \`normal\` in \`netfishing/conf/performance_profile\` to opt a capable device
into the normal rendering profile. See \`netfishing/licenses/\` for bundled into the normal rendering profile. See \`netfishing/licenses/\` for bundled
credits and license information. credits and license information.
## Controls
NETfishing reads the handheld controller directly. PortMaster's system
force-quit chord remains available; this is Start+Select on most devices.
EOF EOF
( (
@ -163,14 +168,26 @@ if [[ "${TOP_LEVELS}" != "${EXPECTED_TOP_LEVELS}" ]]; then
fi fi
grep -q '"version": 4' "${STAGE_ROOT}/port.json" grep -q '"version": 4' "${STAGE_ROOT}/port.json"
grep -q '"name": "netfishing.zip"' "${STAGE_ROOT}/port.json" grep -q '"name": "netfishing.zip"' "${STAGE_ROOT}/port.json"
grep -q '"rtr": true' "${STAGE_ROOT}/port.json"
grep -q '^# PORTMASTER: netfishing.zip, NETfishing.sh$' \ grep -q '^# PORTMASTER: netfishing.zip, NETfishing.sh$' \
"${STAGE_ROOT}/NETfishing.sh" "${STAGE_ROOT}/NETfishing.sh"
grep -Fq 'PROFILE_PATH="$CONFDIR/performance_profile"' \ grep -Fq 'PROFILE_PATH="$CONFDIR/performance_profile"' \
"${STAGE_ROOT}/NETfishing.sh" "${STAGE_ROOT}/NETfishing.sh"
grep -Fq 'NETFISHING_PERFORMANCE_PROFILE=light' \ grep -Fq 'NETFISHING_PERFORMANCE_PROFILE=light' \
"${STAGE_ROOT}/NETfishing.sh" "${STAGE_ROOT}/NETfishing.sh"
if grep -q 'GPTOKEYB' "${STAGE_ROOT}/NETfishing.sh"; then grep -Fq '$GPTOKEYB "NETfishing.aarch64" &' \
echo "GPTOKEYB must not be enabled for NETfishing." >&2 "${STAGE_ROOT}/NETfishing.sh"
grep -Fq 'pm_platform_helper "$GAME_EXECUTABLE"' \
"${STAGE_ROOT}/NETfishing.sh"
grep -Fq 'pm_finish' "${STAGE_ROOT}/NETfishing.sh"
if [[ "$(grep -Ec '^[[:space:]]*\$GPTOKEYB[[:space:]]' \
"${STAGE_ROOT}/NETfishing.sh")" -ne 1 ]]; then
echo "NETfishing must start exactly one GPTOKEYB exit handler." >&2
exit 1
fi
if grep -Eq '\$GPTOKEYB.*[[:space:]]-c([[:space:]]|$)' \
"${STAGE_ROOT}/NETfishing.sh"; then
echo "GPTOKEYB controller mappings must not be enabled for NETfishing." >&2
exit 1 exit 1
fi fi
if unzip -Z1 "${ARCHIVE}" | grep -E \ if unzip -Z1 "${ARCHIVE}" | grep -E \

View file

@ -117,6 +117,12 @@ case "$PERFORMANCE_PROFILE" in
;; ;;
esac esac
# Keep NETfishing's native controller input separate from PortMaster's exit
# handling. Without a -c mapping file, GPTOKEYB only watches for the
# device-specific force-quit chord and does not inject gameplay inputs.
$GPTOKEYB "NETfishing.aarch64" &
pm_platform_helper "$GAME_EXECUTABLE"
$ESUDO env CRUSTY_RESOLUTION="${DISPLAY_WIDTH}x${DISPLAY_HEIGHT}" \ $ESUDO env CRUSTY_RESOLUTION="${DISPLAY_WIDTH}x${DISPLAY_HEIGHT}" \
"$WESTON_DIR/westonwrap.sh" headless noop kiosk crusty_x11egl \ "$WESTON_DIR/westonwrap.sh" headless noop kiosk crusty_x11egl \
XDG_DATA_HOME="$CONFDIR/data" \ XDG_DATA_HOME="$CONFDIR/data" \

View file

@ -20,7 +20,7 @@
"simulation" "simulation"
], ],
"image": {}, "image": {},
"rtr": false, "rtr": true,
"exp": false, "exp": false,
"runtime": [ "runtime": [
"weston_pkg_0.2.squashfs" "weston_pkg_0.2.squashfs"

View file

@ -216,8 +216,7 @@ func _run() -> void:
) as EmoteRadialMenu ) as EmoteRadialMenu
assert(quick_menu != null and emote_menu != null) assert(quick_menu != null and emote_menu != null)
assert(QuickRadialMenu.ACTIONS.size() == QuickRadialMenu.SECTOR_COUNT) assert(QuickRadialMenu.ACTIONS.size() == QuickRadialMenu.SECTOR_COUNT)
assert(QuickRadialMenu.ACTIONS.has(&"freecam")) assert(QuickRadialMenu.ACTIONS == [&"chat", &"freecam", &"hud"])
assert(QuickRadialMenu.ACTIONS.has(&"hud"))
game_ui.call("_on_quick_action_selected", &"freecam") game_ui.call("_on_quick_action_selected", &"freecam")
assert(player.is_free_camera_active()) assert(player.is_free_camera_active())
var free_camera := player.get_active_gameplay_camera() var free_camera := player.get_active_gameplay_camera()
@ -544,10 +543,6 @@ func _run() -> void:
await process_frame await process_frame
assert(player.hotbar.get_selected_slot() == 1) assert(player.hotbar.get_selected_slot() == 1)
assert(not service.is_active() and not toolbar.visible) assert(not service.is_active() and not toolbar.visible)
game_ui.call("_on_quick_action_selected", &"paint")
await process_frame
assert(player.hotbar.get_selected_slot() == 0)
assert(service.is_active() and toolbar.visible)
print("Art tools validation: PASS") print("Art tools validation: PASS")
var session := main.get_node("%NetworkSession") as NetworkSession var session := main.get_node("%NetworkSession") as NetworkSession

View file

@ -211,12 +211,17 @@ func _validate_portmaster_launcher() -> String:
"leftstick:b9", "leftstick:b9",
"rightstick:b12", "rightstick:b12",
"netfishing_controllerconfig=\"$GODOT_MUOS_MAPPING\"", "netfishing_controllerconfig=\"$GODOT_MUOS_MAPPING\"",
"$GPTOKEYB \"NETfishing.aarch64\" &",
"pm_platform_helper \"$GAME_EXECUTABLE\"",
"SDL_GAMECONTROLLERCONFIG=\"$netfishing_controllerconfig\" \\", "SDL_GAMECONTROLLERCONFIG=\"$netfishing_controllerconfig\" \\",
"pm_finish",
]: ]:
if expected_fragment not in launcher: if expected_fragment not in launcher:
return "PortMaster launcher omitted " + expected_fragment return "PortMaster launcher omitted " + expected_fragment
if "$'\\n'" in launcher: if "$'\\n'" in launcher:
return "PortMaster launcher appends mappings across a Weston-unsafe newline" return "PortMaster launcher appends mappings across a Weston-unsafe newline"
if "$GPTOKEYB \"NETfishing.aarch64\" -c" in launcher:
return "PortMaster launcher injects duplicate GPTOKEYB gameplay mappings"
return "" return ""

View file

@ -4,6 +4,9 @@ const MainScene: PackedScene = preload("res://main/main.tscn")
const RuntimePerformanceProfileType = preload( const RuntimePerformanceProfileType = preload(
"res://main/runtime_performance_profile.gd" "res://main/runtime_performance_profile.gd"
) )
const FOLIAGE_WIND_SHADER: Shader = preload(
"res://world/materials/foliage_wind.gdshader"
)
func _initialize() -> void: func _initialize() -> void:
@ -28,12 +31,27 @@ func _run() -> void:
assert(bool(main.get("_application_initialized"))) assert(bool(main.get("_application_initialized")))
_validate_main_profile(main) _validate_main_profile(main)
_validate_minimal_weather(main) _validate_minimal_weather(main)
_stop_audio_players(main)
main.queue_free() main.queue_free()
for _frame: int in 4: for _frame: int in 8:
await process_frame await process_frame
OS.unset_environment( OS.unset_environment(
RuntimePerformanceProfileType.PROFILE_ENVIRONMENT_VARIABLE RuntimePerformanceProfileType.PROFILE_ENVIRONMENT_VARIABLE
) )
var normal_main: Node = MainScene.instantiate()
root.add_child(normal_main)
for _frame: int in 4:
await process_frame
if not bool(normal_main.get("_application_initialized")):
normal_main.call("_activate_selected_data_path", "", true)
for _frame: int in 8:
await process_frame
assert(bool(normal_main.get("_application_initialized")))
_validate_normal_profile(normal_main)
_stop_audio_players(normal_main)
normal_main.queue_free()
for _frame: int in 8:
await process_frame
print("Light performance profile validation: PASS") print("Light performance profile validation: PASS")
quit() quit()
@ -70,6 +88,131 @@ func _validate_main_profile(main: Node) -> void:
assert(not pond.material_override is ShaderMaterial) assert(not pond.material_override is ShaderMaterial)
assert(not ocean.material_override is ShaderMaterial) assert(not ocean.material_override is ShaderMaterial)
assert(not ocean.is_processing()) assert(not ocean.is_processing())
_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 title_background := main.get_node("%TitleBackground") as ColorRect
var title_material := title_background.material as ShaderMaterial
assert(title_material != null)
assert(not bool(title_material.get_shader_parameter(
"animation_enabled"
)))
for backdrop_name: StringName in [
&"PlayerMenuBackdrop",
&"ShopBackdrop",
]:
var backdrop := main.get_node("%%%s" % backdrop_name) as ColorRect
var backdrop_material := backdrop.material as ShaderMaterial
assert(backdrop_material != null)
assert(not bool(backdrop_material.get_shader_parameter(
"animation_enabled"
)))
assert(
backdrop_material.get_shader_parameter(
"scroll_velocity_pixels"
) == Vector2.ZERO
)
func _validate_normal_profile(main: Node) -> void:
var profile: RuntimePerformanceProfile = main.get("_performance_profile")
assert(profile != null and not profile.is_light())
assert(is_equal_approx(root.scaling_3d_scale, 1.0))
var pond := main.get_node(
"TestWorld/Regions/StarterIslandRegion/WaterBodies/Pond/VisualWater"
) as MeshInstance3D
var ocean := main.get_node(
"TestWorld/Regions/StarterIslandRegion/WaterBodies/Ocean/VisualWater"
) as MeshInstance3D
assert(pond.material_override is ShaderMaterial)
assert(ocean.material_override is ShaderMaterial)
assert(not 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 title_background := main.get_node("%TitleBackground") as ColorRect
var title_material := title_background.material as ShaderMaterial
assert(title_material != null)
assert(bool(title_material.get_shader_parameter("animation_enabled")))
for backdrop_name: StringName in [
&"PlayerMenuBackdrop",
&"ShopBackdrop",
]:
var backdrop := main.get_node("%%%s" % backdrop_name) as ColorRect
var backdrop_material := backdrop.material as ShaderMaterial
assert(backdrop_material != null)
assert(bool(backdrop_material.get_shader_parameter(
"animation_enabled"
)))
assert(
backdrop_material.get_shader_parameter(
"scroll_velocity_pixels"
) != Vector2.ZERO
)
var visuals: WorldTimeVisualController = main.get_node(
"%WorldTimeVisualController"
)
var rain := visuals.get_node("LocalRain") as GPUParticles3D
assert(rain.amount == WorldTimeVisualController.RAIN_PARTICLE_AMOUNT)
assert(rain.fixed_fps == 30)
func _validate_ocean_fishing_coverage(
main: Node,
ocean: MeshInstance3D,
) -> void:
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"
) 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,
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()
elif root_node is AudioStreamPlayer2D:
(root_node as AudioStreamPlayer2D).stop()
elif root_node is AudioStreamPlayer3D:
(root_node as AudioStreamPlayer3D).stop()
for child: Node in root_node.get_children():
_stop_audio_players(child)
func _validate_minimal_weather(main: Node) -> void: func _validate_minimal_weather(main: Node) -> void:
@ -79,6 +222,8 @@ func _validate_minimal_weather(main: Node) -> void:
var rain := visuals.get_node("LocalRain") as GPUParticles3D var rain := visuals.get_node("LocalRain") as GPUParticles3D
assert(rain.amount == WorldTimeVisualController.LIGHT_RAIN_PARTICLE_AMOUNT) assert(rain.amount == WorldTimeVisualController.LIGHT_RAIN_PARTICLE_AMOUNT)
assert(rain.fixed_fps == WorldTimeVisualController.LIGHT_RAIN_FIXED_FPS) assert(rain.fixed_fps == WorldTimeVisualController.LIGHT_RAIN_FIXED_FPS)
assert(rain.amount == 128)
assert(rain.fixed_fps == 12)
var clouds := visuals.get_node("LocalStormClouds") as LocalStormCloudLayer var clouds := visuals.get_node("LocalStormClouds") as LocalStormCloudLayer
assert(clouds.get_patch_count() == 1) assert(clouds.get_patch_count() == 1)
var ceiling := clouds.get_node("CloudCeiling") as MeshInstance3D var ceiling := clouds.get_node("CloudCeiling") as MeshInstance3D

View file

@ -122,7 +122,6 @@ const SHOP_ANIMALESE_VOICE_ID: String = "natural"
const SHOP_ANIMALESE_BASE_PITCH: float = 1.08 const SHOP_ANIMALESE_BASE_PITCH: float = 1.08
const SHOP_SPEECH_CHARACTERS_PER_SECOND: float = 28.0 const SHOP_SPEECH_CHARACTERS_PER_SECOND: float = 28.0
const SHOP_NPC_SPEECH_COOLDOWN_MILLISECONDS: int = 5000 const SHOP_NPC_SPEECH_COOLDOWN_MILLISECONDS: int = 5000
const ART_KIT_ITEM_ID: StringName = &"art_kit"
@onready var _canonical_stage: Control = %CanonicalStage @onready var _canonical_stage: Control = %CanonicalStage
@ -612,20 +611,6 @@ func _on_emote_selected(emote_id: StringName) -> void:
func _on_quick_action_selected(action_id: StringName) -> void: func _on_quick_action_selected(action_id: StringName) -> void:
match action_id: match action_id:
&"stuff":
_player_menu.open_section(PlayerMenuType.Section.COOLER)
&"logbook":
_player_menu.open_section(PlayerMenuType.Section.LOGBOOK)
&"fishnet":
_player_menu.open_section(PlayerMenuType.Section.NET)
&"mail":
_player_menu.open_section(PlayerMenuType.Section.MAIL)
&"profile":
_player_menu.open_section(PlayerMenuType.Section.PROFILE)
&"online":
_player_menu.open_section(PlayerMenuType.Section.PLAYERS)
&"paint":
_select_art_kit_hotbar_slot()
&"chat": &"chat":
_chat_ui.open_chat() _chat_ui.open_chat()
&"freecam": &"freecam":
@ -1116,15 +1101,6 @@ static func get_virtual_mouse_window_bounds(window_size: Vector2) -> Rect2:
return Rect2(cursor_margin, bounds_size) return Rect2(cursor_margin, bounds_size)
func _select_art_kit_hotbar_slot() -> void:
if _hotbar == null:
return
for slot_index: int in range(PlayerHotbarType.SLOT_COUNT):
if _hotbar.get_item_id(slot_index) == ART_KIT_ITEM_ID:
_hotbar.select_slot(slot_index)
return
func set_surface_drawing_hotbar_selected(is_selected: bool) -> void: func set_surface_drawing_hotbar_selected(is_selected: bool) -> void:
_surface_drawing_hotbar_selected = is_selected _surface_drawing_hotbar_selected = is_selected
_refresh_surface_drawing_activation() _refresh_surface_drawing_activation()

View file

@ -4,10 +4,14 @@ uniform sampler2D pattern_texture : source_color, repeat_enable, filter_nearest;
uniform vec2 source_tile_size = vec2(128.0, 128.0); uniform vec2 source_tile_size = vec2(128.0, 128.0);
uniform float display_scale : hint_range(0.5, 2.0, 0.05) = 0.85; uniform float display_scale : hint_range(0.5, 2.0, 0.05) = 0.85;
uniform vec2 scroll_velocity_pixels = vec2(-7.0, -5.0); uniform vec2 scroll_velocity_pixels = vec2(-7.0, -5.0);
uniform bool animation_enabled = true;
void fragment() { void fragment() {
vec2 viewport_pixels = 1.0 / SCREEN_PIXEL_SIZE; vec2 viewport_pixels = 1.0 / SCREEN_PIXEL_SIZE;
vec2 display_tile_size = max(source_tile_size * display_scale, vec2(1.0)); vec2 display_tile_size = max(source_tile_size * display_scale, vec2(1.0));
vec2 moving_pixels = UV * viewport_pixels + TIME * scroll_velocity_pixels; vec2 moving_pixels = UV * viewport_pixels;
if (animation_enabled) {
moving_pixels += TIME * scroll_velocity_pixels;
}
COLOR = texture(pattern_texture, moving_pixels / display_tile_size); COLOR = texture(pattern_texture, moving_pixels / display_tile_size);
} }

View file

@ -16,30 +16,16 @@ const ControllerMappingManagerType = preload(
"res://settings/controller_mapping_manager.gd" "res://settings/controller_mapping_manager.gd"
) )
const ACTIONS: Array[StringName] = [ const ACTIONS: Array[StringName] = [
&"stuff",
&"logbook",
&"fishnet",
&"mail",
&"profile",
&"online",
&"paint",
&"chat", &"chat",
&"freecam", &"freecam",
&"hud", &"hud",
] ]
const LABELS: Array[String] = [ const LABELS: Array[String] = [
"stuff",
"logbook",
"fishnet",
"mail",
"profile",
"online",
"paint",
"chat", "chat",
"freecam", "freecam",
"hide hud", "hide hud",
] ]
const SECTOR_COUNT: int = 10 const SECTOR_COUNT: int = 3
var _buttons: Array[BubbleButton] = [] var _buttons: Array[BubbleButton] = []
var _is_open: bool = false var _is_open: bool = false

View file

@ -10,12 +10,15 @@ uniform float wave_speed : hint_range(0.0, 0.5, 0.01) = 0.10;
uniform float wave_scale : hint_range(0.5, 8.0, 0.1) = 2.4; uniform float wave_scale : hint_range(0.5, 8.0, 0.1) = 2.4;
uniform float wave_strength : hint_range(0.0, 0.05, 0.001) = 0.012; uniform float wave_strength : hint_range(0.0, 0.05, 0.001) = 0.012;
uniform float caustic_strength : hint_range(0.0, 0.08, 0.001) = 0.018; uniform float caustic_strength : hint_range(0.0, 0.08, 0.001) = 0.018;
uniform bool animation_enabled = true;
void fragment() { void fragment() {
vec2 grid_size = max(virtual_pixel_density, vec2(1.0)); vec2 grid_size = max(virtual_pixel_density, vec2(1.0));
vec2 pixel_uv = (floor(UV * grid_size) + vec2(0.5)) / grid_size; vec2 pixel_uv = (floor(UV * grid_size) + vec2(0.5)) / grid_size;
float continuous_depth = pixel_uv.y;
float caustic_level = 0.0;
if (animation_enabled) {
float motion = TIME * wave_speed; float motion = TIME * wave_speed;
float broad_wave = sin( float broad_wave = sin(
pixel_uv.x * wave_scale + motion pixel_uv.x * wave_scale + motion
); );
@ -28,12 +31,31 @@ void fragment() {
* wave_strength * wave_strength
* movement_falloff * movement_falloff
); );
float continuous_depth = clamp( continuous_depth = clamp(
pixel_uv.y pixel_uv.y
+ depth_displacement, + depth_displacement,
0.0, 0.0,
1.0 1.0
); );
float caustic_wave = clamp(
(
sin(
pixel_uv.x * 4.0
+ pixel_uv.y * 1.2
+ motion * 0.7
)
* sin(
pixel_uv.x * 1.7
- pixel_uv.y * 2.2
- motion * 0.55
)
- 0.25
) / 0.75,
0.0,
1.0
);
caustic_level = floor(caustic_wave * 3.0) / 3.0;
}
float depth_steps = max(2.0, floor(color_step_count)); float depth_steps = max(2.0, floor(color_step_count));
float band_index = floor( float band_index = floor(
continuous_depth * (depth_steps - 1.0) + 0.5 continuous_depth * (depth_steps - 1.0) + 0.5
@ -60,26 +82,6 @@ void fragment() {
); );
} }
float caustic_wave = clamp(
(
sin(
pixel_uv.x * 4.0
+ pixel_uv.y * 1.2
+ motion * 0.7
)
* sin(
pixel_uv.x * 1.7
- pixel_uv.y * 2.2
- motion * 0.55
)
- 0.25
) / 0.75,
0.0,
1.0
);
float caustic_level = floor(
caustic_wave * 3.0
) / 3.0;
float upper_water_mask = 1.0 - smoothstep( float upper_water_mask = 1.0 - smoothstep(
0.18, 0.18,
0.34, 0.34,

View file

@ -47,10 +47,13 @@ var fishing_shop_path: NodePath = (
@export_range(0.0, 0.4, 0.005) var foliage_wind_strength: float = 0.36 @export_range(0.0, 0.4, 0.005) var foliage_wind_strength: float = 0.36
@export_range(0.0, 4.0, 0.05) var foliage_wind_speed: float = 0.9 @export_range(0.0, 4.0, 0.05) var foliage_wind_speed: float = 0.9
var _foliage_wind_enabled: bool = false
func _ready() -> void: func _ready() -> void:
if rebuild_terrain_collision_on_ready or not has_terrain_collision(): if rebuild_terrain_collision_on_ready or not has_terrain_collision():
_build_terrain_collision() _build_terrain_collision()
_apply_foliage_wind() _set_foliage_wind_enabled(true)
if Engine.is_editor_hint(): if Engine.is_editor_hint():
update_configuration_warnings() update_configuration_warnings()
@ -69,6 +72,7 @@ func get_saltwater_shoreline_mesh() -> MeshInstance3D:
func set_light_performance_profile(enabled: bool) -> void: func set_light_performance_profile(enabled: bool) -> void:
_set_foliage_wind_enabled(not enabled)
var pond := get_node_or_null( var pond := get_node_or_null(
^"WaterBodies/Pond/VisualWater" ^"WaterBodies/Pond/VisualWater"
) as MeshInstance3D ) as MeshInstance3D
@ -93,9 +97,10 @@ func set_light_performance_profile(enabled: bool) -> void:
if enabled if enabled
else NORMAL_SALT_WATER_MATERIAL else NORMAL_SALT_WATER_MATERIAL
) )
var surface_motion := ocean as WaterSurfaceMotion
if surface_motion != null:
surface_motion.set_motion_enabled(not enabled) func is_foliage_wind_enabled() -> bool:
return _foliage_wind_enabled
func _create_light_water_material( func _create_light_water_material(
@ -254,14 +259,33 @@ func _collect_terrain_mesh_instances(root: Node) -> Array[MeshInstance3D]:
return mesh_instances return mesh_instances
func _apply_foliage_wind() -> void: func _set_foliage_wind_enabled(enabled: bool) -> void:
_foliage_wind_enabled = enabled
var terrain_root: Node = get_node_or_null(terrain_visual_root_path) var terrain_root: Node = get_node_or_null(terrain_visual_root_path)
if terrain_root == null: if terrain_root == null:
return return
for mesh_instance: MeshInstance3D in _collect_terrain_mesh_instances( for mesh_instance: MeshInstance3D in _collect_terrain_mesh_instances(
terrain_root terrain_root
): ):
if enabled:
_apply_foliage_wind_to_mesh(mesh_instance) _apply_foliage_wind_to_mesh(mesh_instance)
else:
_remove_foliage_wind_from_mesh(mesh_instance)
func _remove_foliage_wind_from_mesh(mesh_instance: MeshInstance3D) -> void:
if mesh_instance.mesh == null:
return
for surface_index: int in mesh_instance.mesh.get_surface_count():
var override_material: Material = (
mesh_instance.get_surface_override_material(surface_index)
)
var shader_material := override_material as ShaderMaterial
if (
shader_material != null
and shader_material.shader == FOLIAGE_WIND_SHADER
):
mesh_instance.set_surface_override_material(surface_index, null)
func _apply_foliage_wind_to_mesh(mesh_instance: MeshInstance3D) -> void: func _apply_foliage_wind_to_mesh(mesh_instance: MeshInstance3D) -> void:

View file

@ -12,8 +12,8 @@ const RAIN_VISIBILITY_AABB := AABB(
Vector3(27.0, 12.0, 27.0), Vector3(27.0, 12.0, 27.0),
) )
const RAIN_PARTICLE_AMOUNT: int = 2240 const RAIN_PARTICLE_AMOUNT: int = 2240
const LIGHT_RAIN_PARTICLE_AMOUNT: int = 256 const LIGHT_RAIN_PARTICLE_AMOUNT: int = 128
const LIGHT_RAIN_FIXED_FPS: int = 15 const LIGHT_RAIN_FIXED_FPS: int = 12
const RAIN_VELOCITY_MIN: float = 16.0 const RAIN_VELOCITY_MIN: float = 16.0
const RAIN_VELOCITY_MAX: float = 20.0 const RAIN_VELOCITY_MAX: float = 20.0
const RAIN_DROP_SIZE := Vector3(0.014, 0.34, 0.014) const RAIN_DROP_SIZE := Vector3(0.014, 0.34, 0.014)