Compare commits

...

26 commits

Author SHA1 Message Date
014c064fae Prepare private v0.20.2-alpha test build 2026-08-31 06:06:09 -04:00
3be79fa3fc Normalize cursor handoff across gameplay menus 2026-08-31 05:48:47 -04:00
6578d3e870 Add transactional RV decor synchronization 2026-08-31 05:48:36 -04:00
ee992d9f0c Restore public discovery default after patron build 2026-08-31 00:52:31 -04:00
9b96a3bf53 Replace starter RV placeholder with donated motorcoach 2026-08-31 00:31:02 -04:00
826f62c352 Prepare private v0.20.1-alpha patron release 2026-08-30 23:53:07 -04:00
b48cb73f3a Harden multiplayer validation for portable homes 2026-08-30 23:52:25 -04:00
d2dce7f900 Stabilize runtime validation under dynamic UI load 2026-08-30 22:32:27 -04:00
983e4d0ec3 Update validation for portable home protocol 2026-08-30 22:00:04 -04:00
fe099f47dd Add portable RV homes and decor prototype 2026-08-30 21:37:26 -04:00
81decf2b71 Add logbook catalog index filters 2026-08-29 20:19:47 -04:00
4575830857 Improve nearby water recovery and camera prompts
Recover players on generated worlds to the nearest safe grass or sand surface. Use the active gameplay camera when positioning shop and storage prompts.
2026-08-29 17:43:11 -04:00
ae802493a0 Fix clam spurt origin artifact (#118) 2026-08-29 17:41:10 -04:00
f910e6bdbb Update Forgejo links for Straywild rename 2026-08-26 00:05:51 -04:00
ed2f3f7abc chore: prepare v0.19.0-alpha release 2026-08-25 22:22:18 -04:00
2b06b79bbf Repair restored hand-net texture imports 2026-08-25 22:18:30 -04:00
98a27e2f53 Preserve data across the Straywild transition 2026-08-25 22:18:20 -04:00
737d1cf2e0 Restore authored hand net 2026-08-25 17:14:22 -04:00
7ae20789a9 Rebrand project and refresh title presentation 2026-08-25 17:14:12 -04:00
17fef2c5d4 chore: prepare v0.18.1-alpha release 2026-08-25 02:13:31 -04:00
aaa5803e6b repair generated terrain seams 2026-08-25 02:05:52 -04:00
0cc3ae9709 add bulk staging to shop sales 2026-08-25 02:05:52 -04:00
046f5e0fdb fix clam surface spawn validation 2026-08-25 02:05:52 -04:00
135fd9110f stabilize movement reconciliation states 2026-08-25 00:12:11 -04:00
1ab2a291d2 fix terrain collision and water recovery 2026-08-25 00:12:01 -04:00
da7f072365 fix logbook scrolling focus 2026-08-25 00:11:47 -04:00
261 changed files with 11597 additions and 1960 deletions

View file

@ -1,15 +1,15 @@
# NETfishing Asset License 1.1 # straywild Asset License 1.1
Copyright © 2026 Woofmeow. All rights reserved except for the permissions Copyright © 2026 Woofmeow. All rights reserved except for the permissions
expressly granted below. expressly granted below.
## 1. Scope ## 1. Scope
This license applies to original NETfishing artwork, graphics, textures, This license applies to original straywild artwork, graphics, textures,
models, animations, music, sound recordings, narrative content, visual models, animations, music, sound recordings, narrative content, visual
designs, characters, and other creative assets owned by the NETfishing owners designs, characters, and other creative assets owned by the straywild owners
or licensed to them with authority to sublicense, publishing as Woofmeow (the or licensed to them with authority to sublicense, publishing as Woofmeow (the
**“NETfishing Assets”**). **“straywild Assets”**).
It does not apply to: It does not apply to:
@ -17,77 +17,77 @@ It does not apply to:
- third-party material identified in the source repository's - third-party material identified in the source repository's
`docs/ATTRIBUTION.md` or the packaged `ATTRIBUTION.md` notice; `docs/ATTRIBUTION.md` or the packaged `ATTRIBUTION.md` notice;
- material accompanied by a different license; or - material accompanied by a different license; or
- the NETfishing and Woofmeow names and logos, which are addressed in - the straywild and Woofmeow names and logos, which are addressed in
`TRADEMARKS.md`. `TRADEMARKS.md`.
Repository location alone does not change third-party ownership or licensing. Repository location alone does not change third-party ownership or licensing.
## 2. Definitions ## 2. Definitions
**Official NETfishing Software** means: **Official straywild Software** means:
- an unmodified binary release published by the NETfishing owners through an - an unmodified binary release published by the straywild owners through an
official NETfishing distribution channel; or official straywild distribution channel; or
- a build made from an unmodified official NETfishing source release or tagged - a build made from an unmodified official straywild source release or tagged
revision. revision.
**Mod** means a non-standalone add-on created to operate with Official **Mod** means a non-standalone add-on created to operate with Official
NETfishing Software. A Mod may add, replace, or alter content loaded by the straywild Software. A Mod may add, replace, or alter content loaded by the
Official NETfishing Software, but it may not redistribute a replacement copy Official straywild Software, but it may not redistribute a replacement copy
of the game or represent a fork, clone, sequel, or separate game. of the game or represent a fork, clone, sequel, or separate game.
**Contribution Fork** means a source-control fork or clone of an official **Contribution Fork** means a source-control fork or clone of an official
NETfishing source repository that is created and maintained in good faith straywild source repository that is created and maintained in good faith
primarily to prepare, test, review, or submit changes for possible inclusion primarily to prepare, test, review, or submit changes for possible inclusion
in Official NETfishing Software. It does not include a fork presented or in Official straywild Software. It does not include a fork presented or
developed as an independent game, community edition, replacement distribution, developed as an independent game, community edition, replacement distribution,
or ongoing product separate from the official NETfishing project. or ongoing product separate from the official straywild project.
## 3. Permission to use the official game ## 3. Permission to use the official game
You may reproduce NETfishing Assets as technically necessary to install, run, You may reproduce straywild Assets as technically necessary to install, run,
back up, and privately modify a lawfully obtained copy of Official NETfishing back up, and privately modify a lawfully obtained copy of Official straywild
Software. Software.
You may capture, publish, and monetize screenshots, gameplay videos, You may capture, publish, and monetize screenshots, gameplay videos,
livestreams, reviews, tutorials, and similar commentary showing a lawfully livestreams, reviews, tutorials, and similar commentary showing a lawfully
obtained copy of NETfishing. This permission does not authorize redistribution obtained copy of straywild. This permission does not authorize redistribution
of the source asset files or imply endorsement. of the source asset files or imply endorsement.
## 4. Community Mod license ## 4. Community Mod license
Subject to this license, you may create, modify, and distribute NETfishing Subject to this license, you may create, modify, and distribute straywild
Assets solely as part of a Mod for Official NETfishing Software. Assets solely as part of a Mod for Official straywild Software.
This permission is worldwide, nonexclusive, royalty-free, and available for This permission is worldwide, nonexclusive, royalty-free, and available for
as long as you comply with this license. It permits only the copies of as long as you comply with this license. It permits only the copies of
NETfishing Assets reasonably necessary for the Mod to function. It does not straywild Assets reasonably necessary for the Mod to function. It does not
permit distributing the original game, a substantially complete collection of permit distributing the original game, a substantially complete collection of
its assets, or a general-purpose asset pack. its assets, or a general-purpose asset pack.
A distributed Mod must: A distributed Mod must:
1. require a separately obtained copy of Official NETfishing Software; 1. require a separately obtained copy of Official straywild Software;
2. be presented as unofficial community content; 2. be presented as unofficial community content;
3. include the following notice in its documentation or distribution page: 3. include the following notice in its documentation or distribution page:
> Contains or adapts NETfishing assets © 2026 Woofmeow, used under the > Contains or adapts straywild assets © 2026 Woofmeow, used under the
> NETfishing Asset License 1.1. This Mod is unofficial and is not endorsed > straywild Asset License 1.1. This Mod is unofficial and is not endorsed
> by Woofmeow. > by Woofmeow.
4. identify material created by the Mod author separately from NETfishing 4. identify material created by the Mod author separately from straywild
Assets; and Assets; and
5. comply with `TRADEMARKS.md`. 5. comply with `TRADEMARKS.md`.
Mods may be shared without charge and may accept voluntary donations that do Mods may be shared without charge and may accept voluntary donations that do
not purchase access, features, or preferential treatment. Selling a Mod, not purchase access, features, or preferential treatment. Selling a Mod,
charging for access to it, licensing it commercially, or using NETfishing charging for access to it, licensing it commercially, or using straywild
Assets in paid products requires separate written permission from both Assets in paid products requires separate written permission from both
NETfishing owners. straywild owners.
## 5. Contribution Fork license ## 5. Contribution Fork license
Subject to this license, you may reproduce, modify, and distribute NETfishing Subject to this license, you may reproduce, modify, and distribute straywild
Assets within a Contribution Fork. You may also make the nonpublic builds Assets within a Contribution Fork. You may also make the nonpublic builds
reasonably necessary to develop, test, and review its proposed contribution. reasonably necessary to develop, test, and review its proposed contribution.
@ -99,8 +99,8 @@ fork are permitted distributions.
A Contribution Fork must: A Contribution Fork must:
1. remain primarily directed toward preparing changes for possible inclusion 1. remain primarily directed toward preparing changes for possible inclusion
in the official NETfishing project; in the official straywild project;
2. preserve this license and the official NETfishing project's applicable 2. preserve this license and the official straywild project's applicable
attribution, third-party license, and copyright notices; attribution, third-party license, and copyright notices;
3. remain visibly linked to or identified as a fork of the official source 3. remain visibly linked to or identified as a fork of the official source
repository and not be presented as official or endorsed; repository and not be presented as official or endorsed;
@ -109,63 +109,63 @@ A Contribution Fork must:
contribution; contribution;
5. not publish general-audience binaries, releases, packages, or downloads, 5. not publish general-audience binaries, releases, packages, or downloads,
operate a persistent public alternative service, or monetize access; operate a persistent public alternative service, or monetize access;
6. not distribute the NETfishing Assets as a standalone extraction, archive, 6. not distribute the straywild Assets as a standalone extraction, archive,
or general-purpose asset pack; and or general-purpose asset pack; and
7. comply with `TRADEMARKS.md`. 7. comply with `TRADEMARKS.md`.
Temporary access-controlled servers may be operated when reasonably necessary Temporary access-controlled servers may be operated when reasonably necessary
to test a proposed contribution. They may not be promoted or maintained as a to test a proposed contribution. They may not be promoted or maintained as a
public alternative to Official NETfishing Software or its services. public alternative to Official straywild Software or its services.
Submitting work from a Contribution Fork is governed separately by Submitting work from a Contribution Fork is governed separately by
`CONTRIBUTOR-TERMS.md` and does not require the NETfishing owners to accept the `CONTRIBUTOR-TERMS.md` and does not require the straywild owners to accept the
work. Declining or closing a proposed contribution does not retroactively work. Declining or closing a proposed contribution does not retroactively
invalidate good-faith use under this section. A compliant Contribution Fork invalidate good-faith use under this section. A compliant Contribution Fork
may remain publicly accessible as an archival development record, but it may may remain publicly accessible as an archival development record, but it may
not be repurposed for continued development or distribution of a separate not be repurposed for continued development or distribution of a separate
product containing NETfishing Assets. product containing straywild Assets.
## 6. Uses not permitted ## 6. Uses not permitted
Except as expressly permitted by Sections 3, 4, and 5 or by separate written 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 permission from both straywild owners, you may not use or distribute
NETfishing Assets: straywild Assets:
- in a fork, clone, remake, sequel, or standalone game that is not a qualifying - in a fork, clone, remake, sequel, or standalone game that is not a qualifying
Contribution Fork; Contribution Fork;
- with a modified distribution of the GPL-covered NETfishing code that is not - with a modified distribution of the GPL-covered straywild code that is not
Official NETfishing Software, except for nonpublic contribution-testing Official straywild Software, except for nonpublic contribution-testing
builds permitted by Section 5; builds permitted by Section 5;
- in another game, application, asset library, template, or merchandise; - in another game, application, asset library, template, or merchandise;
- as a substantially complete asset extraction or archive; - as a substantially complete asset extraction or archive;
- in a way that implies sponsorship, endorsement, or official status; - in a way that implies sponsorship, endorsement, or official status;
- to register a confusingly similar trademark, domain, account, or product - to register a confusingly similar trademark, domain, account, or product
identity; or identity; or
- under terms that claim ownership of the NETfishing Assets themselves. - under terms that claim ownership of the straywild Assets themselves.
The GPL permits forks and redistribution of the GPL-covered code. Except for a 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 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 permit those forks to carry straywild Assets. A code fork that is not a
Contribution Fork must remove or replace the NETfishing Assets and reserved Contribution Fork must remove or replace the straywild Assets and reserved
branding unless both owners grant separate written permission. branding unless both owners grant separate written permission.
## 7. Ownership of Mods and contribution work ## 7. Ownership of Mods and contribution work
You retain ownership of your original Mod material and original material You retain ownership of your original Mod material and original material
created in a Contribution Fork. The NETfishing owners retain ownership of the created in a Contribution Fork. The straywild owners retain ownership of the
NETfishing Assets and adaptations of those assets to the extent provided by straywild Assets and adaptations of those assets to the extent provided by
applicable law. applicable law.
This license does not require you to assign or submit your original material. 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 It also does not grant you ownership of the underlying straywild Assets. If
you intentionally submit material for inclusion in NETfishing, you intentionally submit material for inclusion in straywild,
`CONTRIBUTOR-TERMS.md` applies separately. `CONTRIBUTOR-TERMS.md` applies separately.
## 8. Termination and cure ## 8. Termination and cure
Your permissions under this license terminate automatically if you violate Your permissions under this license terminate automatically if you violate
its terms. They are reinstated provisionally when you stop the violation and its terms. They are reinstated provisionally when you stop the violation and
permanently if both NETfishing owners do not notify you of the violation permanently if both straywild owners do not notify you of the violation
within sixty days after it has been cured. within sixty days after it has been cured.
Termination does not limit remedies available for activity outside this Termination does not limit remedies available for activity outside this
@ -173,15 +173,15 @@ license.
## 9. No warranty ## 9. No warranty
THE NETFISHING ASSETS ARE PROVIDED “AS IS,” WITHOUT WARRANTY OF ANY KIND, THE straywild ASSETS ARE PROVIDED “AS IS,” WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING WARRANTIES OF MERCHANTABILITY, FITNESS FOR A EXPRESS OR IMPLIED, INCLUDING WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE, TITLE, OR NON-INFRINGEMENT. TO THE MAXIMUM EXTENT PARTICULAR PURPOSE, TITLE, OR NON-INFRINGEMENT. TO THE MAXIMUM EXTENT
PERMITTED BY LAW, THE OWNERS WILL NOT BE LIABLE FOR ANY CLAIM, DAMAGES, OR 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. OTHER LIABILITY ARISING FROM THIS LICENSE OR USE OF THE straywild ASSETS.
## 10. Additional permission ## 10. Additional permission
Uses not granted here require written permission from both NETfishing owners. Uses not granted here require written permission from both straywild owners.
A permission granted to one person or project does not amend this license for A permission granted to one person or project does not amend this license for
others. others.

View file

@ -1,6 +1,6 @@
# Contribution policy # Contribution policy
NETfishing is an owner-directed Woofmeow project and is not accepting straywild is an owner-directed Woofmeow project and is not accepting
unsolicited contributions. Please do not submit pull requests, patches, code, unsolicited contributions. Please do not submit pull requests, patches, code,
artwork, models, audio, writing, documentation, translations, designs, or artwork, models, audio, writing, documentation, translations, designs, or
other implementation work. Pull requests are disabled for the official other implementation work. Pull requests are disabled for the official
@ -17,7 +17,7 @@ work; it is not a standing invitation to contribute.
## Contribution forks ## Contribution forks
The [NETfishing Asset License](ASSET-LICENSE.md) contains limited permissions The [straywild Asset License](ASSET-LICENSE.md) contains limited permissions
for qualifying Contribution Forks. Those permissions do not promise review or for qualifying Contribution Forks. Those permissions do not promise review or
acceptance and do not override the policy above. Keep the hosting platform's acceptance and do not override the policy above. Keep the hosting platform's
fork relationship visible, preserve all license and attribution notices, and fork relationship visible, preserve all license and attribution notices, and
@ -79,4 +79,4 @@ using display strings as persistent identifiers.
New standalone source files should use `SPDX-License-Identifier: New standalone source files should use `SPDX-License-Identifier:
GPL-3.0-or-later` where the file format permits comments. Do not add that GPL-3.0-or-later` where the file format permits comments. Do not add that
identifier to asset files governed by the NETfishing Asset License. identifier to asset files governed by the straywild Asset License.

View file

@ -1,6 +1,6 @@
# NETfishing contributor terms # straywild contributor terms
NETfishing does not accept unsolicited contributions. These terms are retained straywild does not accept unsolicited contributions. These terms are retained
as the governing record for contributions previously accepted after their as the governing record for contributions previously accepted after their
publication and for specific future work that both Owners expressly invite in publication and for specific future work that both Owners expressly invite in
writing before submission. They are not a request or standing invitation for writing before submission. They are not a request or standing invitation for
@ -9,9 +9,9 @@ patches, pull requests, assets, or other material.
These terms do not retroactively change ownership of earlier work or replace a These terms do not retroactively change ownership of earlier work or replace a
separately signed agreement. separately signed agreement.
“Owners” means the two NETfishing co-owners who publish the Project as “Owners” means the two straywild co-owners who publish the Project as
Woofmeow and are publicly credited as Voyager and Endeavour. “Project” means Woofmeow and are publicly credited as Voyager and Endeavour. “Project” means
the official NETfishing game, its servers, tools, documentation, websites, the official straywild game, its servers, tools, documentation, websites,
ports, updates, and promotional materials. ports, updates, and promotional materials.
## 1. Your ownership and authority ## 1. Your ownership and authority
@ -56,8 +56,8 @@ royalty-free, transferable, and sublicensable license to:
- license it to Project users under `ASSET-LICENSE.md` or a later Project asset - license it to Project users under `ASSET-LICENSE.md` or a later Project asset
license jointly approved by the Owners. license jointly approved by the Owners.
This grant is limited to NETfishing, its promotion, and official successor or This grant is limited to straywild, its promotion, and official successor or
derived NETfishing products. It does not transfer your copyright or authorize derived straywild products. It does not transfer your copyright or authorize
the Owners to sell your contribution as an unrelated standalone asset. the Owners to sell your contribution as an unrelated standalone asset.
## 4. Documentation accompanying code ## 4. Documentation accompanying code
@ -78,7 +78,7 @@ or release.
## 6. No automatic ownership, employment, or payment ## 6. No automatic ownership, employment, or payment
Submitting a contribution does not provide ownership of NETfishing, create an Submitting a contribution does not provide ownership of straywild, create an
employment, agency, partnership, or fiduciary relationship, or create a right employment, agency, partnership, or fiduciary relationship, or create a right
to compensation. Any payment, ownership, or employment arrangement requires a to compensation. Any payment, ownership, or employment arrangement requires a
separate written agreement signed by the appropriate parties. separate written agreement signed by the appropriate parties.

View file

@ -1,6 +1,6 @@
# NETfishing # straywild
NETfishing is a multiplayer-first fishing game built with Godot 4.7. The straywild is a multiplayer-first fishing game built with Godot 4.7. The
current pre-alpha includes host-authoritative fishing and economy flows, current pre-alpha includes host-authoritative fishing and economy flows,
freshwater and saltwater habitats, persistent player data, social features, freshwater and saltwater habitats, persistent player data, social features,
customization, jobs, and a controller-aware interface. customization, jobs, and a controller-aware interface.
@ -8,39 +8,39 @@ customization, jobs, and a controller-aware interface.
The project is under active development. Interfaces, balance, content, and The project is under active development. Interfaces, balance, content, and
save migrations may continue to change before a stable release. save migrations may continue to change before a stable release.
NETfishing is developed and published by **Woofmeow**, the independent team of straywild is developed and published by **Woofmeow**, the independent team of
co-owners publicly credited as Voyager and Endeavour. co-owners publicly credited as Voyager and Endeavour.
## Links ## Links
Patreon: https://www.patreon.com/cw/woofmeowgames Patreon: https://www.patreon.com/cw/woofmeowgames
Steam: https://store.steampowered.com/app/5068950/NETfishing/ Steam: https://store.steampowered.com/app/5068950/straywild/
Itch.io: https://sol6-vi.itch.io/netfishing Itch.io: https://sol6-vi.itch.io/straywild
Discord: https://discord.gg/5gP22447kc Discord: https://discord.gg/5gP22447kc
Matrix: https://matrix.to/#/#netfishing:matrix.makearmy.io Matrix: https://matrix.to/#/#straywild:matrix.makearmy.io
Repo: https://forge.makearmy.io/woofmeow/netfishing Repo: https://forge.makearmy.io/woofmeow/straywild
Releases: https://forge.makearmy.io/woofmeow/netfishing/releases Releases: https://forge.makearmy.io/woofmeow/straywild/releases
## Installing release builds ## Installing release builds
Download and extract the archive for your platform from the official release Download and extract the archive for your platform from the official release
page, then run NETfishing from the extracted platform folder. page, then run straywild from the extracted platform folder.
### macOS ### macOS
Current macOS builds are not signed or notarized. macOS may block the first Current macOS builds are not signed or notarized. macOS may block the first
launch because it cannot verify the developer. Control-click the NETfishing launch because it cannot verify the developer. Control-click the straywild
application, choose **Open**, then confirm **Open**. If that option is not application, choose **Open**, then confirm **Open**. If that option is not
available, attempt to open the application once, then use **System Settings → available, attempt to open the application once, then use **System Settings →
Privacy & Security → Open Anyway**. Privacy & Security → Open Anyway**.
Only bypass this warning for a NETfishing build downloaded from an official Only bypass this warning for a straywild build downloaded from an official
project release. project release.
## Requirements ## Requirements
@ -110,10 +110,10 @@ world and UI authoring, and testing are consolidated in
- Project-owned software code is licensed under - Project-owned software code is licensed under
[GPL-3.0-or-later](LICENSE). [GPL-3.0-or-later](LICENSE).
- Project-owned artwork, models, music, and other creative assets are covered - Project-owned artwork, models, music, and other creative assets are covered
by the [NETfishing Asset License](ASSET-LICENSE.md). It permits community by the [straywild Asset License](ASSET-LICENSE.md). It permits community
Mods for official NETfishing software and narrowly scoped contribution forks, Mods for official straywild software and narrowly scoped contribution forks,
but not independent forks, clones, or unrelated products. but not independent forks, clones, or unrelated products.
- The NETfishing and Woofmeow names and branding are reserved as described in - The straywild and Woofmeow names and branding are reserved as described in
[TRADEMARKS.md](TRADEMARKS.md). [TRADEMARKS.md](TRADEMARKS.md).
- Third-party materials retain their own terms; see the consolidated - Third-party materials retain their own terms; see the consolidated
[credits, notices, and provenance record](docs/ATTRIBUTION.md). [credits, notices, and provenance record](docs/ATTRIBUTION.md).

View file

@ -1,7 +1,7 @@
# NETfishing and Woofmeow branding # straywild and Woofmeow branding
The NETfishing and Woofmeow names, logos, icons, trade dress, and other source The straywild and Woofmeow names, logos, icons, trade dress, and other source
identifiers are reserved by the NETfishing owners, publishing as Woofmeow. identifiers are reserved by the straywild owners, publishing as Woofmeow.
Their presence in a GPL-licensed repository does not grant trademark rights or Their presence in a GPL-licensed repository does not grant trademark rights or
imply permission to represent another product as official. imply permission to represent another product as official.
@ -10,30 +10,30 @@ exist through use and other law.
## Permitted references ## Permitted references
You may use the names NETfishing and Woofmeow truthfully to: You may use the names straywild and Woofmeow truthfully to:
- refer to the official game or development team; - refer to the official game or development team;
- state that software interoperates with or is a Mod for NETfishing; - state that software interoperates with or is a Mod for straywild;
- link to an official repository or distribution page; and - link to an official repository or distribution page; and
- discuss, review, criticize, or report on the Project. - discuss, review, criticize, or report on the Project.
Use only the amount of branding reasonably necessary to identify the Project. Use only the amount of branding reasonably necessary to identify the Project.
Do not imply sponsorship, certification, endorsement, or official status. Do not imply sponsorship, certification, endorsement, or official status.
An unofficial Mod may use wording such as “Example Mod for NETfishing” if its An unofficial Mod may use wording such as “Example Mod for straywild” if its
name and presentation remain clearly distinct and it displays an unofficial name and presentation remain clearly distinct and it displays an unofficial
notice. notice.
## Contribution forks ## Contribution forks
A qualifying Contribution Fork under `ASSET-LICENSE.md` may retain the A qualifying Contribution Fork under `ASSET-LICENSE.md` may retain the
NETfishing and Woofmeow names, logos, icons, and trade dress already present in straywild and Woofmeow names, logos, icons, and trade dress already present in
the official source repository solely as necessary to reproduce, modify, and the official source repository solely as necessary to reproduce, modify, and
test the Project while preparing a proposed contribution. The source-control test the Project while preparing a proposed contribution. The source-control
host's visible fork relationship or an equivalent repository notice must make host's visible fork relationship or an equivalent repository notice must make
its unofficial contribution purpose clear. its unofficial contribution purpose clear.
This permission does not allow a Contribution Fork to use NETfishing or This permission does not allow a Contribution Fork to use straywild or
Woofmeow branding to advertise a public build, release, service, community Woofmeow branding to advertise a public build, release, service, community
edition, or independent product. It does not imply acceptance, sponsorship, edition, or independent product. It does not imply acceptance, sponsorship,
endorsement, or official status. endorsement, or official status.
@ -44,7 +44,7 @@ The GNU GPL permits forks and redistribution of covered code. A fork or
modified distribution that is not a qualifying Contribution Fork must: modified distribution that is not a qualifying Contribution Fork must:
- use a distinct product name; - use a distinct product name;
- remove NETfishing and Woofmeow logos, icons, and official trade dress; - remove straywild and Woofmeow logos, icons, and official trade dress;
- avoid confusingly similar names, domains, package identifiers, and account - avoid confusingly similar names, domains, package identifiers, and account
names; and names; and
- clearly identify itself as independent and unofficial. - clearly identify itself as independent and unofficial.
@ -53,4 +53,4 @@ These branding terms do not limit rights granted by the GPL to the software
code or lawful nominative, descriptive, comparative, or fair use. code or lawful nominative, descriptive, comparative, or fair use.
Permission for broader branding use must be obtained in writing from both Permission for broader branding use must be obtained in writing from both
NETfishing owners. straywild owners.

View file

@ -1,6 +1,6 @@
[plugin] [plugin]
name="NETfishing Shoreline Auto-Baker" name="straywild Shoreline Auto-Baker"
description="Rebuilds configured static shoreline ribbons after authored terrain reimports." description="Rebuilds configured static shoreline ribbons after authored terrain reimports."
author="woofmeow" author="woofmeow"
version="1.0.0" version="1.0.0"

View file

@ -4,12 +4,12 @@ importer="scene"
importer_version=1 importer_version=1
type="PackedScene" type="PackedScene"
uid="uid://w7n4gjqq1juc" uid="uid://w7n4gjqq1juc"
path="res://.godot/imported/netfishing_base_character.glb-0167c087f16181d405afb962dc877d2c.scn" path="res://.godot/imported/straywild_base_character.glb-65021653e51a906d3b4a3494f170317f.scn"
[deps] [deps]
source_file="res://art/exported/characters/base/netfishing_base_character.glb" source_file="res://art/exported/characters/base/straywild_base_character.glb"
dest_files=["res://.godot/imported/netfishing_base_character.glb-0167c087f16181d405afb962dc877d2c.scn"] dest_files=["res://.godot/imported/straywild_base_character.glb-65021653e51a906d3b4a3494f170317f.scn"]
[params] [params]

View file

@ -3,8 +3,8 @@
importer="texture" importer="texture"
type="CompressedTexture2D" type="CompressedTexture2D"
uid="uid://dyjwgkgw0syo8" uid="uid://dyjwgkgw0syo8"
path.s3tc="res://.godot/imported/netfishing_base_character_eyes_simple_shine.png-140bfa3aa0b6f662e8ff9e5721213a61.s3tc.ctex" path.s3tc="res://.godot/imported/straywild_base_character_eyes_simple_shine.png-200a946ddace2078bbe44addb4e4524c.s3tc.ctex"
path.etc2="res://.godot/imported/netfishing_base_character_eyes_simple_shine.png-140bfa3aa0b6f662e8ff9e5721213a61.etc2.ctex" path.etc2="res://.godot/imported/straywild_base_character_eyes_simple_shine.png-200a946ddace2078bbe44addb4e4524c.etc2.ctex"
metadata={ metadata={
"imported_formats": ["s3tc_bptc", "etc2_astc"], "imported_formats": ["s3tc_bptc", "etc2_astc"],
"vram_texture": true "vram_texture": true
@ -15,8 +15,8 @@ generator_parameters={
[deps] [deps]
source_file="res://art/exported/characters/base/netfishing_base_character_eyes_simple_shine.png" source_file="res://art/exported/characters/base/straywild_base_character_eyes_simple_shine.png"
dest_files=["res://.godot/imported/netfishing_base_character_eyes_simple_shine.png-140bfa3aa0b6f662e8ff9e5721213a61.s3tc.ctex", "res://.godot/imported/netfishing_base_character_eyes_simple_shine.png-140bfa3aa0b6f662e8ff9e5721213a61.etc2.ctex"] dest_files=["res://.godot/imported/straywild_base_character_eyes_simple_shine.png-200a946ddace2078bbe44addb4e4524c.s3tc.ctex", "res://.godot/imported/straywild_base_character_eyes_simple_shine.png-200a946ddace2078bbe44addb4e4524c.etc2.ctex"]
[params] [params]

View file

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Before After
Before After

View file

@ -3,8 +3,8 @@
importer="texture" importer="texture"
type="CompressedTexture2D" type="CompressedTexture2D"
uid="uid://dt5gitdtjdvcd" uid="uid://dt5gitdtjdvcd"
path.s3tc="res://.godot/imported/netfishing_base_character_mouth_three.png-90b4a98403d2a291a56a8ed42aaa8473.s3tc.ctex" path.s3tc="res://.godot/imported/straywild_base_character_mouth_three.png-5bcf329b31a11eff496760a63e62d0e2.s3tc.ctex"
path.etc2="res://.godot/imported/netfishing_base_character_mouth_three.png-90b4a98403d2a291a56a8ed42aaa8473.etc2.ctex" path.etc2="res://.godot/imported/straywild_base_character_mouth_three.png-5bcf329b31a11eff496760a63e62d0e2.etc2.ctex"
metadata={ metadata={
"imported_formats": ["s3tc_bptc", "etc2_astc"], "imported_formats": ["s3tc_bptc", "etc2_astc"],
"vram_texture": true "vram_texture": true
@ -15,8 +15,8 @@ generator_parameters={
[deps] [deps]
source_file="res://art/exported/characters/base/netfishing_base_character_mouth_three.png" source_file="res://art/exported/characters/base/straywild_base_character_mouth_three.png"
dest_files=["res://.godot/imported/netfishing_base_character_mouth_three.png-90b4a98403d2a291a56a8ed42aaa8473.s3tc.ctex", "res://.godot/imported/netfishing_base_character_mouth_three.png-90b4a98403d2a291a56a8ed42aaa8473.etc2.ctex"] dest_files=["res://.godot/imported/straywild_base_character_mouth_three.png-5bcf329b31a11eff496760a63e62d0e2.s3tc.ctex", "res://.godot/imported/straywild_base_character_mouth_three.png-5bcf329b31a11eff496760a63e62d0e2.etc2.ctex"]
[params] [params]

View file

@ -3,8 +3,8 @@
importer="texture" importer="texture"
type="CompressedTexture2D" type="CompressedTexture2D"
uid="uid://bmho1ryst3smt" uid="uid://bmho1ryst3smt"
path.s3tc="res://.godot/imported/netfishing_base_character_nose_dog_round.png-0008adff5271dd0403e610ef13dd52cf.s3tc.ctex" path.s3tc="res://.godot/imported/straywild_base_character_nose_dog_round.png-e6909a2c139cf4a2748c2b79d1e12d92.s3tc.ctex"
path.etc2="res://.godot/imported/netfishing_base_character_nose_dog_round.png-0008adff5271dd0403e610ef13dd52cf.etc2.ctex" path.etc2="res://.godot/imported/straywild_base_character_nose_dog_round.png-e6909a2c139cf4a2748c2b79d1e12d92.etc2.ctex"
metadata={ metadata={
"imported_formats": ["s3tc_bptc", "etc2_astc"], "imported_formats": ["s3tc_bptc", "etc2_astc"],
"vram_texture": true "vram_texture": true
@ -15,8 +15,8 @@ generator_parameters={
[deps] [deps]
source_file="res://art/exported/characters/base/netfishing_base_character_nose_dog_round.png" source_file="res://art/exported/characters/base/straywild_base_character_nose_dog_round.png"
dest_files=["res://.godot/imported/netfishing_base_character_nose_dog_round.png-0008adff5271dd0403e610ef13dd52cf.s3tc.ctex", "res://.godot/imported/netfishing_base_character_nose_dog_round.png-0008adff5271dd0403e610ef13dd52cf.etc2.ctex"] dest_files=["res://.godot/imported/straywild_base_character_nose_dog_round.png-e6909a2c139cf4a2748c2b79d1e12d92.s3tc.ctex", "res://.godot/imported/straywild_base_character_nose_dog_round.png-e6909a2c139cf4a2748c2b79d1e12d92.etc2.ctex"]
[params] [params]

Binary file not shown.

View file

@ -0,0 +1,45 @@
[remap]
importer="scene"
importer_version=1
type="PackedScene"
uid="uid://dsa6yss1sxfcy"
path="res://.godot/imported/net.glb-cbbc53905aec6a50c4a3322ca7a93b1e.scn"
[deps]
source_file="res://art/exported/items/equipment/net.glb"
dest_files=["res://.godot/imported/net.glb-cbbc53905aec6a50c4a3322ca7a93b1e.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: 276 KiB

View file

@ -0,0 +1,45 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://binoqhg45vgbs"
path.s3tc="res://.godot/imported/net_Material.png-f4c4b0400561bc2751fe719b6b1a1e1a.s3tc.ctex"
path.etc2="res://.godot/imported/net_Material.png-f4c4b0400561bc2751fe719b6b1a1e1a.etc2.ctex"
metadata={
"imported_formats": ["s3tc_bptc", "etc2_astc"],
"vram_texture": true
}
generator_parameters={
"md5": "ef2c2ae657c9460c90eb808ba5217f39"
}
[deps]
source_file="res://art/exported/items/equipment/net_Material.png"
dest_files=["res://.godot/imported/net_Material.png-f4c4b0400561bc2751fe719b6b1a1e1a.s3tc.ctex", "res://.godot/imported/net_Material.png-f4c4b0400561bc2751fe719b6b1a1e1a.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: 42 KiB

View file

@ -0,0 +1,45 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cdsabucjy5rpk"
path.s3tc="res://.godot/imported/net_Plane Base Color.png-3875ae3dd186ddf319c55aabb13e663f.s3tc.ctex"
path.etc2="res://.godot/imported/net_Plane Base Color.png-3875ae3dd186ddf319c55aabb13e663f.etc2.ctex"
metadata={
"imported_formats": ["s3tc_bptc", "etc2_astc"],
"vram_texture": true
}
generator_parameters={
"md5": "a6001c381d4c8e16bd2d64ea74ccff82"
}
[deps]
source_file="res://art/exported/items/equipment/net_Plane Base Color.png"
dest_files=["res://.godot/imported/net_Plane Base Color.png-3875ae3dd186ddf319c55aabb13e663f.s3tc.ctex", "res://.godot/imported/net_Plane Base Color.png-3875ae3dd186ddf319c55aabb13e663f.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.

Before

Width:  |  Height:  |  Size: 362 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 362 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

View file

@ -3,15 +3,15 @@
importer="texture" importer="texture"
type="CompressedTexture2D" type="CompressedTexture2D"
uid="uid://bkh5hw8wwu8pc" uid="uid://bkh5hw8wwu8pc"
path="res://.godot/imported/netfishing_1024.png-7c53a099ed2743b4a623c258641b1d29.ctex" path="res://.godot/imported/straywild_1024.png-b9b5c159c9fcecc21b04324370d4522f.ctex"
metadata={ metadata={
"vram_texture": false "vram_texture": false
} }
[deps] [deps]
source_file="res://art/exported/system_icons/netfishing_1024.png" source_file="res://art/exported/system_icons/straywild_1024.png"
dest_files=["res://.godot/imported/netfishing_1024.png-7c53a099ed2743b4a623c258641b1d29.ctex"] dest_files=["res://.godot/imported/straywild_1024.png-b9b5c159c9fcecc21b04324370d4522f.ctex"]
[params] [params]

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

View file

@ -3,15 +3,15 @@
importer="texture" importer="texture"
type="CompressedTexture2D" type="CompressedTexture2D"
uid="uid://cty16m8ydla06" uid="uid://cty16m8ydla06"
path="res://.godot/imported/netfishing_256.png-d9dfc9c7871ac38bc73d09d00df679be.ctex" path="res://.godot/imported/straywild_256.png-87b7e62e710eae4d3e3ed24a8454ca6d.ctex"
metadata={ metadata={
"vram_texture": false "vram_texture": false
} }
[deps] [deps]
source_file="res://art/exported/system_icons/netfishing_256.png" source_file="res://art/exported/system_icons/straywild_256.png"
dest_files=["res://.godot/imported/netfishing_256.png-d9dfc9c7871ac38bc73d09d00df679be.ctex"] dest_files=["res://.godot/imported/straywild_256.png-87b7e62e710eae4d3e3ed24a8454ca6d.ctex"]
[params] [params]

View file

@ -4,12 +4,12 @@ importer="scene"
importer_version=1 importer_version=1
type="PackedScene" type="PackedScene"
uid="uid://cb4bcakhoprm8" uid="uid://cb4bcakhoprm8"
path="res://.godot/imported/netfishing_base_character.blend-65bf3bc5b77cc1a37fa3f81ebd808d37.scn" path="res://.godot/imported/straywild_base_character.blend-02abbfdb9b1d393f1bd3cfface615e27.scn"
[deps] [deps]
source_file="res://art/source/characters/base/netfishing_base_character.blend" source_file="res://art/source/characters/base/straywild_base_character.blend"
dest_files=["res://.godot/imported/netfishing_base_character.blend-65bf3bc5b77cc1a37fa3f81ebd808d37.scn"] dest_files=["res://.godot/imported/straywild_base_character.blend-02abbfdb9b1d393f1bd3cfface615e27.scn"]
[params] [params]

View file

@ -4,12 +4,12 @@ importer="scene"
importer_version=1 importer_version=1
type="PackedScene" type="PackedScene"
uid="uid://bfhnup14638r7" uid="uid://bfhnup14638r7"
path="res://.godot/imported/netfishing_base_character.glb-cbdc33eaa4fcd1d2d2a8f4a463b228d6.scn" path="res://.godot/imported/straywild_base_character.glb-c9650c715a1b5b9ae0d8541191f7d7a9.scn"
[deps] [deps]
source_file="res://art/source/characters/base/netfishing_base_character.glb" source_file="res://art/source/characters/base/straywild_base_character.glb"
dest_files=["res://.godot/imported/netfishing_base_character.glb-cbdc33eaa4fcd1d2d2a8f4a463b228d6.scn"] dest_files=["res://.godot/imported/straywild_base_character.glb-c9650c715a1b5b9ae0d8541191f7d7a9.scn"]
[params] [params]

View file

@ -4,12 +4,12 @@ importer="scene"
importer_version=1 importer_version=1
type="PackedScene" type="PackedScene"
uid="uid://d38y8blk2w5c5" uid="uid://d38y8blk2w5c5"
path="res://.godot/imported/netfishing_character_source.blend-1d18c95606937d4c8bcb87e334e02ee5.scn" path="res://.godot/imported/straywild_character_source.blend-0e7b8a87008f88e309ad73a63561ef5b.scn"
[deps] [deps]
source_file="res://art/source/characters/base/netfishing_character_source.blend" source_file="res://art/source/characters/base/straywild_character_source.blend"
dest_files=["res://.godot/imported/netfishing_character_source.blend-1d18c95606937d4c8bcb87e334e02ee5.scn"] dest_files=["res://.godot/imported/straywild_character_source.blend-0e7b8a87008f88e309ad73a63561ef5b.scn"]
[params] [params]

View file

@ -4,12 +4,12 @@ importer="scene"
importer_version=1 importer_version=1
type="PackedScene" type="PackedScene"
uid="uid://3uq4cxiomttt" uid="uid://3uq4cxiomttt"
path="res://.godot/imported/netfishing_character_source.glb-9c98ee44b142c5b3d06f0838b7d2c9c4.scn" path="res://.godot/imported/straywild_character_source.glb-3a8ccc55dbe60da1b70b62110b206353.scn"
[deps] [deps]
source_file="res://art/source/characters/base/netfishing_character_source.glb" source_file="res://art/source/characters/base/straywild_character_source.glb"
dest_files=["res://.godot/imported/netfishing_character_source.glb-9c98ee44b142c5b3d06f0838b7d2c9c4.scn"] dest_files=["res://.godot/imported/straywild_character_source.glb-3a8ccc55dbe60da1b70b62110b206353.scn"]
[params] [params]

View file

@ -3,8 +3,8 @@
importer="texture" importer="texture"
type="CompressedTexture2D" type="CompressedTexture2D"
uid="uid://bdomsntfeovnc" uid="uid://bdomsntfeovnc"
path.s3tc="res://.godot/imported/netfishing_character_source_eyes1_eyebrows1.png-a0b9f287e407afd9688e141dc5f2b801.s3tc.ctex" path.s3tc="res://.godot/imported/straywild_character_source_eyes1_eyebrows1.png-3c638377e631a94b998336d0ca1d8b8e.s3tc.ctex"
path.etc2="res://.godot/imported/netfishing_character_source_eyes1_eyebrows1.png-a0b9f287e407afd9688e141dc5f2b801.etc2.ctex" path.etc2="res://.godot/imported/straywild_character_source_eyes1_eyebrows1.png-3c638377e631a94b998336d0ca1d8b8e.etc2.ctex"
metadata={ metadata={
"imported_formats": ["s3tc_bptc", "etc2_astc"], "imported_formats": ["s3tc_bptc", "etc2_astc"],
"vram_texture": true "vram_texture": true
@ -15,8 +15,8 @@ generator_parameters={
[deps] [deps]
source_file="res://art/source/characters/base/netfishing_character_source_eyes1_eyebrows1.png" source_file="res://art/source/characters/base/straywild_character_source_eyes1_eyebrows1.png"
dest_files=["res://.godot/imported/netfishing_character_source_eyes1_eyebrows1.png-a0b9f287e407afd9688e141dc5f2b801.s3tc.ctex", "res://.godot/imported/netfishing_character_source_eyes1_eyebrows1.png-a0b9f287e407afd9688e141dc5f2b801.etc2.ctex"] dest_files=["res://.godot/imported/straywild_character_source_eyes1_eyebrows1.png-3c638377e631a94b998336d0ca1d8b8e.s3tc.ctex", "res://.godot/imported/straywild_character_source_eyes1_eyebrows1.png-3c638377e631a94b998336d0ca1d8b8e.etc2.ctex"]
[params] [params]

View file

@ -3,8 +3,8 @@
importer="texture" importer="texture"
type="CompressedTexture2D" type="CompressedTexture2D"
uid="uid://du4u6meapay05" uid="uid://du4u6meapay05"
path.s3tc="res://.godot/imported/netfishing_character_source_eyes_simple_shine.png-965743f3716320bbc385096956ffef2d.s3tc.ctex" path.s3tc="res://.godot/imported/straywild_character_source_eyes_simple_shine.png-97500ad873e0da3aaf0e9e3c3198e38a.s3tc.ctex"
path.etc2="res://.godot/imported/netfishing_character_source_eyes_simple_shine.png-965743f3716320bbc385096956ffef2d.etc2.ctex" path.etc2="res://.godot/imported/straywild_character_source_eyes_simple_shine.png-97500ad873e0da3aaf0e9e3c3198e38a.etc2.ctex"
metadata={ metadata={
"imported_formats": ["s3tc_bptc", "etc2_astc"], "imported_formats": ["s3tc_bptc", "etc2_astc"],
"vram_texture": true "vram_texture": true
@ -15,8 +15,8 @@ generator_parameters={
[deps] [deps]
source_file="res://art/source/characters/base/netfishing_character_source_eyes_simple_shine.png" source_file="res://art/source/characters/base/straywild_character_source_eyes_simple_shine.png"
dest_files=["res://.godot/imported/netfishing_character_source_eyes_simple_shine.png-965743f3716320bbc385096956ffef2d.s3tc.ctex", "res://.godot/imported/netfishing_character_source_eyes_simple_shine.png-965743f3716320bbc385096956ffef2d.etc2.ctex"] dest_files=["res://.godot/imported/straywild_character_source_eyes_simple_shine.png-97500ad873e0da3aaf0e9e3c3198e38a.s3tc.ctex", "res://.godot/imported/straywild_character_source_eyes_simple_shine.png-97500ad873e0da3aaf0e9e3c3198e38a.etc2.ctex"]
[params] [params]

View file

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Before After
Before After

View file

@ -3,8 +3,8 @@
importer="texture" importer="texture"
type="CompressedTexture2D" type="CompressedTexture2D"
uid="uid://css0k0virgoph" uid="uid://css0k0virgoph"
path.s3tc="res://.godot/imported/netfishing_character_source_mouth1.png-b323b9153acd3f552f35c1d1108567c8.s3tc.ctex" path.s3tc="res://.godot/imported/straywild_character_source_mouth1.png-ff2ed280ca7e84bdd0749ed5b5b16f54.s3tc.ctex"
path.etc2="res://.godot/imported/netfishing_character_source_mouth1.png-b323b9153acd3f552f35c1d1108567c8.etc2.ctex" path.etc2="res://.godot/imported/straywild_character_source_mouth1.png-ff2ed280ca7e84bdd0749ed5b5b16f54.etc2.ctex"
metadata={ metadata={
"imported_formats": ["s3tc_bptc", "etc2_astc"], "imported_formats": ["s3tc_bptc", "etc2_astc"],
"vram_texture": true "vram_texture": true
@ -15,8 +15,8 @@ generator_parameters={
[deps] [deps]
source_file="res://art/source/characters/base/netfishing_character_source_mouth1.png" source_file="res://art/source/characters/base/straywild_character_source_mouth1.png"
dest_files=["res://.godot/imported/netfishing_character_source_mouth1.png-b323b9153acd3f552f35c1d1108567c8.s3tc.ctex", "res://.godot/imported/netfishing_character_source_mouth1.png-b323b9153acd3f552f35c1d1108567c8.etc2.ctex"] dest_files=["res://.godot/imported/straywild_character_source_mouth1.png-ff2ed280ca7e84bdd0749ed5b5b16f54.s3tc.ctex", "res://.godot/imported/straywild_character_source_mouth1.png-ff2ed280ca7e84bdd0749ed5b5b16f54.etc2.ctex"]
[params] [params]

View file

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Before After
Before After

View file

@ -3,8 +3,8 @@
importer="texture" importer="texture"
type="CompressedTexture2D" type="CompressedTexture2D"
uid="uid://di20c1x8vksi1" uid="uid://di20c1x8vksi1"
path.s3tc="res://.godot/imported/netfishing_character_source_mouth_three.png-6bf79d4c9c660d30fb7fc365f0039313.s3tc.ctex" path.s3tc="res://.godot/imported/straywild_character_source_mouth_three.png-adf40ee30e725bafadf6a8fa4dbd8abf.s3tc.ctex"
path.etc2="res://.godot/imported/netfishing_character_source_mouth_three.png-6bf79d4c9c660d30fb7fc365f0039313.etc2.ctex" path.etc2="res://.godot/imported/straywild_character_source_mouth_three.png-adf40ee30e725bafadf6a8fa4dbd8abf.etc2.ctex"
metadata={ metadata={
"imported_formats": ["s3tc_bptc", "etc2_astc"], "imported_formats": ["s3tc_bptc", "etc2_astc"],
"vram_texture": true "vram_texture": true
@ -15,8 +15,8 @@ generator_parameters={
[deps] [deps]
source_file="res://art/source/characters/base/netfishing_character_source_mouth_three.png" source_file="res://art/source/characters/base/straywild_character_source_mouth_three.png"
dest_files=["res://.godot/imported/netfishing_character_source_mouth_three.png-6bf79d4c9c660d30fb7fc365f0039313.s3tc.ctex", "res://.godot/imported/netfishing_character_source_mouth_three.png-6bf79d4c9c660d30fb7fc365f0039313.etc2.ctex"] dest_files=["res://.godot/imported/straywild_character_source_mouth_three.png-adf40ee30e725bafadf6a8fa4dbd8abf.s3tc.ctex", "res://.godot/imported/straywild_character_source_mouth_three.png-adf40ee30e725bafadf6a8fa4dbd8abf.etc2.ctex"]
[params] [params]

View file

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Before After
Before After

View file

@ -3,8 +3,8 @@
importer="texture" importer="texture"
type="CompressedTexture2D" type="CompressedTexture2D"
uid="uid://7dwa0bfev1rk" uid="uid://7dwa0bfev1rk"
path.s3tc="res://.godot/imported/netfishing_character_source_nose1.2.png-ef4dd456c5b0f0db73b3dc063bfba941.s3tc.ctex" path.s3tc="res://.godot/imported/straywild_character_source_nose1.2.png-0a8355472731651768eaa5fc2ee168d4.s3tc.ctex"
path.etc2="res://.godot/imported/netfishing_character_source_nose1.2.png-ef4dd456c5b0f0db73b3dc063bfba941.etc2.ctex" path.etc2="res://.godot/imported/straywild_character_source_nose1.2.png-0a8355472731651768eaa5fc2ee168d4.etc2.ctex"
metadata={ metadata={
"imported_formats": ["s3tc_bptc", "etc2_astc"], "imported_formats": ["s3tc_bptc", "etc2_astc"],
"vram_texture": true "vram_texture": true
@ -15,8 +15,8 @@ generator_parameters={
[deps] [deps]
source_file="res://art/source/characters/base/netfishing_character_source_nose1.2.png" source_file="res://art/source/characters/base/straywild_character_source_nose1.2.png"
dest_files=["res://.godot/imported/netfishing_character_source_nose1.2.png-ef4dd456c5b0f0db73b3dc063bfba941.s3tc.ctex", "res://.godot/imported/netfishing_character_source_nose1.2.png-ef4dd456c5b0f0db73b3dc063bfba941.etc2.ctex"] dest_files=["res://.godot/imported/straywild_character_source_nose1.2.png-0a8355472731651768eaa5fc2ee168d4.s3tc.ctex", "res://.godot/imported/straywild_character_source_nose1.2.png-0a8355472731651768eaa5fc2ee168d4.etc2.ctex"]
[params] [params]

View file

@ -3,8 +3,8 @@
importer="texture" importer="texture"
type="CompressedTexture2D" type="CompressedTexture2D"
uid="uid://b0p55hwdry2cr" uid="uid://b0p55hwdry2cr"
path.s3tc="res://.godot/imported/netfishing_character_source_nose_dog_round.png-1c89150861ebff3f930698c1747a0369.s3tc.ctex" path.s3tc="res://.godot/imported/straywild_character_source_nose_dog_round.png-d1ea0db61e41dfeeed3b599286ffb82c.s3tc.ctex"
path.etc2="res://.godot/imported/netfishing_character_source_nose_dog_round.png-1c89150861ebff3f930698c1747a0369.etc2.ctex" path.etc2="res://.godot/imported/straywild_character_source_nose_dog_round.png-d1ea0db61e41dfeeed3b599286ffb82c.etc2.ctex"
metadata={ metadata={
"imported_formats": ["s3tc_bptc", "etc2_astc"], "imported_formats": ["s3tc_bptc", "etc2_astc"],
"vram_texture": true "vram_texture": true
@ -15,8 +15,8 @@ generator_parameters={
[deps] [deps]
source_file="res://art/source/characters/base/netfishing_character_source_nose_dog_round.png" source_file="res://art/source/characters/base/straywild_character_source_nose_dog_round.png"
dest_files=["res://.godot/imported/netfishing_character_source_nose_dog_round.png-1c89150861ebff3f930698c1747a0369.s3tc.ctex", "res://.godot/imported/netfishing_character_source_nose_dog_round.png-1c89150861ebff3f930698c1747a0369.etc2.ctex"] dest_files=["res://.godot/imported/straywild_character_source_nose_dog_round.png-d1ea0db61e41dfeeed3b599286ffb82c.s3tc.ctex", "res://.godot/imported/straywild_character_source_nose_dog_round.png-d1ea0db61e41dfeeed3b599286ffb82c.etc2.ctex"]
[params] [params]

Binary file not shown.

View file

@ -7,7 +7,7 @@ prove authorship or licensing.
## Ownership and credits ## Ownership and credits
NETfishing is jointly owned, developed, and published by its creators, straywild is jointly owned, developed, and published by its creators,
publicly credited as Voyager and Endeavour and operating collectively as the publicly credited as Voyager and Endeavour and operating collectively as the
independent team Woofmeow. independent team Woofmeow.
@ -31,7 +31,7 @@ independent team Woofmeow.
Voyager is also the original catalog porter credit in PortMaster metadata. Voyager is also the original catalog porter credit in PortMaster metadata.
Third-party creators are credited with their individual records below. Those Third-party creators are credited with their individual records below. Those
credits do not imply endorsement of NETfishing, Woofmeow, or its owners. credits do not imply endorsement of straywild, Woofmeow, or its owners.
## License map ## License map
@ -41,7 +41,7 @@ Do not describe the complete repository as covered only by the GPL.
| --- | --- | | --- | --- |
| Project-owned software code, shaders, code-oriented scene/resource configuration, tests, build tooling, and server software | GPL-3.0-or-later; source-root `LICENSE` and packaged `GPL-3.0-or-later.txt` | | Project-owned software code, shaders, code-oriented scene/resource configuration, tests, build tooling, and server software | GPL-3.0-or-later; source-root `LICENSE` and packaged `GPL-3.0-or-later.txt` |
| Project-owned artwork, textures, models, animation, music, audio, narrative content, characters, and other creative assets | Source-root or packaged `ASSET-LICENSE.md` | | Project-owned artwork, textures, models, animation, music, audio, narrative content, characters, and other creative assets | Source-root or packaged `ASSET-LICENSE.md` |
| NETfishing and Woofmeow names, logos, icons, and official identity | Source-root or packaged `TRADEMARKS.md` | | straywild and Woofmeow names, logos, icons, and official identity | Source-root or packaged `TRADEMARKS.md` |
| Third-party engines, fonts, sounds, and other external material | Their original terms, recorded below and in adjacent license texts where applicable | | Third-party engines, fonts, sounds, and other external material | Their original terms, recorded below and in adjacent license texts where applicable |
| Previously accepted or expressly invited outside contributions | Source-root `CONTRIBUTOR-TERMS.md` plus any separately signed agreement | | Previously accepted or expressly invited outside contributions | Source-root `CONTRIBUTOR-TERMS.md` plus any separately signed agreement |
@ -50,22 +50,22 @@ the terms of their source material or generating code. Repository location
alone does not relicense third-party material. alone does not relicense third-party material.
The GPL permits study, modification, redistribution, and forks of covered The GPL permits study, modification, redistribution, and forks of covered
code. It does not grant rights to NETfishing creative assets or branding. The code. It does not grant rights to straywild creative assets or branding. The
NETfishing Asset License separately permits qualifying noncommercial add-on straywild Asset License separately permits qualifying noncommercial add-on
Mods for official NETfishing software and narrowly scoped Contribution Forks Mods for official straywild software and narrowly scoped Contribution Forks
used to prepare changes for possible upstream inclusion. It does not permit used to prepare changes for possible upstream inclusion. It does not permit
those assets to accompany independent code forks, clones, standalone games, those assets to accompany independent code forks, clones, standalone games,
unrelated products, or general-purpose asset packs. A GPL fork that is not a unrelated products, or general-purpose asset packs. A GPL fork that is not a
qualifying Contribution Fork must replace NETfishing assets and reserved qualifying Contribution Fork must replace straywild assets and reserved
branding with independently licensed material and a distinct identity. branding with independently licensed material and a distinct identity.
Project-wide code, asset, branding, or contributor-license changes require Project-wide code, asset, branding, or contributor-license changes require
written approval from both NETfishing owners under their private joint written approval from both straywild owners under their private joint
ownership agreement. ownership agreement.
## Godot Engine notice ## Godot Engine notice
NETfishing uses Godot Engine under the MIT License: straywild uses Godot Engine under the MIT License:
> Copyright (c) 2014-present Godot Engine contributors. > Copyright (c) 2014-present Godot Engine contributors.
> Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. > Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.
@ -103,7 +103,7 @@ FreeType for font rendering; portions are copyright The FreeType Project
Runtime file: `ui/fonts/seattle_avenue.otf`; source, hash, and original notice: Runtime file: `ui/fonts/seattle_avenue.otf`; source, hash, and original notice:
`ui/fonts/Seattle-Avenue-LICENSE.txt`. `ui/fonts/Seattle-Avenue-LICENSE.txt`.
Public-domain fonts are not governed by the NETfishing Asset License. Public-domain fonts are not governed by the straywild Asset License.
## Intake policy ## Intake policy
@ -122,7 +122,7 @@ owners' private project records. This public document should contain the
credits and license notices needed by recipients; it should not duplicate a credits and license notices needed by recipients; it should not duplicate a
changing file inventory that can be derived from the repository itself. changing file inventory that can be derived from the repository itself.
NETfishing does not accept unsolicited contributions. For work expressly straywild does not accept unsolicited contributions. For work expressly
invited by both Owners, maintainers must obtain affirmative agreement to the invited by both Owners, maintainers must obtain affirmative agreement to the
source-root `CONTRIBUTOR-TERMS.md` and retain the written invitation and source-root `CONTRIBUTOR-TERMS.md` and retain the written invitation and
agreement in a durable Project record. agreement in a durable Project record.
@ -142,8 +142,10 @@ environment and UI artwork, item artwork, finished fish and creature artwork,
and character customization pattern and channel-map artwork. Voyager's and character customization pattern and channel-map artwork. Voyager's
incorporated original work includes 3D assets, the provisional fish incorporated original work includes 3D assets, the provisional fish
placeholder artwork, the title and dusk music identified above, and the robot placeholder artwork, the title and dusk music identified above, and the robot
animalese tones. Specific contributor exceptions are recorded below and in the animalese tones. The current starter motorcoach exterior was supplied by a
in-game credits. contributing artist who released its copyright to the project; the original
and cleaned source files are retained in the owners' source-work archive.
Specific contributor exceptions are recorded below and in the in-game credits.
These credits intentionally describe work by creator and contribution family These credits intentionally describe work by creator and contribution family
instead of maintaining a second file-by-file asset catalog. Runtime resources instead of maintaining a second file-by-file asset catalog. Runtime resources
@ -159,8 +161,8 @@ against the repository when this record was revised.
| Material | Runtime location | Credit or terms | | Material | Runtime location | Credit or terms |
| --- | --- | --- | | --- | --- | --- |
| Kat animalese alphabet and number sounds | `sound/dialogue/animalese/alphanumeric/` | Project-processed derivatives of the original source voice recording supplied by NETfishing community member kat. | | Kat animalese alphabet and number sounds | `sound/dialogue/animalese/alphanumeric/` | Project-processed derivatives of the original source voice recording supplied by straywild community member kat. |
| Kim animalese alphabet and number sounds | `sound/dialogue/animalese/kim/` | Project-processed derivatives of the original source voice recording supplied by NETfishing community member kim. | | Kim animalese alphabet and number sounds | `sound/dialogue/animalese/kim/` | Project-processed derivatives of the original source voice recording supplied by straywild community member kim. |
| Tuffy font | `ui/fonts/Tuffy_Bold.otf` | Public-domain dedication in `ui/fonts/Tuffy-LICENSE.txt`. | | Tuffy font | `ui/fonts/Tuffy_Bold.otf` | Public-domain dedication in `ui/fonts/Tuffy-LICENSE.txt`. |
| Seattle Avenue font | `ui/fonts/seattle_avenue.otf` | Public-domain font by JLH Fonts; notice in `ui/fonts/Seattle-Avenue-LICENSE.txt`. | | Seattle Avenue font | `ui/fonts/seattle_avenue.otf` | Public-domain font by JLH Fonts; notice in `ui/fonts/Seattle-Avenue-LICENSE.txt`. |
| Fishing fight loop | `audio/sfx/fishing/fighting.wav` | Edited from “Spinning reel.wav” by Freesound user tosha73, sound 509902, CC0. | | Fishing fight loop | `audio/sfx/fishing/fighting.wav` | Edited from “Spinning reel.wav” by Freesound user tosha73, sound 509902, CC0. |

View file

@ -1,6 +1,6 @@
# Development reference # Development reference
This is the authoritative reference for NETfishing architecture, engineering This is the authoritative reference for straywild architecture, engineering
decisions, content policy, authoring, and validation. decisions, content policy, authoring, and validation.
## Architecture ## Architecture
@ -43,7 +43,7 @@ open host may publish a short-lived room lease, and the shared Join Game page
may browse compatible leases before handing the selected address back to the may browse compatible leases before handing the selected address back to the
existing direct ENet connection flow. The directory does not carry gameplay existing direct ENet connection flow. The directory does not carry gameplay
traffic or become a gameplay authority. Its base URL comes from traffic or become a gameplay authority. Its base URL comes from
`network/discovery/base_url`, with `NETFISHING_DISCOVERY_URL` available as a `network/discovery/base_url`, with `straywild_DISCOVERY_URL` available as a
development/deployment override. development/deployment override.
Friendships are local identity relationships shared across save slots. A live, Friendships are local identity relationships shared across save slots. A live,
@ -261,7 +261,7 @@ copies of the entire customization catalog.
### Texture sampling ### Texture sampling
NETfishing artwork uses nearest-neighbor sampling. Do not enable linear, straywild artwork uses nearest-neighbor sampling. Do not enable linear,
bilinear, trilinear, or anisotropic texture filtering. Shader samplers must bilinear, trilinear, or anisotropic texture filtering. Shader samplers must
declare `filter_nearest`. The three canonical repeating terrain textures are declare `filter_nearest`. The three canonical repeating terrain textures are
the sole mipmap exception: their projected shader uses nearest-filtered the sole mipmap exception: their projected shader uses nearest-filtered
@ -351,10 +351,10 @@ godot --headless --path . --script tests/fish_catalog_content_validation.gd
rm -rf -- "$test_root" rm -rf -- "$test_root"
``` ```
Do not point `NETFISHING_DATA_DIR` at an arbitrary empty directory; it is an Do not point `straywild_DATA_DIR` at an arbitrary empty directory; it is an
explicit portable-data override and must identify a valid data root. The explicit portable-data override and must identify a valid data root. The
PortMaster launcher is the narrow exception: it also sets PortMaster launcher is the narrow exception: it also sets
`NETFISHING_CREATE_DATA_DIR=1`, allowing the game to initialize its known empty `straywild_CREATE_DATA_DIR=1`, allowing the game to initialize its known empty
external save directory on first launch. external save directory on first launch.
Headless tests cannot prove visual alignment, mouse routing, shader appearance, Headless tests cannot prove visual alignment, mouse routing, shader appearance,

View file

@ -1,15 +1,15 @@
# PortMaster reference # PortMaster reference
NETfishing's PortMaster release is a canonical PortMaster package, not a straywild's PortMaster release is a canonical PortMaster package, not a
version-named generic ZIP. The installer-facing archive must be named version-named generic ZIP. The installer-facing archive must be named
`netfishing.zip`, and its root must contain exactly: `straywild.zip`, and its root must contain exactly:
```text ```text
NETfishing.sh straywild.sh
netfishing/ straywild/
``` ```
`port.json` belongs at `netfishing/port.json` in the distributable archive. `port.json` belongs at `straywild/port.json` in the distributable archive.
The upstream source-submission layout keeps it beside the launcher and port The upstream source-submission layout keeps it beside the launcher and port
directory, but PortMaster's release builder relocates it into the declared directory, but PortMaster's release builder relocates it into the declared
port directory. Do not copy the source-submission layout directly into a port directory. Do not copy the source-submission layout directly into a
@ -17,7 +17,7 @@ release ZIP.
HarbourMaster's installer also relocates recognized non-script files found at HarbourMaster's installer also relocates recognized non-script files found at
an archive root into the detected port directory. That compatibility behavior an archive root into the detected port directory. That compatibility behavior
masked NETfishing's former root-level `port.json` during auto-install tests. masked straywild's former root-level `port.json` during auto-install tests.
Directly extracting the same archive into `/ports` did not perform the repair. Directly extracting the same archive into `/ports` did not perform the repair.
Do not rely on installer relocation; build the distributable layout correctly. Do not rely on installer relocation; build the distributable layout correctly.
@ -26,30 +26,34 @@ directory. The templates in `scripts/portmaster/` are authoritative.
## Release contract ## Release contract
- `port.json` uses schema version 4, names `netfishing.zip`, and marks the - `port.json` uses schema version 4, names `straywild.zip`, and marks the
self-contained package ready to run. self-contained package ready to run.
- `NETfishing.sh` does not contain a `# PORTMASTER:` signature in the source or - `straywild.sh` does not contain a `# PORTMASTER:` signature in the source or
distributable archive. HarbourMaster adds that installation marker when it distributable archive. HarbourMaster adds that installation marker when it
installs the port. installs the port.
- The executable is `netfishing/NETfishing.aarch64`. - The executable is `straywild/straywild.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 starts GPTOKEYB with the explicit `netfishing.gptk` no-op map. - The launcher starts GPTOKEYB with the explicit `straywild.gptk` no-op map.
Every keyboard and mouse binding is disabled, leaving only GPTOKEYB's Every keyboard and mouse binding is disabled, leaving only GPTOKEYB's
device-specific force-quit chord while Godot and NETfishing's controller device-specific force-quit chord while Godot and straywild's controller
mapping manager handle gameplay input. The explicit map also avoids the mapping manager handle gameplay input. The explicit map also avoids the
anomalous controller behavior observed on ROCKNIX without a passthrough map. anomalous controller behavior observed on ROCKNIX without a passthrough map.
- The launcher calls `pm_platform_helper` for the game executable after - The launcher calls `pm_platform_helper` for the game executable after
starting GPTOKEYB and calls `pm_finish` after the game exits. starting GPTOKEYB and calls `pm_finish` after the game exits.
- NETfishing automatically creates player data for new installations under - straywild automatically creates player data for new installations under
`ports/saves/netfishing/` and does not present the in-game folder picker. `ports/saves/straywild/` and does not present the in-game folder picker.
- Existing installations with a selected data root keep that location when - Existing installations with a selected data root keep that location when
upgraded. Device-local data, configuration, and cache remain under upgraded. Device-local data, configuration, and cache remain under
`netfishing/conf/data`, `netfishing/conf/config`, and `straywild/conf/data`, `straywild/conf/config`, and
`netfishing/conf/cache`. `straywild/conf/cache`.
- A rebrand upgrade copies an existing `netfishing/conf/` tree into the new
`straywild/conf/` tree when the latter does not exist. It also reuses an
existing `ports/saves/netfishing/` data root rather than creating an empty
replacement. New installations use straywild paths exclusively.
- The archive must not contain `conf/`, saves, identities, logs, source files, - The archive must not contain `conf/`, saves, identities, logs, source files,
`.git`, or `.godot` content. `.git`, or `.godot` content.
- Release downloads must publish only the canonical `netfishing.zip` for this - Release downloads must publish only the canonical `straywild.zip` for this
platform. Do not also publish a byte-identical versioned PortMaster ZIP; platform. Do not also publish a byte-identical versioned PortMaster ZIP;
HarbourMaster identifies the port by its canonical archive name and the HarbourMaster identifies the port by its canonical archive name and the
Forgejo release tag already supplies the version. Forgejo release tag already supplies the version.
@ -67,7 +71,7 @@ The script deletes and regenerates the Linux ARM64 export, stages the package,
validates its structure, and writes: validates its structure, and writes:
```text ```text
builds/v<project-version>/netfishing.zip builds/v<project-version>/straywild.zip
``` ```
`--package-only` is reserved for repackaging an already validated ARM64 export. `--package-only` is reserved for repackaging an already validated ARM64 export.
@ -83,7 +87,7 @@ bash scripts/build_portmaster.sh --package-only --hotfix
The hotfix keeps the game executable and PCK tied to the release tag while The hotfix keeps the game executable and PCK tied to the release tag while
recording the newer packaging commit separately in `BUILD-INFO.txt`. Replace recording the newer packaging commit separately in `BUILD-INFO.txt`. Replace
the release's existing `netfishing.zip`; never move the game release tag. the release's existing `straywild.zip`; never move the game release tag.
## Local muOS installation ## Local muOS installation
@ -93,22 +97,22 @@ The muOS auto-install directory is:
/mnt/mmc/MUOS/PortMaster/autoinstall/ /mnt/mmc/MUOS/PortMaster/autoinstall/
``` ```
For an explicit offline installation test, copy `netfishing.zip` to a temporary For an explicit offline installation test, copy `straywild.zip` to a temporary
device directory and run: device directory and run:
```bash ```bash
/mnt/mmc/MUOS/PortMaster/harbourmaster \ /mnt/mmc/MUOS/PortMaster/harbourmaster \
--offline --no-check install ./netfishing.zip --offline --no-check install ./straywild.zip
``` ```
Before upgrading an existing installation, preserve Before upgrading an existing installation, preserve either the current
`/mnt/mmc/ports/netfishing/conf/` and `/mnt/mmc/ports/saves/netfishing/` on the `/mnt/mmc/ports/straywild/` paths or the former `/mnt/mmc/ports/netfishing/`
device. After installation: paths 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 HarbourMaster added its `# PORTMASTER:` installation signature to 2. Confirm HarbourMaster added its `# PORTMASTER:` installation signature to
the installed launcher, then confirm the launcher starts GPTOKEYB with the installed launcher, then confirm the launcher starts GPTOKEYB with
`netfishing.gptk` and calls `pm_platform_helper` for the game executable. `straywild.gptk` and calls `pm_platform_helper` for the game executable.
3. Confirm `conf/` and the external save directory were not replaced or 3. Confirm `conf/` and the external save directory were not replaced or
removed. removed.
4. Launch the installed port through the normal muOS menu. 4. Launch the installed port through the normal muOS menu.
@ -117,7 +121,7 @@ device. After installation:
Start+Select on most devices. 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 `straywild.zip` is updated. A local release
test must install the generated local archive explicitly. test must install the generated local archive explicitly.
## Template provenance ## Template provenance
@ -130,7 +134,7 @@ f24daae48f543b3a270a5aa46b5e73a31904bf0c7216d4c8a9e15a8dc48a5eed screenshot.png
``` ```
The remaining catalog metadata assets were copied from the installed official The remaining catalog metadata assets were copied from the installed official
`netfishing.zip` package and are intentionally preserved byte-for-byte: `straywild.zip` package and are intentionally preserved byte-for-byte:
```text ```text
e2a9d132744684c67865c01eca027a8c9946b4c5a19da57c82f1af0373dc83b7 gameinfo.xml e2a9d132744684c67865c01eca027a8c9946b4c5a19da57c82f1af0373dc83b7 gameinfo.xml
@ -160,7 +164,7 @@ The device-verified Godot mapping is:
- left-stick click `b9`, left trigger `b10`, right trigger `b11`, right-stick click `b12` - left-stick click `b9`, left trigger `b10`, right trigger `b11`, right-stick click `b12`
- D-pad `h0`; left and right sticks `a0..a3` - D-pad `h0`; left and right sticks `a0..a3`
`scripts/portmaster/NETfishing.sh` must replace the incompatible SDL2 entry `scripts/portmaster/straywild.sh` must replace the incompatible SDL2 entry
with the Godot entry when that SDL2 GUID is selected. Do not append the two with the Godot entry when that SDL2 GUID is selected. Do not append the two
entries with a newline. WestonPack reconstructs the wrapped game command entries with a newline. WestonPack reconstructs the wrapped game command
through a shell, so raw mapping values passed in that command can be split into through a shell, so raw mapping values passed in that command can be split into
@ -169,14 +173,14 @@ this failure with the `GO-Super Gamepad` mapping. The outer Weston environment
also does not reliably preserve the corrected muOS mapping for the game. also does not reliably preserve the corrected muOS mapping for the game.
Write the exact selected mapping to `conf/cache/controller_mapping.txt` before Write the exact selected mapping to `conf/cache/controller_mapping.txt` before
starting Weston. Weston must launch `launch-netfishing.sh`, which reads that starting Weston. Weston must launch `launch-straywild.sh`, which reads that
file, exports `SDL_GAMECONTROLLERCONFIG`, and immediately replaces itself with file, exports `SDL_GAMECONTROLLERCONFIG`, and immediately replaces itself with
the game process. This applies the mapping at the same point as the working the game process. This applies the mapping at the same point as the working
0.10.1 launcher without passing its raw text through WestonPack's command 0.10.1 launcher without passing its raw text through WestonPack's command
reconstruction. Do not move the mapping back to either side of reconstruction. Do not move the mapping back to either side of
`westonwrap.sh`. `westonwrap.sh`.
Always pass `netfishing.gptk`, which explicitly unassigns every direct and Always pass `straywild.gptk`, which explicitly unassigns every direct and
hotkey binding with GPTOKEYB's `\"` value. This keeps GPTOKEYB from injecting hotkey binding with GPTOKEYB's `\"` value. This keeps GPTOKEYB from injecting
keyboard or mouse events on top of Godot's native controller input and retains keyboard or mouse events on top of Godot's native controller input and retains
the device-specific kill chord. Supplying the explicit passthrough map also the device-specific kill chord. Supplying the explicit passthrough map also
@ -209,8 +213,8 @@ The light profile passes these Godot options:
- `--max-fps 30` - `--max-fps 30`
- `--audio-output-latency 40` - `--audio-output-latency 40`
It also passes `NETFISHING_PERFORMANCE_PROFILE=light` and It also passes `straywild_PERFORMANCE_PROFILE=light` and
`NETFISHING_LOW_END=1`. The game renders the 3D world at 37.5% linear `straywild_LOW_END=1`. The game renders the 3D world at 37.5% linear
resolution with nearest-neighbor scaling, reducing 3D pixel work by about 86% resolution with nearest-neighbor scaling, reducing 3D pixel work by about 86%
while the separately rendered UI retains its canonical resolution. The light while the separately rendered UI retains its canonical resolution. The light
profile also: profile also:
@ -244,7 +248,7 @@ omitted ambience or replace the micro textures with desktop-resolution assets.
The normal profile otherwise retains the full visual presentation. The normal profile otherwise retains the full visual presentation.
To opt a capable device into the normal profile, create the persistent file To opt a capable device into the normal profile, create the persistent file
`netfishing/conf/performance_profile` containing exactly: `straywild/conf/performance_profile` containing exactly:
```text ```text
normal normal
@ -253,13 +257,13 @@ normal
On a typical muOS installation, the full path is: On a typical muOS installation, the full path is:
```text ```text
/mnt/mmc/ports/netfishing/conf/performance_profile /mnt/mmc/ports/straywild/conf/performance_profile
``` ```
Set the file to `light`, or remove it, to restore the default. An externally Set the file to `light`, or remove it, to restore the default. An externally
provided `NETFISHING_PERFORMANCE_PROFILE=normal` or `light` environment value provided `straywild_PERFORMANCE_PROFILE=normal` or `light` environment value
takes precedence over the persistent file. Invalid values safely fall back to takes precedence over the persistent file. Invalid values safely fall back to
the light profile and are reported in `netfishing/log.txt`. the light profile and are reported in `straywild/log.txt`.
Do not use Godot's low processor mode for the light profile. It reduces idle Do not use Godot's low processor mode for the light profile. It reduces idle
CPU usage by sleeping between updates and is not a game-performance CPU usage by sleeping between updates and is not a game-performance

View file

@ -1,10 +1,10 @@
NETfishing straywild
v0.18.0-alpha v0.20.2-alpha
Alpha 0.18.0 Alpha 0.20.2
Thank you for trying this early private playtest. Thank you for supporting this private testing release.
NETfishing is an early multiplayer fishing game. This alpha includes local straywild is an early multiplayer fishing game. This alpha includes local
progression, player-hosted and dedicated multiplayer sessions, public room progression, player-hosted and dedicated multiplayer sessions, public room
discovery, and direct UDP connections. Expect rough edges and please keep a discovery, and direct UDP connections. Expect rough edges and please keep a
backup of any save or identity data you care about. backup of any save or identity data you care about.
@ -12,15 +12,15 @@ backup of any save or identity data you care about.
INSTALLATION INSTALLATION
1. Extract the entire ZIP into a new folder. 1. Extract the entire ZIP into a new folder.
2. Keep the executable and netfishing.pck together. 2. Keep the executable and straywild.pck together.
3. Launch netfishing.exe on Windows or netfishing on Linux. 3. Launch straywild.exe on Windows or straywild on Linux.
Windows may show a warning for an unsigned application. This private build is Windows may show a warning for an unsigned application. This private build is
not code signed. Only run a copy received from the playtest organizer. not code signed. Only run a copy received from the playtest organizer.
Linux users may need to allow execution: Linux users may need to allow execution:
chmod +x netfishing chmod +x straywild
CONTROLS CONTROLS
@ -83,6 +83,8 @@ FEATURES TO TRY
- Fishing, net gathering, and beach digging - Fishing, net gathering, and beach digging
- Equipment and consumable use from the hotbar - Equipment and consumable use from the hotbar
- Fishing Shop sell tray, wallet updates, and persistent upgrades - Fishing Shop sell tray, wallet updates, and persistent upgrades
- Portable RV homes, multiplayer visits, RV storage, and the early decor
purchasing and placement prototype
- Seeded generated worlds and the authored starter-island layout - Seeded generated worlds and the authored starter-island layout
- Generated beaches, ponds, lakes, complete rivers, denser forests, varied - Generated beaches, ponds, lakes, complete rivers, denser forests, varied
elevation, and closed coastal cliffs elevation, and closed coastal cliffs
@ -117,5 +119,6 @@ KNOWN ALPHA LIMITATIONS
networks may prevent some players from joining. networks may prevent some players from joining.
- Multiplayer, dedicated-server deployment, UI, and balance are still alpha. - Multiplayer, dedicated-server deployment, UI, and balance are still alpha.
- Some fish and character artwork remains temporary. - Some fish and character artwork remains temporary.
- RV exteriors and decor use temporary prototype geometry and presentation.
- There is no installer or automatic updater. - There is no installer or automatic updater.
- Windows builds are unsigned. - Windows builds are unsigned.

View file

@ -1,4 +1,4 @@
# NETfishing documentation # straywild documentation
This directory is the single home for maintained project documentation. This directory is the single home for maintained project documentation.
Start here instead of searching feature, asset, world, or script directories Start here instead of searching feature, asset, world, or script directories
@ -17,10 +17,10 @@ for local README files.
## Related repositories ## Related repositories
- [Discovery service API](https://forge.makearmy.io/woofmeow/netfishing-discovery-server/src/branch/main/docs/API.md) - [Discovery service API](https://forge.makearmy.io/woofmeow/straywild-discovery-server/src/branch/main/docs/API.md)
stays with the service implementation and defines its versioned HTTP stays with the service implementation and defines its versioned HTTP
contract. contract.
- [Dedicated-server packaging](https://forge.makearmy.io/woofmeow/netfishing-dedicated-server/src/branch/main/README.md) - [Dedicated-server packaging](https://forge.makearmy.io/woofmeow/straywild-dedicated-server/src/branch/main/README.md)
stays with its installer and update scripts. It consumes this repository's stays with its installer and update scripts. It consumes this repository's
attribution and legal notices from the exact pinned game commit instead of attribution and legal notices from the exact pinned game commit instead of
maintaining copies. maintaining copies.

View file

@ -9,7 +9,7 @@ custom_features=""
export_filter="all_resources" export_filter="all_resources"
include_filter="LICENSE,ASSET-LICENSE.md,TRADEMARKS.md,docs/ATTRIBUTION.md,ui/fonts/*LICENSE.txt" include_filter="LICENSE,ASSET-LICENSE.md,TRADEMARKS.md,docs/ATTRIBUTION.md,ui/fonts/*LICENSE.txt"
exclude_filter="builds/*,scripts/*" exclude_filter="builds/*,scripts/*"
export_path="builds/v0.18.0-alpha/windows-x86_64/NETfishing.exe" export_path="builds/v0.20.2-alpha/windows-x86_64/straywild.exe"
patches=PackedStringArray() patches=PackedStringArray()
encryption_include_filters="" encryption_include_filters=""
encryption_exclude_filters="" encryption_exclude_filters=""
@ -32,16 +32,16 @@ codesign/digest_algorithm=1
codesign/description="" codesign/description=""
codesign/custom_options=PackedStringArray() codesign/custom_options=PackedStringArray()
application/modify_resources=true application/modify_resources=true
application/icon="res://art/exported/system_icons/netfishing.ico" application/icon="res://art/exported/system_icons/straywild.ico"
application/console_wrapper_icon="" application/console_wrapper_icon=""
application/icon_interpolation=4 application/icon_interpolation=0
application/file_version="0.18.0.0" application/file_version="0.20.2.0"
application/product_version="0.18.0.0" application/product_version="0.20.2.0"
application/company_name="Woofmeow" application/company_name="Woofmeow"
application/product_name="NETfishing" application/product_name="straywild"
application/file_description="NETfishing v0.18.0-alpha" application/file_description="straywild v0.20.2-alpha"
application/copyright="Copyright © 2026 Woofmeow" application/copyright="Copyright © 2026 Woofmeow"
application/trademarks="NETfishing and Woofmeow branding is reserved; see TRADEMARKS.md" application/trademarks="straywild and Woofmeow branding is reserved; see TRADEMARKS.md"
application/export_angle=0 application/export_angle=0
ssh_remote_deploy/enabled=false ssh_remote_deploy/enabled=false
ssh_remote_deploy/host="user@host_ip" ssh_remote_deploy/host="user@host_ip"
@ -62,7 +62,7 @@ custom_features=""
export_filter="all_resources" export_filter="all_resources"
include_filter="LICENSE,ASSET-LICENSE.md,TRADEMARKS.md,docs/ATTRIBUTION.md,ui/fonts/*LICENSE.txt" include_filter="LICENSE,ASSET-LICENSE.md,TRADEMARKS.md,docs/ATTRIBUTION.md,ui/fonts/*LICENSE.txt"
exclude_filter="builds/*,scripts/*" exclude_filter="builds/*,scripts/*"
export_path="builds/v0.18.0-alpha/linux-arm64/NETfishing.arm64" export_path="builds/v0.20.2-alpha/linux-arm64/straywild.arm64"
patches=PackedStringArray() patches=PackedStringArray()
encryption_include_filters="" encryption_include_filters=""
encryption_exclude_filters="" encryption_exclude_filters=""
@ -92,7 +92,7 @@ custom_features=""
export_filter="all_resources" export_filter="all_resources"
include_filter="LICENSE,ASSET-LICENSE.md,TRADEMARKS.md,docs/ATTRIBUTION.md,ui/fonts/*LICENSE.txt" include_filter="LICENSE,ASSET-LICENSE.md,TRADEMARKS.md,docs/ATTRIBUTION.md,ui/fonts/*LICENSE.txt"
exclude_filter="builds/*,scripts/*" exclude_filter="builds/*,scripts/*"
export_path="builds/v0.18.0-alpha/macos/NETfishing.zip" export_path="builds/v0.20.2-alpha/macos/straywild.zip"
patches=PackedStringArray() patches=PackedStringArray()
encryption_include_filters="" encryption_include_filters=""
encryption_exclude_filters="" encryption_exclude_filters=""
@ -106,10 +106,10 @@ script_export_mode=2
custom_template/debug="" custom_template/debug=""
custom_template/release="" custom_template/release=""
application/bundle_identifier="io.woofmeow.netfishing" application/bundle_identifier="io.woofmeow.netfishing"
application/icon="res://art/exported/system_icons/netfishing_1024.png" application/icon="res://art/exported/system_icons/straywild_1024.png"
application/icon_interpolation=0 application/icon_interpolation=0
application/short_version="0.18.0" application/short_version="0.20.2"
application/version="0.18.0" application/version="0.20.2"
application/architecture="universal" application/architecture="universal"
codesign/enable=false codesign/enable=false
notarization/enable=false notarization/enable=false
@ -125,7 +125,7 @@ custom_features=""
export_filter="all_resources" export_filter="all_resources"
include_filter="LICENSE,ASSET-LICENSE.md,TRADEMARKS.md,docs/ATTRIBUTION.md,ui/fonts/*LICENSE.txt" include_filter="LICENSE,ASSET-LICENSE.md,TRADEMARKS.md,docs/ATTRIBUTION.md,ui/fonts/*LICENSE.txt"
exclude_filter="builds/*,scripts/*,tests/*" exclude_filter="builds/*,scripts/*,tests/*"
export_path="builds/v0.18.0-alpha/server-linux-x86_64/NETfishingServer.x86_64" export_path="builds/v0.20.2-alpha/server-linux-x86_64/straywildServer.x86_64"
patches=PackedStringArray() patches=PackedStringArray()
encryption_include_filters="" encryption_include_filters=""
encryption_exclude_filters="" encryption_exclude_filters=""
@ -155,7 +155,7 @@ custom_features=""
export_filter="all_resources" export_filter="all_resources"
include_filter="LICENSE,ASSET-LICENSE.md,TRADEMARKS.md,docs/ATTRIBUTION.md,ui/fonts/*LICENSE.txt" include_filter="LICENSE,ASSET-LICENSE.md,TRADEMARKS.md,docs/ATTRIBUTION.md,ui/fonts/*LICENSE.txt"
exclude_filter="builds/*,scripts/*" exclude_filter="builds/*,scripts/*"
export_path="builds/v0.18.0-alpha/android/NETfishing.apk" export_path="builds/v0.20.2-alpha/android/straywild.apk"
patches=PackedStringArray() patches=PackedStringArray()
encryption_include_filters="" encryption_include_filters=""
encryption_exclude_filters="" encryption_exclude_filters=""
@ -176,10 +176,10 @@ architectures/armeabi-v7a=false
architectures/arm64-v8a=true architectures/arm64-v8a=true
architectures/x86=false architectures/x86=false
architectures/x86_64=false architectures/x86_64=false
version/code=180000 version/code=200200
version/name="v0.18.0-alpha" version/name="v0.20.2-alpha"
package/unique_name="io.woofmeow.netfishing" package/unique_name="io.woofmeow.netfishing"
package/name="NETfishing" package/name="straywild"
package/signed=true package/signed=true
package/app_category=0 package/app_category=0
package/retain_data_on_uninstall=false package/retain_data_on_uninstall=false
@ -187,7 +187,7 @@ package/exclude_from_recents=false
package/show_in_android_tv=false package/show_in_android_tv=false
package/show_in_app_library=true package/show_in_app_library=true
package/show_as_launcher_app=true package/show_as_launcher_app=true
launcher_icons/main_192x192="res://art/exported/system_icons/netfishing_256.png" launcher_icons/main_192x192="res://art/exported/system_icons/straywild_256.png"
graphics/opengl_debug=false graphics/opengl_debug=false
screen/immersive_mode=true screen/immersive_mode=true
screen/edge_to_edge=false screen/edge_to_edge=false
@ -214,7 +214,7 @@ custom_features=""
export_filter="all_resources" export_filter="all_resources"
include_filter="LICENSE,ASSET-LICENSE.md,TRADEMARKS.md,docs/ATTRIBUTION.md,ui/fonts/*LICENSE.txt" include_filter="LICENSE,ASSET-LICENSE.md,TRADEMARKS.md,docs/ATTRIBUTION.md,ui/fonts/*LICENSE.txt"
exclude_filter="builds/*,scripts/*" exclude_filter="builds/*,scripts/*"
export_path="builds/v0.18.0-alpha/linux-x86_64/NETfishing.x86_64" export_path="builds/v0.20.2-alpha/linux-x86_64/straywild.x86_64"
patches=PackedStringArray() patches=PackedStringArray()
encryption_include_filters="" encryption_include_filters=""
encryption_exclude_filters="" encryption_exclude_filters=""

View file

@ -41,8 +41,6 @@ const FishingSurfaceSampleType = preload(
const FishingSurfaceResolverType = preload( const FishingSurfaceResolverType = preload(
"res://fishing/fishing_surface_resolver.gd" "res://fishing/fishing_surface_resolver.gd"
) )
const ArtShopStockType = preload("res://economy/art_shop_stock.gd")
const FishingShopStockType = preload("res://economy/fishing_shop_stock.gd")
const WorldTimeServiceType = preload("res://world/world_time_service.gd") const WorldTimeServiceType = preload("res://world/world_time_service.gd")
const WorldWeatherServiceType = preload( const WorldWeatherServiceType = preload(
"res://world/world_weather_service.gd" "res://world/world_weather_service.gd"
@ -134,6 +132,7 @@ const ALTERNATE_REEL_ACTION: StringName = &"reel_alternate"
var state: FishingState = FishingState.READY var state: FishingState = FishingState.READY
var _external_input_blocked: bool = false var _external_input_blocked: bool = false
var _gameplay_input_enabled: bool = false var _gameplay_input_enabled: bool = false
var _fishing_environment_enabled: bool = true
var _local_menu_input_owners: Dictionary[StringName, bool] = {} var _local_menu_input_owners: Dictionary[StringName, bool] = {}
var _local_player: PlayerType var _local_player: PlayerType
var _local_inventory: FishInventoryType var _local_inventory: FishInventoryType
@ -150,6 +149,7 @@ var _network_session: NetworkSessionType
var _network_fishing: NetworkFishingServiceType var _network_fishing: NetworkFishingServiceType
var _world_time: WorldTimeServiceType var _world_time: WorldTimeServiceType
var _world_weather: WorldWeatherServiceType var _world_weather: WorldWeatherServiceType
var _fishable_water_regions_resolver := Callable()
var _active_player: PlayerType var _active_player: PlayerType
var _state_time_remaining: float = 0.0 var _state_time_remaining: float = 0.0
var _cast_charge: float = 0.0 var _cast_charge: float = 0.0
@ -222,6 +222,7 @@ func setup(
network_fishing: NetworkFishingServiceType = null, network_fishing: NetworkFishingServiceType = null,
world_time: WorldTimeServiceType = null, world_time: WorldTimeServiceType = null,
world_weather: WorldWeatherServiceType = null, world_weather: WorldWeatherServiceType = null,
fishable_water_regions_resolver: Callable = Callable(),
) -> void: ) -> void:
_local_player = local_player _local_player = local_player
_local_inventory = local_inventory _local_inventory = local_inventory
@ -242,6 +243,7 @@ func setup(
_network_fishing = network_fishing _network_fishing = network_fishing
_world_time = world_time _world_time = world_time
_world_weather = world_weather _world_weather = world_weather
_fishable_water_regions_resolver = fishable_water_regions_resolver
if ( if (
_network_session != null _network_session != null
and not _network_session.state_changed.is_connected( and not _network_session.state_changed.is_connected(
@ -396,6 +398,22 @@ func set_gameplay_input_enabled(enabled: bool) -> void:
reset_transient_state() reset_transient_state()
func set_fishing_environment_enabled(enabled: bool) -> void:
if _fishing_environment_enabled == enabled:
return
_fishing_environment_enabled = enabled
if enabled:
_new_cast_press_armed = not Input.is_action_pressed("fish_primary")
return
if _network_fishing != null and _network_fishing.has_local_attempt():
_network_fishing.cancel_local_attempt("")
reset_transient_state()
func is_fishing_environment_enabled() -> bool:
return _fishing_environment_enabled
func reset_transient_state(restore_movement: bool = true) -> void: func reset_transient_state(restore_movement: bool = true) -> void:
var local_state_was_active: bool = ( var local_state_was_active: bool = (
state != FishingState.READY or _active_player != null state != FishingState.READY or _active_player != null
@ -530,6 +548,7 @@ func _process(delta: float) -> void:
func _unhandled_input(event: InputEvent) -> void: func _unhandled_input(event: InputEvent) -> void:
if ( if (
not _gameplay_input_enabled not _gameplay_input_enabled
or not _fishing_environment_enabled
or _external_input_blocked or _external_input_blocked
or not _local_menu_input_owners.is_empty() or not _local_menu_input_owners.is_empty()
): ):
@ -542,51 +561,23 @@ func _unhandled_input(event: InputEvent) -> void:
alternate_reel and state == FishingState.FIGHTING alternate_reel and state == FishingState.FIGHTING
): ):
return return
# Fishing owns the primary action only after a rod starts a cast, or while
# completing an already-active fishing sequence. An empty hand, decor,
# gathering tools, and other items must remain available to their own world
# interaction controllers.
if state == FishingState.READY and not has_active_fishing_rod():
return
# A held keyboard key emits echo events. Treat the alternate reel exactly # A held keyboard key emits echo events. Treat the alternate reel exactly
# like a mouse button: one barrier hit per physical press, while the normal # like a mouse button: one barrier hit per physical press, while the normal
# held-input path continues driving free reeling between barriers. # held-input path continues driving free reeling between barriers.
if event is InputEventKey and event.echo: if event is InputEventKey and event.echo:
get_viewport().set_input_as_handled() get_viewport().set_input_as_handled()
return return
var selected_item: ItemDataType = _get_active_item()
if (
state == FishingState.READY
and selected_item != null
and FishingShopStockType.is_gathering_tool(selected_item.item_id)
):
return
if event.is_pressed(): if event.is_pressed():
match state: match state:
FishingState.READY: FishingState.READY:
if not _new_cast_press_armed: if not _new_cast_press_armed:
return return
var active_item: ItemDataType = _get_active_item()
if (
active_item != null
and (
active_item.category == ItemDataType.Category.CONSUMABLE
or (
active_item.item_id == PlayerItemEffectsType.FISH_FINDER_ID
)
)
):
if _network_item_use != null:
_network_item_use.request_use(active_item.item_id)
elif _item_effects.use_item(active_item, _local_bag):
status_changed.emit(_item_effects.get_feedback(
active_item.item_id
))
elif (
active_item.item_id == PlayerItemEffectsType.FISH_FINDER_ID
):
local_speech_requested.emit(
PlayerItemEffectsType.FISH_FINDER_DEAD_MESSAGE
)
get_viewport().set_input_as_handled()
return
if not has_active_fishing_rod():
get_viewport().set_input_as_handled()
return
if ( if (
_is_inventory_full() _is_inventory_full()
): ):
@ -658,6 +649,34 @@ func _unhandled_input(event: InputEvent) -> void:
get_viewport().set_input_as_handled() get_viewport().set_input_as_handled()
func try_use_active_non_fishing_item() -> bool:
if (
not _gameplay_input_enabled
or _external_input_blocked
or not _local_menu_input_owners.is_empty()
or state != FishingState.READY
):
return false
var active_item: ItemDataType = _get_active_item()
if (
active_item == null
or (
active_item.category != ItemDataType.Category.CONSUMABLE
and active_item.item_id != PlayerItemEffectsType.FISH_FINDER_ID
)
):
return false
if _network_item_use != null:
_network_item_use.request_use(active_item.item_id)
elif _item_effects.use_item(active_item, _local_bag):
status_changed.emit(_item_effects.get_feedback(active_item.item_id))
elif active_item.item_id == PlayerItemEffectsType.FISH_FINDER_ID:
local_speech_requested.emit(
PlayerItemEffectsType.FISH_FINDER_DEAD_MESSAGE
)
return true
func _is_reel_input_pressed() -> bool: func _is_reel_input_pressed() -> bool:
return ( return (
Input.is_action_pressed("fish_primary") Input.is_action_pressed("fish_primary")
@ -1664,6 +1683,47 @@ func build_network_context(
return _build_fishing_context(region) return _build_fishing_context(region)
func is_species_available_now(fish: FishDataType) -> bool:
if fish == null or not fish.is_selectable():
return false
var season := (
_world_time.get_season()
if _world_time != null
else CalendarSeason.UNKNOWN
)
if not fish.is_available_in_season(season):
return false
if fish.collection_method != FishDataType.CollectionMethod.FISHING:
return true
for region: FishableWaterRegionType in _get_logbook_water_regions():
if (
region == null
or region.fish_pool == null
or region.fish_pool.get_fish_by_id(fish.id) == null
):
continue
var context := _build_fishing_context(region)
if not fish.is_allowed_in_water(context.water_type):
continue
if fish.availability == null or fish.availability.is_available(context):
return true
return false
func _get_logbook_water_regions() -> Array[FishableWaterRegionType]:
var regions: Array[FishableWaterRegionType] = []
if not _fishable_water_regions_resolver.is_valid():
return regions
var resolved: Variant = _fishable_water_regions_resolver.call()
if resolved is not Array:
return regions
for value: Variant in resolved:
var region := value as FishableWaterRegionType
if region != null and is_instance_valid(region):
regions.append(region)
return regions
func _get_active_bait_tags() -> Array[StringName]: func _get_active_bait_tags() -> Array[StringName]:
if _local_player == null or _item_catalog == null: if _local_player == null or _item_catalog == null:
return [] return []

View file

@ -29,12 +29,16 @@ func configure(
entity_id = configured_entity_id entity_id = configured_entity_id
data = configured_data data = configured_data
type_id = data.type_id if data != null else StringName() type_id = data.type_id if data != null else StringName()
# Establish the authoritative transform before constructing transient visuals.
# Effects emitted before this point would begin at the default world origin.
apply_network_state(position, yaw, true)
if data != null and data.is_stationary_hotspot(): if data != null and data.is_stationary_hotspot():
_ensure_water_spurt_visual() _ensure_water_spurt_visual()
_water_spurt_elapsed = ( _water_spurt_elapsed = (
float(abs(hash(entity_id)) % 1000) / 1000.0 float(abs(hash(entity_id)) % 1000) / 1000.0
* WATER_SPURT_INTERVAL_SECONDS * WATER_SPURT_INTERVAL_SECONDS
) )
_emit_water_spurt()
else: else:
_ensure_visual() _ensure_visual()
if data != null and data.catch_data != null and _sprite != null: if data != null and data.catch_data != null and _sprite != null:
@ -50,7 +54,6 @@ func configure(
) )
) )
_sprite.rotation_degrees.x = data.sprite_tilt_degrees _sprite.rotation_degrees.x = data.sprite_tilt_degrees
apply_network_state(position, yaw, true)
func apply_network_state( func apply_network_state(
@ -147,7 +150,6 @@ func _ensure_water_spurt_visual() -> void:
hole.mesh = hole_mesh hole.mesh = hole_mesh
hole.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF hole.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
_water_spurt_root.add_child(hole) _water_spurt_root.add_child(hole)
_emit_water_spurt()
func _emit_water_spurt() -> void: func _emit_water_spurt() -> void:
@ -159,6 +161,7 @@ func _emit_water_spurt() -> void:
particles.amount = 6 particles.amount = 6
particles.lifetime = 0.42 particles.lifetime = 0.42
particles.one_shot = true particles.one_shot = true
particles.local_coords = true
particles.explosiveness = 1.0 particles.explosiveness = 1.0
particles.direction = Vector3.UP particles.direction = Vector3.UP
particles.spread = 32.0 particles.spread = 32.0

Binary file not shown.

View file

@ -0,0 +1,45 @@
[remap]
importer="scene"
importer_version=1
type="PackedScene"
uid="uid://bhph22caipcj6"
path="res://.godot/imported/starter_motorcoach.glb-153681ef1e622a556fba4df576dc6f84.scn"
[deps]
source_file="res://homes/assets/starter_motorcoach.glb"
dest_files=["res://.godot/imported/starter_motorcoach.glb-153681ef1e622a556fba4df576dc6f84.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

59
homes/decor_catalog.gd Normal file
View file

@ -0,0 +1,59 @@
class_name DecorCatalog
extends RefCounted
const BASIC_CUBE_ID: StringName = &"basic_cube"
const MAX_OWNED_PER_PRODUCT: int = 32
const _PRODUCTS: Dictionary = {
BASIC_CUBE_ID: {
"display_name": "basic cube",
"description": "a simple piece of decor for testing your RV layout.",
"price": 1,
"footprint": Vector2(1.0, 0.8),
"height": 0.8,
},
}
static func get_product_ids() -> Array[StringName]:
var ids: Array[StringName] = []
for value: Variant in _PRODUCTS.keys():
ids.append(StringName(str(value)))
ids.sort_custom(func(first: StringName, second: StringName) -> bool:
return String(first) < String(second)
)
return ids
static func has_product(product_id: StringName) -> bool:
return _PRODUCTS.has(product_id)
static func get_product(product_id: StringName) -> Dictionary:
var value: Variant = _PRODUCTS.get(product_id)
return (
(value as Dictionary).duplicate(true)
if typeof(value) == TYPE_DICTIONARY
else {}
)
static func get_display_name(product_id: StringName) -> String:
return str(get_product(product_id).get("display_name", product_id))
static func get_description(product_id: StringName) -> String:
return str(get_product(product_id).get("description", ""))
static func get_price(product_id: StringName) -> int:
return int(get_product(product_id).get("price", -1))
static func get_footprint(product_id: StringName) -> Vector2:
var value: Variant = get_product(product_id).get("footprint", Vector2.ZERO)
return value as Vector2 if value is Vector2 else Vector2.ZERO
static func get_height(product_id: StringName) -> float:
return float(get_product(product_id).get("height", 0.0))

View file

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

View file

@ -0,0 +1,499 @@
class_name HomeDecorController
extends Node
const DecorCatalogType = preload("res://homes/decor_catalog.gd")
const DRAG_RESPONSE: float = 22.0
const DRAG_POSITION_EPSILON: float = 0.005
var _player: Player
var _hotbar: PlayerHotbar
var _bag: PlayerBag
var _home_state: PlayerHomeState
var _network_home: NetworkHomeService
var _home_world: HomeWorldService
var _fishing_spot: FishingSpot
var _toolbar: HomeDecorToolbar
var _mode: PlayerHomeState.PlacementMode = PlayerHomeState.PlacementMode.GRID
var _selected_instance_id: String = ""
var _drag_instance_id: String = ""
var _drag_owner_fingerprint: String = ""
var _drag_pointer_offset: Vector3 = Vector3.ZERO
var _drag_original_position: Vector3 = Vector3.ZERO
var _drag_original_yaw: float = 0.0
var _drag_target_position: Vector3 = Vector3.ZERO
var _drag_target_yaw: float = 0.0
var _drag_target_valid: bool = false
func setup(
player: Player,
hotbar: PlayerHotbar,
bag: PlayerBag,
home_state: PlayerHomeState,
network_home: NetworkHomeService,
home_world: HomeWorldService,
fishing_spot: FishingSpot,
toolbar: HomeDecorToolbar,
) -> void:
_player = player
_hotbar = hotbar
_bag = bag
_home_state = home_state
_network_home = network_home
_home_world = home_world
_fishing_spot = fishing_spot
_toolbar = toolbar
if not _network_home.local_space_changed.is_connected(
_on_local_space_changed
):
_network_home.local_space_changed.connect(_on_local_space_changed)
if not _network_home.local_manifest_rejected.is_connected(
_on_local_manifest_rejected
):
_network_home.local_manifest_rejected.connect(
_on_local_manifest_rejected
)
if not _home_state.changed.is_connected(_on_home_changed):
_home_state.changed.connect(_on_home_changed)
if not _toolbar.placement_mode_requested.is_connected(
_on_placement_mode_requested
):
_toolbar.placement_mode_requested.connect(
_on_placement_mode_requested
)
_toolbar.rotate_requested.connect(_rotate_selected)
_toolbar.store_requested.connect(_store_selected)
_toolbar.cancel_requested.connect(_clear_selection)
_toolbar.set_mode(_mode)
_refresh_toolbar()
func begin_decorating() -> void:
_refresh_toolbar()
if _toolbar.visible:
_toolbar.report_status(
"use held decor to place it; click or drag placed decor"
)
func _input(event: InputEvent) -> void:
if _drag_instance_id.is_empty():
return
var pointer_motion := event as InputEventMouseMotion
if pointer_motion != null:
_update_drag(pointer_motion.position)
get_viewport().set_input_as_handled()
return
var pointer_button := event as InputEventMouseButton
if (
pointer_button != null
and pointer_button.button_index == MOUSE_BUTTON_LEFT
and not pointer_button.pressed
):
# A gameplay menu can take ownership between the press and release. Do
# not let that release finalize decor behind the menu; preserve the
# original transform instead.
if _can_decorate():
_finish_drag()
else:
_cancel_drag()
get_viewport().set_input_as_handled()
func _unhandled_input(event: InputEvent) -> void:
var pointer_button := event as InputEventMouseButton
if (
pointer_button != null
and pointer_button.button_index == MOUSE_BUTTON_LEFT
):
if not pointer_button.pressed or not _can_decorate():
return
if _begin_drag(pointer_button.position):
get_viewport().set_input_as_handled()
return
var pointer_decor_id: StringName = _get_selected_decor_id()
if (
not pointer_decor_id.is_empty()
and _bag.get_quantity(pointer_decor_id) > 0
):
_place_carried_decor(pointer_decor_id)
get_viewport().set_input_as_handled()
return
if not _can_decorate() or not event.is_action_pressed("fish_primary"):
return
if event is InputEventKey and event.echo:
return
var decor_id: StringName = _get_selected_decor_id()
if not decor_id.is_empty() and _bag.get_quantity(decor_id) > 0:
_place_carried_decor(decor_id)
get_viewport().set_input_as_handled()
func _process(delta: float) -> void:
if _drag_instance_id.is_empty():
return
# Menus and other gameplay input handoffs must cancel an active drag before
# evaluating a held/released pointer. This prevents a same-frame TAB press
# followed by left-button release from committing a move behind the UI.
if not _can_decorate():
_cancel_drag()
return
if not Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT):
_finish_drag()
return
if not _drag_target_valid:
return
var current: Dictionary = _home_world.get_decor_preview_transform(
_drag_owner_fingerprint, _drag_instance_id
)
if current.is_empty():
_cancel_drag()
return
var current_position: Vector3 = current["position"]
var current_yaw: float = float(current["yaw"])
var blend: float = 1.0 - exp(-DRAG_RESPONSE * delta)
_home_world.set_decor_preview_transform(
_drag_owner_fingerprint,
_drag_instance_id,
current_position.lerp(_drag_target_position, blend),
lerp_angle(current_yaw, _drag_target_yaw, blend),
)
func _begin_drag(screen_position: Vector2) -> bool:
if _network_home.has_pending_local_decor_mutation():
_toolbar.report_status("waiting for the previous decor update")
return false
var owner_fingerprint: String = (
_network_home.get_local_home_owner_fingerprint()
)
var camera: Camera3D = _player.get_active_gameplay_camera()
var clicked_instance: String = _home_world.find_decor_instance_at_screen(
owner_fingerprint, camera, screen_position
)
if clicked_instance.is_empty():
return false
var placement: Dictionary = _home_state.get_placement(clicked_instance)
if placement.is_empty():
return false
var original_position: Vector3 = _placement_position(placement)
if not original_position.is_finite():
return false
if not _home_world.set_decor_manipulation_active(
owner_fingerprint, clicked_instance, true
):
return false
var pointer_position: Vector3 = _home_world.get_floor_position_from_screen(
owner_fingerprint, camera, screen_position
)
_drag_instance_id = clicked_instance
_drag_owner_fingerprint = owner_fingerprint
_drag_original_position = original_position
_drag_original_yaw = float(placement.get("yaw", 0.0))
_drag_target_position = original_position
_drag_target_yaw = _drag_original_yaw
_drag_target_valid = true
_drag_pointer_offset = (
original_position - pointer_position
if pointer_position.is_finite()
else Vector3.ZERO
)
_drag_pointer_offset.y = 0.0
_select_instance(clicked_instance)
_toolbar.report_status("drag decor, then release to place it")
return true
func _update_drag(screen_position: Vector2) -> void:
if not _can_decorate():
_cancel_drag()
return
var camera: Camera3D = _player.get_active_gameplay_camera()
var pointer_position: Vector3 = _home_world.get_floor_position_from_screen(
_drag_owner_fingerprint, camera, screen_position
)
if not pointer_position.is_finite():
# Keep the most recent valid target when the pointer briefly leaves the
# RV floor. Releasing there should place at that target, not throw the
# decor all the way back to where the drag began.
_toolbar.report_status("keep the pointer over the RV floor")
return
var placement: Dictionary = _home_state.get_placement(_drag_instance_id)
if placement.is_empty():
_clear_selection()
return
var candidate: Vector3 = pointer_position + _drag_pointer_offset
candidate.y = 0.0
var normalized: Dictionary = _network_home.get_valid_local_decor_transform(
_drag_instance_id,
candidate,
float(placement.get("yaw", _drag_original_yaw)),
_mode,
)
if normalized.is_empty():
# A densely furnished area may have no nearby solution. Preserve the
# last valid preview so the interaction remains stable while the player
# moves the pointer toward open space.
_toolbar.report_status("holding at the nearest available position")
return
_drag_target_position = normalized["position"]
_drag_target_yaw = float(normalized["yaw"])
_drag_target_valid = true
_toolbar.report_status("release to place decor")
func _finish_drag() -> void:
var instance_id: String = _drag_instance_id
var owner_fingerprint: String = _drag_owner_fingerprint
var original_position: Vector3 = _drag_original_position
var original_yaw: float = _drag_original_yaw
var target_position: Vector3 = _drag_target_position
var target_yaw: float = _drag_target_yaw
var target_valid: bool = _drag_target_valid
var placement: Dictionary = _home_state.get_placement(instance_id)
var decor_id := StringName(str(placement.get("decor_id", "")))
if (
not target_valid
or decor_id.is_empty()
or not _home_world.is_decor_placement_clear_of_players(
owner_fingerprint, decor_id, target_position, target_yaw
)
):
_home_world.set_decor_preview_transform(
owner_fingerprint,
instance_id,
original_position,
original_yaw,
)
_home_world.set_decor_manipulation_active(
owner_fingerprint, instance_id, false
)
_reset_drag_state()
_toolbar.report_status(
"a player is in the way"
if target_valid
else "that space is not available"
)
return
_home_world.set_decor_preview_transform(
owner_fingerprint, instance_id, target_position, target_yaw
)
if (
original_position.distance_to(target_position)
<= DRAG_POSITION_EPSILON
and is_zero_approx(angle_difference(original_yaw, target_yaw))
):
_home_world.set_decor_manipulation_active(
owner_fingerprint, instance_id, false
)
_reset_drag_state()
_select_instance(instance_id)
return
if _network_home.update_local_decor(
instance_id, target_position, target_yaw, _mode
):
_home_world.set_decor_manipulation_active(
owner_fingerprint, instance_id, false
)
_reset_drag_state()
_toolbar.report_status("decor moved")
return
_home_world.set_decor_preview_transform(
owner_fingerprint,
instance_id,
original_position,
original_yaw,
)
_home_world.set_decor_manipulation_active(
owner_fingerprint, instance_id, false
)
_reset_drag_state()
_toolbar.report_status("that space is not available")
func _cancel_drag() -> void:
if _drag_instance_id.is_empty():
return
_home_world.set_decor_preview_transform(
_drag_owner_fingerprint,
_drag_instance_id,
_drag_original_position,
_drag_original_yaw,
)
_home_world.set_decor_manipulation_active(
_drag_owner_fingerprint, _drag_instance_id, false
)
_reset_drag_state()
func _reset_drag_state() -> void:
_drag_instance_id = ""
_drag_owner_fingerprint = ""
_drag_pointer_offset = Vector3.ZERO
_drag_original_position = Vector3.ZERO
_drag_original_yaw = 0.0
_drag_target_position = Vector3.ZERO
_drag_target_yaw = 0.0
_drag_target_valid = false
static func _placement_position(placement: Dictionary) -> Vector3:
if placement.is_empty():
return Vector3.INF
var values: Array = placement.get("position", [])
if values.size() != 3:
return Vector3.INF
return Vector3(
float(values[0]), float(values[1]), float(values[2])
)
func _place_carried_decor(decor_id: StringName) -> void:
if _network_home.has_pending_local_decor_mutation():
_toolbar.report_status("waiting for the previous decor update")
return
var owner_fingerprint: String = (
_network_home.get_local_home_owner_fingerprint()
)
# RV transitions keep the gameplay root axis-aligned so its transform can be
# synchronized independently from the presentation. Use the visual facing
# direction instead; otherwise carried decor is always placed along world -Z
# instead of in front of the character the player sees.
var forward: Vector3 = _player.get_facing_direction()
var global_position: Vector3 = _player.global_position + forward * 1.45
var local_position: Vector3 = _home_world.interior_global_to_local(
owner_fingerprint, global_position
)
var placement_yaw: float = atan2(-forward.x, -forward.z)
var instance_id: String = _network_home.place_local_decor(
decor_id, local_position, placement_yaw, _mode
)
if instance_id.is_empty():
_toolbar.report_status("make room in front of you to place this decor")
return
# A local host can reject its own optimistic manifest synchronously. Do not
# claim success after that rollback or leave the player looking for decor that
# was never accepted into the room.
if _home_state.get_placement(instance_id).is_empty():
_toolbar.report_status("decor placement was not accepted")
return
_select_instance(instance_id)
_toolbar.report_status("decor placed")
func _select_instance(instance_id: String) -> void:
var placement: Dictionary = _home_state.get_placement(instance_id)
if placement.is_empty():
_clear_selection()
return
_selected_instance_id = instance_id
var decor_id := StringName(str(placement.get("decor_id", "")))
_toolbar.set_selection(
instance_id, DecorCatalogType.get_display_name(decor_id)
)
func _rotate_selected() -> void:
if not _can_decorate() or _selected_instance_id.is_empty():
return
if _network_home.has_pending_local_decor_mutation():
_toolbar.report_status("waiting for the previous decor update")
return
var placement: Dictionary = _home_state.get_placement(
_selected_instance_id
)
if placement.is_empty():
_clear_selection()
return
var values: Array = placement.get("position", [])
if values.size() != 3:
return
var position := Vector3(
float(values[0]), float(values[1]), float(values[2])
)
if _network_home.update_local_decor(
_selected_instance_id,
position,
float(placement.get("yaw", 0.0)) + PI * 0.5,
_mode,
):
_toolbar.report_status("decor rotated")
else:
_toolbar.report_status("not enough room to rotate here")
func _store_selected() -> void:
if _selected_instance_id.is_empty():
return
if _network_home.has_pending_local_decor_mutation():
_toolbar.report_status("waiting for the previous decor update")
return
if _network_home.remove_local_decor(_selected_instance_id):
_clear_selection()
_toolbar.report_status("decor packed into storage")
else:
_toolbar.report_status("inventory and storage are full")
func _clear_selection() -> void:
_cancel_drag()
_selected_instance_id = ""
if _toolbar != null:
_toolbar.set_selection("")
func _on_placement_mode_requested(mode: int) -> void:
_mode = mode as PlayerHomeState.PlacementMode
func _on_local_space_changed(
_space_id: StringName,
_owner_fingerprint: String,
) -> void:
_clear_selection()
_refresh_toolbar()
func _on_local_manifest_rejected(message: String) -> void:
_clear_selection()
if _toolbar != null and _toolbar.visible:
_toolbar.report_status(message)
func _on_home_changed() -> void:
if (
not _selected_instance_id.is_empty()
and _home_state.get_placement(_selected_instance_id).is_empty()
):
_clear_selection()
_refresh_toolbar()
func _refresh_toolbar() -> void:
if _toolbar == null:
return
_toolbar.set_active(_can_decorate())
if not _toolbar.visible:
_clear_selection()
func _can_decorate() -> bool:
return (
_player != null
and _network_home != null
and _network_home.can_local_decorate()
and _fishing_spot != null
and _fishing_spot.can_open_fishing_shop()
and _player.is_movement_enabled()
)
func _get_selected_decor_id() -> StringName:
if (
_hotbar == null
or _hotbar.get_selected_assignment_kind()
!= PlayerHotbar.AssignmentKind.ITEM
):
return StringName()
var item_id: StringName = _hotbar.get_selected_item_id()
return item_id if DecorCatalogType.has_product(item_id) else StringName()

View file

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

1209
homes/home_world_service.gd Normal file

File diff suppressed because it is too large Load diff

View file

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

786
homes/player_home_state.gd Normal file
View file

@ -0,0 +1,786 @@
class_name PlayerHomeState
extends Node
signal changed
const DecorCatalogType = preload("res://homes/decor_catalog.gd")
const HOME_SCHEMA_VERSION: int = 1
const STARTER_TIER: int = 0
const STARTER_EXTERIOR_ID: StringName = &"starter_motorcoach"
const MAX_TIER: int = 8
const MAX_PLACEMENTS: int = 128
const MAX_INSTANCE_ID_LENGTH: int = 96
const MAX_REVISION: int = 2147483647
const GRID_SIZE: float = 0.5
const FREE_PLACEMENT_SEARCH_STEP: float = 0.1
const FREE_PLACEMENT_SEARCH_ANGLES: int = 24
const NEAREST_PLACEMENT_SEARCH_DISTANCE: float = 2.0
# The starter RV is intentionally cramped. Later RV tiers can replace these
# bounds with larger layouts when tiered interiors are introduced.
# The starter interior is 8.0 m by 4.5 m with 0.18 m walls. These bounds
# follow the walls' inner faces, with a small visual gap to avoid z-fighting.
const INTERIOR_HALF_EXTENTS := Vector2(3.91, 2.16)
const PLACEMENT_EDGE_PADDING: float = 0.02
enum Privacy {
OPEN,
CLOSED,
}
enum PlacementMode {
GRID,
FREE,
}
var _tier: int = STARTER_TIER
var _exterior_id: StringName = STARTER_EXTERIOR_ID
var _privacy: Privacy = Privacy.OPEN
var _owned_decor: Dictionary[StringName, int] = {}
var _placements: Array[Dictionary] = []
var _revision: int = 0
var _next_instance_sequence: int = 1
func get_tier() -> int:
return _tier
func get_exterior_id() -> StringName:
return _exterior_id
func get_privacy() -> Privacy:
return _privacy
func get_revision() -> int:
return _revision
func get_owned_count(decor_id: StringName) -> int:
return int(_owned_decor.get(decor_id, 0))
func get_placed_count(decor_id: StringName) -> int:
var count: int = 0
for placement: Dictionary in _placements:
if StringName(str(placement.get("decor_id", ""))) == decor_id:
count += 1
return count
func get_available_count(decor_id: StringName) -> int:
return maxi(
get_owned_count(decor_id) - get_placed_count(decor_id),
0,
)
func get_owned_decor() -> Dictionary[StringName, int]:
return _owned_decor.duplicate()
func get_placements() -> Array[Dictionary]:
var result: Array[Dictionary] = []
for placement: Dictionary in _placements:
result.append(placement.duplicate(true))
return result
func get_placement(instance_id: String) -> Dictionary:
var index: int = _placement_index(instance_id)
return _placements[index].duplicate(true) if index >= 0 else {}
func can_place_decor(
decor_id: StringName,
position: Vector3,
yaw_radians: float,
mode: PlacementMode,
) -> bool:
if (
get_available_count(decor_id) <= 0
or _placements.size() >= MAX_PLACEMENTS
or not _valid_mode(mode)
):
return false
return not get_nearest_valid_placement_transform(
decor_id,
position,
yaw_radians,
mode,
"",
).is_empty()
func set_privacy(value: Privacy) -> bool:
if value < Privacy.OPEN or value > Privacy.CLOSED:
return false
if _privacy == value:
return true
_privacy = value
_touch()
return true
func add_owned_decor(
decor_id: StringName,
quantity: int = 1,
) -> bool:
if (
not DecorCatalogType.has_product(decor_id)
or quantity <= 0
):
return false
var current: int = get_owned_count(decor_id)
if current + quantity > DecorCatalogType.MAX_OWNED_PER_PRODUCT:
return false
_owned_decor[decor_id] = current + quantity
_touch()
return true
func remove_owned_decor(
decor_id: StringName,
quantity: int = 1,
) -> bool:
if quantity <= 0:
return false
var current: int = get_owned_count(decor_id)
if current - quantity < get_placed_count(decor_id):
return false
var remaining: int = current - quantity
if remaining < 0:
return false
if remaining == 0:
_owned_decor.erase(decor_id)
else:
_owned_decor[decor_id] = remaining
_touch()
return true
func place_decor(
decor_id: StringName,
position: Vector3,
yaw_radians: float,
mode: PlacementMode,
) -> String:
if (
get_available_count(decor_id) <= 0
or _placements.size() >= MAX_PLACEMENTS
or not _valid_mode(mode)
):
return ""
var normalized := get_nearest_valid_placement_transform(
decor_id,
position,
yaw_radians,
mode,
"",
)
if normalized.is_empty():
return ""
var instance_id := _make_instance_id()
var placement: Dictionary = {
"instance_id": instance_id,
"decor_id": String(decor_id),
"mode": int(mode),
"position": _vector3_to_array(normalized["position"]),
"yaw": float(normalized["yaw"]),
}
_placements.append(placement)
_touch()
return instance_id
func update_placement(
instance_id: String,
position: Vector3,
yaw_radians: float,
mode: PlacementMode,
) -> bool:
var index: int = _placement_index(instance_id)
var normalized: Dictionary = get_valid_update_transform(
instance_id, position, yaw_radians, mode
)
if index < 0 or normalized.is_empty():
return false
var placement: Dictionary = _placements[index].duplicate(true)
placement["mode"] = int(mode)
placement["position"] = _vector3_to_array(normalized["position"])
placement["yaw"] = float(normalized["yaw"])
_placements[index] = placement
_touch()
return true
func get_valid_update_transform(
instance_id: String,
position: Vector3,
yaw_radians: float,
mode: PlacementMode,
candidate_validator: Callable = Callable(),
) -> Dictionary:
var index: int = _placement_index(instance_id)
if index < 0 or not _valid_mode(mode):
return {}
var decor_id := StringName(
str(_placements[index].get("decor_id", ""))
)
return get_nearest_valid_placement_transform(
decor_id,
position,
yaw_radians,
mode,
instance_id,
candidate_validator,
)
func get_nearest_valid_placement_transform(
decor_id: StringName,
position: Vector3,
yaw_radians: float,
mode: PlacementMode,
ignored_instance_id: String = "",
candidate_validator: Callable = Callable(),
) -> Dictionary:
var normalized: Dictionary = normalized_placement_transform(
decor_id, position, yaw_radians, mode
)
if normalized.is_empty():
return {}
if (
not _overlaps_existing(
decor_id, normalized, ignored_instance_id
)
and _candidate_is_accepted(normalized, candidate_validator)
):
return normalized
return (
_find_nearest_grid_transform(
decor_id,
normalized,
ignored_instance_id,
candidate_validator,
)
if mode == PlacementMode.GRID
else _find_nearest_free_transform(
decor_id,
normalized,
ignored_instance_id,
candidate_validator,
)
)
func remove_placement(instance_id: String) -> bool:
var index: int = _placement_index(instance_id)
if index < 0:
return false
_placements.remove_at(index)
_touch()
return true
func reset_to_defaults() -> void:
_tier = STARTER_TIER
_exterior_id = STARTER_EXTERIOR_ID
_privacy = Privacy.OPEN
_owned_decor.clear()
_placements.clear()
_revision = 0
_next_instance_sequence = 1
changed.emit()
func to_save_data() -> Dictionary:
var owned: Dictionary = {}
var ids: Array[StringName] = []
ids.assign(_owned_decor.keys())
ids.sort_custom(func(first: StringName, second: StringName) -> bool:
return String(first) < String(second)
)
for decor_id: StringName in ids:
owned[String(decor_id)] = _owned_decor[decor_id]
return {
"schema_version": HOME_SCHEMA_VERSION,
"tier": _tier,
"exterior_id": String(_exterior_id),
"privacy": int(_privacy),
"owned_decor": owned,
"placements": get_placements(),
"revision": _revision,
"next_instance_sequence": _next_instance_sequence,
}
func restore_from_save_data(data: Dictionary) -> bool:
var sanitized: Dictionary = sanitize_save_data(data)
if sanitized.is_empty():
return false
_tier = int(sanitized["tier"])
_exterior_id = StringName(str(sanitized["exterior_id"]))
_privacy = int(sanitized["privacy"])
_owned_decor.clear()
for key: Variant in (sanitized["owned_decor"] as Dictionary):
_owned_decor[StringName(str(key))] = int(
(sanitized["owned_decor"] as Dictionary)[key]
)
_placements.clear()
for value: Variant in sanitized["placements"] as Array:
_placements.append((value as Dictionary).duplicate(true))
_revision = int(sanitized["revision"])
_next_instance_sequence = int(sanitized["next_instance_sequence"])
changed.emit()
return true
static func default_save_data() -> Dictionary:
return {
"schema_version": HOME_SCHEMA_VERSION,
"tier": STARTER_TIER,
"exterior_id": String(STARTER_EXTERIOR_ID),
"privacy": int(Privacy.OPEN),
"owned_decor": {},
"placements": [],
"revision": 0,
"next_instance_sequence": 1,
}
static func sanitize_save_data(data: Dictionary) -> Dictionary:
if (
not _is_integer_value(data.get("schema_version"))
or int(data.get("schema_version", -1)) != HOME_SCHEMA_VERSION
or not _is_integer_value(data.get("tier"))
or typeof(data.get("exterior_id")) not in [
TYPE_STRING, TYPE_STRING_NAME,
]
or not _is_integer_value(data.get("privacy"))
or typeof(data.get("owned_decor")) != TYPE_DICTIONARY
or typeof(data.get("placements")) != TYPE_ARRAY
or not _is_integer_value(data.get("revision"))
or not _is_integer_value(data.get("next_instance_sequence"))
):
return {}
var tier: int = int(data["tier"])
var exterior_id: String = str(data["exterior_id"])
var privacy: int = int(data["privacy"])
var revision: int = int(data["revision"])
var next_sequence: int = int(data["next_instance_sequence"])
if (
tier < STARTER_TIER
or tier > MAX_TIER
or exterior_id.is_empty()
or exterior_id.length() > 96
or privacy < Privacy.OPEN
or privacy > Privacy.CLOSED
or revision < 0
or revision > MAX_REVISION
or next_sequence < 1
or next_sequence > MAX_REVISION
):
return {}
var sanitized_owned: Dictionary = {}
var owned: Dictionary = data["owned_decor"]
for key: Variant in owned:
if typeof(key) not in [TYPE_STRING, TYPE_STRING_NAME]:
return {}
var decor_id := StringName(str(key))
var quantity_value: Variant = owned[key]
if (
not DecorCatalogType.has_product(decor_id)
or not _is_integer_value(quantity_value)
or int(quantity_value) <= 0
or int(quantity_value) > DecorCatalogType.MAX_OWNED_PER_PRODUCT
):
return {}
sanitized_owned[String(decor_id)] = int(quantity_value)
var values: Array = data["placements"]
if values.size() > MAX_PLACEMENTS:
return {}
var sanitized_placements: Array[Dictionary] = []
var seen_instance_ids: Dictionary[String, bool] = {}
var placed_counts: Dictionary[StringName, int] = {}
for value: Variant in values:
if typeof(value) != TYPE_DICTIONARY:
return {}
var sanitized: Dictionary = sanitize_placement(value)
if sanitized.is_empty():
return {}
var instance_id: String = str(sanitized["instance_id"])
var decor_id := StringName(str(sanitized["decor_id"]))
if seen_instance_ids.has(instance_id):
return {}
seen_instance_ids[instance_id] = true
placed_counts[decor_id] = int(
placed_counts.get(decor_id, 0)
) + 1
if (
int(placed_counts[decor_id])
> int(sanitized_owned.get(String(decor_id), 0))
):
return {}
for previous: Dictionary in sanitized_placements:
if placements_overlap(previous, sanitized):
return {}
sanitized_placements.append(sanitized)
return {
"schema_version": HOME_SCHEMA_VERSION,
"tier": tier,
"exterior_id": exterior_id,
"privacy": privacy,
"owned_decor": sanitized_owned,
"placements": sanitized_placements,
"revision": revision,
"next_instance_sequence": next_sequence,
}
static func sanitize_placement(value: Dictionary) -> Dictionary:
for key: String in [
"instance_id", "decor_id", "mode", "position", "yaw",
]:
if not value.has(key):
return {}
if (
typeof(value["instance_id"]) != TYPE_STRING
or typeof(value["decor_id"]) not in [
TYPE_STRING, TYPE_STRING_NAME,
]
or not _is_integer_value(value["mode"])
or typeof(value["position"]) != TYPE_ARRAY
or typeof(value["yaw"]) not in [TYPE_FLOAT, TYPE_INT]
):
return {}
var instance_id: String = value["instance_id"]
var decor_id := StringName(str(value["decor_id"]))
var mode: int = int(value["mode"])
var position_values: Array = value["position"]
var yaw: float = float(value["yaw"])
if (
instance_id.is_empty()
or instance_id.length() > MAX_INSTANCE_ID_LENGTH
or not DecorCatalogType.has_product(decor_id)
or not _valid_mode(mode)
or position_values.size() != 3
or not is_finite(yaw)
):
return {}
for component: Variant in position_values:
if typeof(component) not in [TYPE_FLOAT, TYPE_INT]:
return {}
var position := Vector3(
float(position_values[0]),
float(position_values[1]),
float(position_values[2]),
)
var normalized := normalized_placement_transform(
decor_id,
position,
yaw,
mode as PlacementMode,
)
if normalized.is_empty():
return {}
return {
"instance_id": instance_id,
"decor_id": String(decor_id),
"mode": mode,
"position": _vector3_to_array(normalized["position"]),
"yaw": float(normalized["yaw"]),
}
static func _is_integer_value(value: Variant) -> bool:
if typeof(value) == TYPE_INT:
return true
if typeof(value) != TYPE_FLOAT:
return false
var number: float = float(value)
return is_finite(number) and is_equal_approx(number, roundf(number))
static func normalized_placement_transform(
decor_id: StringName,
position: Vector3,
yaw_radians: float,
mode: PlacementMode,
) -> Dictionary:
if (
not DecorCatalogType.has_product(decor_id)
or not position.is_finite()
or not is_finite(yaw_radians)
or not _valid_mode(mode)
):
return {}
var normalized_position := Vector3(position.x, 0.0, position.z)
var normalized_yaw: float = wrapf(yaw_radians, -PI, PI)
if mode == PlacementMode.GRID:
normalized_position.x = snappedf(normalized_position.x, GRID_SIZE)
normalized_position.z = snappedf(normalized_position.z, GRID_SIZE)
normalized_yaw = snappedf(normalized_yaw, PI * 0.5)
var footprint: Vector2 = DecorCatalogType.get_footprint(decor_id)
var cosine: float = absf(cos(normalized_yaw))
var sine: float = absf(sin(normalized_yaw))
var half_size := Vector2(
(footprint.x * cosine + footprint.y * sine) * 0.5,
(footprint.x * sine + footprint.y * cosine) * 0.5,
)
var maximum_x: float = (
INTERIOR_HALF_EXTENTS.x - half_size.x - PLACEMENT_EDGE_PADDING
)
var maximum_z: float = (
INTERIOR_HALF_EXTENTS.y - half_size.y - PLACEMENT_EDGE_PADDING
)
if maximum_x < 0.0 or maximum_z < 0.0:
return {}
# Clamp after grid snapping so a grid-placed item can still sit flush at a
# wall even when the wall itself is not on an exact grid interval.
normalized_position.x = clampf(
normalized_position.x, -maximum_x, maximum_x
)
normalized_position.z = clampf(
normalized_position.z, -maximum_z, maximum_z
)
return {
"position": normalized_position,
"yaw": normalized_yaw,
}
func _find_nearest_grid_transform(
decor_id: StringName,
origin: Dictionary,
ignored_instance_id: String,
candidate_validator: Callable,
) -> Dictionary:
var origin_position: Vector3 = origin["position"]
var yaw: float = float(origin["yaw"])
var maximum_ring: int = ceili(
NEAREST_PLACEMENT_SEARCH_DISTANCE / GRID_SIZE
)
var seen: Dictionary[String, bool] = {}
for ring: int in range(1, maximum_ring + 1):
var nearest: Dictionary = {}
var nearest_distance_squared: float = INF
for x_offset: int in range(-ring, ring + 1):
for z_offset: int in range(-ring, ring + 1):
if maxi(absi(x_offset), absi(z_offset)) != ring:
continue
var candidate: Dictionary = normalized_placement_transform(
decor_id,
origin_position + Vector3(
float(x_offset) * GRID_SIZE,
0.0,
float(z_offset) * GRID_SIZE,
),
yaw,
PlacementMode.GRID,
)
if candidate.is_empty() or not _mark_candidate_unseen(
candidate, seen
):
continue
if _overlaps_existing(
decor_id, candidate, ignored_instance_id
) or not _candidate_is_accepted(
candidate, candidate_validator
):
continue
var candidate_position: Vector3 = candidate["position"]
var distance_squared: float = origin_position.distance_squared_to(
candidate_position
)
if distance_squared < nearest_distance_squared:
nearest = candidate
nearest_distance_squared = distance_squared
if not nearest.is_empty():
return nearest
return {}
func _find_nearest_free_transform(
decor_id: StringName,
origin: Dictionary,
ignored_instance_id: String,
candidate_validator: Callable,
) -> Dictionary:
var origin_position: Vector3 = origin["position"]
var yaw: float = float(origin["yaw"])
var maximum_ring: int = ceili(
NEAREST_PLACEMENT_SEARCH_DISTANCE / FREE_PLACEMENT_SEARCH_STEP
)
var seen: Dictionary[String, bool] = {}
for ring: int in range(1, maximum_ring + 1):
var radius: float = float(ring) * FREE_PLACEMENT_SEARCH_STEP
var nearest: Dictionary = {}
var nearest_distance_squared: float = INF
for angle_index: int in FREE_PLACEMENT_SEARCH_ANGLES:
var angle: float = (
TAU * float(angle_index) / float(FREE_PLACEMENT_SEARCH_ANGLES)
)
var candidate: Dictionary = normalized_placement_transform(
decor_id,
origin_position + Vector3(
cos(angle) * radius, 0.0, sin(angle) * radius
),
yaw,
PlacementMode.FREE,
)
if candidate.is_empty() or not _mark_candidate_unseen(
candidate, seen
):
continue
if _overlaps_existing(
decor_id, candidate, ignored_instance_id
) or not _candidate_is_accepted(
candidate, candidate_validator
):
continue
var candidate_position: Vector3 = candidate["position"]
var distance_squared: float = origin_position.distance_squared_to(
candidate_position
)
if distance_squared < nearest_distance_squared:
nearest = candidate
nearest_distance_squared = distance_squared
if not nearest.is_empty():
return nearest
return {}
static func _mark_candidate_unseen(
candidate: Dictionary,
seen: Dictionary[String, bool],
) -> bool:
var position: Vector3 = candidate["position"]
var key := "%d:%d" % [
roundi(position.x * 10000.0),
roundi(position.z * 10000.0),
]
if seen.has(key):
return false
seen[key] = true
return true
static func _candidate_is_accepted(
candidate: Dictionary,
validator: Callable,
) -> bool:
return not validator.is_valid() or bool(validator.call(candidate))
static func placements_overlap(first: Dictionary, second: Dictionary) -> bool:
var first_id := StringName(str(first.get("decor_id", "")))
var second_id := StringName(str(second.get("decor_id", "")))
if (
not DecorCatalogType.has_product(first_id)
or not DecorCatalogType.has_product(second_id)
):
return true
var first_position: Vector3 = _array_to_vector3(first.get("position", []))
var second_position: Vector3 = _array_to_vector3(second.get("position", []))
var first_yaw: float = float(first.get("yaw", 0.0))
var second_yaw: float = float(second.get("yaw", 0.0))
var first_half: Vector2 = (
DecorCatalogType.get_footprint(first_id) * 0.5
)
var second_half: Vector2 = (
DecorCatalogType.get_footprint(second_id) * 0.5
)
var first_axes: Array[Vector2] = _placement_axes(first_yaw)
var second_axes: Array[Vector2] = _placement_axes(second_yaw)
var delta := Vector2(
second_position.x - first_position.x,
second_position.z - first_position.z,
)
for axis: Vector2 in [
first_axes[0], first_axes[1], second_axes[0], second_axes[1],
]:
var first_radius: float = (
absf(axis.dot(first_axes[0])) * first_half.x
+ absf(axis.dot(first_axes[1])) * first_half.y
)
var second_radius: float = (
absf(axis.dot(second_axes[0])) * second_half.x
+ absf(axis.dot(second_axes[1])) * second_half.y
)
if absf(delta.dot(axis)) >= (
first_radius + second_radius - 0.001
):
return false
return true
func _make_instance_id() -> String:
var result := "decor-%d-%d" % [
Time.get_ticks_usec(),
_next_instance_sequence,
]
_next_instance_sequence = mini(
_next_instance_sequence + 1,
MAX_REVISION,
)
return result
func _placement_index(instance_id: String) -> int:
if instance_id.is_empty():
return -1
for index: int in _placements.size():
if str(_placements[index].get("instance_id", "")) == instance_id:
return index
return -1
func _overlaps_existing(
decor_id: StringName,
normalized: Dictionary,
ignored_instance_id: String,
) -> bool:
var candidate: Dictionary = {
"decor_id": String(decor_id),
"position": _vector3_to_array(normalized["position"]),
"yaw": float(normalized["yaw"]),
}
for placement: Dictionary in _placements:
if str(placement.get("instance_id", "")) == ignored_instance_id:
continue
if placements_overlap(candidate, placement):
return true
return false
func _touch() -> void:
_revision = mini(_revision + 1, MAX_REVISION)
changed.emit()
static func _valid_mode(value: int) -> bool:
return value >= PlacementMode.GRID and value <= PlacementMode.FREE
static func _vector3_to_array(value: Vector3) -> Array[float]:
return [value.x, value.y, value.z]
static func _array_to_vector3(value: Variant) -> Vector3:
if typeof(value) != TYPE_ARRAY or (value as Array).size() != 3:
return Vector3.INF
var components: Array = value
return Vector3(
float(components[0]),
float(components[1]),
float(components[2]),
)
static func _placement_axes(yaw: float) -> Array[Vector2]:
var cosine: float = cos(yaw)
var sine: float = sin(yaw)
return [Vector2(cosine, sine), Vector2(-sine, cosine)]

View file

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

View file

@ -60,6 +60,28 @@ func add_item(item_id: StringName, quantity: int = 1) -> bool:
return true return true
func add_item_to_storage(item_id: StringName, quantity: int = 1) -> bool:
var existing: OwnedItemType = get_owned_item(item_id)
if existing != null:
return add_item(item_id, quantity)
var item: ItemDataType = _resolve_available_item(item_id)
if (
item == null
or quantity <= 0
or quantity > (item.max_stack if item.stackable else 1)
or _inventory_layout == null
or not _inventory_layout.can_accept_item_in_storage(item_id)
):
return false
var owned := OwnedItemType.new()
owned.item_id = item_id
owned.quantity = quantity
owned.storage_slot = _first_available_storage_slot(item, _items)
_items.append(owned)
contents_changed.emit()
return true
func can_add_item(item_id: StringName, quantity: int = 1) -> bool: func can_add_item(item_id: StringName, quantity: int = 1) -> bool:
var item: ItemDataType = _resolve_available_item(item_id) var item: ItemDataType = _resolve_available_item(item_id)
if item == null or quantity <= 0: if item == null or quantity <= 0:

View file

@ -314,6 +314,25 @@ func get_fish_slots() -> Array[StringName]:
return _fish_slots.duplicate() return _fish_slots.duplicate()
func find_item_slot(item_id: StringName) -> int:
return _slots.find(item_id)
func find_first_empty_slot() -> int:
for slot_index: int in SLOT_COUNT:
if get_assignment_kind(slot_index) != AssignmentKind.EMPTY:
continue
if (
_inventory_layout == null
or _inventory_layout.get_key_at(
PlayerInventoryLayout.InventoryContainer.HOTBAR,
slot_index,
).is_empty()
):
return slot_index
return -1
func replace_state( func replace_state(
slots: Array[StringName], slots: Array[StringName],
selected_slot: int, selected_slot: int,

View file

@ -37,6 +37,7 @@ var _fish_inventory: FishInventoryType
var _item_catalog: ItemCatalogType var _item_catalog: ItemCatalogType
var _storage_capacity: PlayerCoolerCapacityType var _storage_capacity: PlayerCoolerCapacityType
var _placements: Dictionary[String, Dictionary] = {} var _placements: Dictionary[String, Dictionary] = {}
var _prepared_item_placements: Dictionary[String, Dictionary] = {}
var _reconciling: bool = false var _reconciling: bool = false
var _backpack_level: int = 0 var _backpack_level: int = 0
@ -72,7 +73,44 @@ func can_accept_item(item_id: StringName) -> bool:
if not is_managed_item(item_id): if not is_managed_item(item_id):
return true return true
var key := item_key(item_id) var key := item_key(item_id)
return _placements.has(key) or _first_free_slot(InventoryContainer.INVENTORY) >= 0 return (
_placements.has(key)
or _is_prepared_item_placement_available(key)
or _first_free_slot(InventoryContainer.INVENTORY) >= 0
)
func prepare_new_item_placement(
item_id: StringName,
target_container: InventoryContainer,
target_slot: int,
) -> bool:
if not is_managed_item(item_id):
return false
var key := item_key(item_id)
if (
_placements.has(key)
or not _is_slot_valid(target_container, target_slot)
or not _key_at(target_container, target_slot).is_empty()
or _prepared_slot_is_occupied(target_container, target_slot)
):
return false
_prepared_item_placements[key] = {
"container": int(target_container),
"slot": target_slot,
}
return true
func cancel_prepared_item_placement(item_id: StringName) -> void:
_prepared_item_placements.erase(item_key(item_id))
func can_accept_item_in_storage(item_id: StringName) -> bool:
if not is_managed_item(item_id):
return true
var key := item_key(item_id)
return _placements.has(key) or _first_free_slot(InventoryContainer.STORAGE) >= 0
func can_accept_catch(catch_id: StringName = StringName()) -> bool: func can_accept_catch(catch_id: StringName = StringName()) -> bool:
@ -332,6 +370,7 @@ func restore_from_save_data(data: Dictionary) -> bool:
"container": container, "container": container,
"slot": slot, "slot": slot,
} }
_prepared_item_placements.clear()
_placements = restored _placements = restored
_reconcile(true) _reconcile(true)
return true return true
@ -340,6 +379,7 @@ func restore_from_save_data(data: Dictionary) -> bool:
func reset_to_defaults() -> void: func reset_to_defaults() -> void:
var capacity_changed := _backpack_level != 0 var capacity_changed := _backpack_level != 0
_backpack_level = 0 _backpack_level = 0
_prepared_item_placements.clear()
_placements.clear() _placements.clear()
_reconcile(true) _reconcile(true)
if capacity_changed: if capacity_changed:
@ -402,8 +442,17 @@ func _reconcile(force_signal: bool) -> void:
for key: String in expected: for key: String in expected:
if _placements.has(key): if _placements.has(key):
continue continue
var prepared: Dictionary = _prepared_item_placements.get(key, {})
var target_container := InventoryContainer.INVENTORY var target_container := InventoryContainer.INVENTORY
var target_slot := _first_free_slot(target_container) var target_slot: int = -1
if _is_prepared_item_placement_available(key):
target_container = int(
prepared["container"]
) as InventoryContainer
target_slot = int(prepared["slot"])
_prepared_item_placements.erase(key)
if target_slot < 0:
target_slot = _first_free_slot(target_container)
if target_slot < 0: if target_slot < 0:
target_container = InventoryContainer.STORAGE target_container = InventoryContainer.STORAGE
target_slot = _first_free_slot(target_container) target_slot = _first_free_slot(target_container)
@ -484,6 +533,37 @@ func _first_free_slot(container: InventoryContainer) -> int:
return -1 return -1
func _is_prepared_item_placement_available(key: String) -> bool:
var placement: Dictionary = _prepared_item_placements.get(key, {})
if placement.is_empty():
return false
var container: int = int(placement.get("container", -1))
var slot: int = int(placement.get("slot", -1))
if (
container not in [
InventoryContainer.INVENTORY,
InventoryContainer.STORAGE,
InventoryContainer.HOTBAR,
]
or not _is_slot_valid(container as InventoryContainer, slot)
):
return false
return _key_at(container as InventoryContainer, slot).is_empty()
func _prepared_slot_is_occupied(
container: InventoryContainer,
slot: int,
) -> bool:
for placement: Dictionary in _prepared_item_placements.values():
if (
int(placement.get("container", -1)) == int(container)
and int(placement.get("slot", -1)) == slot
):
return true
return false
func _key_at(container: InventoryContainer, slot: int) -> String: func _key_at(container: InventoryContainer, slot: int) -> String:
for key: String in _placements: for key: String in _placements:
var placement: Dictionary = _placements[key] var placement: Dictionary = _placements[key]

View file

@ -0,0 +1,17 @@
[gd_resource type="Resource" script_class="ItemData" load_steps=3 format=3]
[ext_resource type="Script" path="res://items/item_data.gd" id="1_item"]
[ext_resource type="Texture2D" path="res://ui/icons/pictograms/x_light.png" id="2_icon"]
[resource]
script = ExtResource("1_item")
item_id = &"basic_cube"
display_name = "basic cube"
description = "a simple piece of decor for testing your RV layout."
category = 6
icon = ExtResource("2_icon")
stackable = true
max_stack = 32
usable = false
equippable = true
hotbar_allowed = true

View file

@ -1,4 +1,4 @@
[gd_resource type="Resource" script_class="ItemCatalog" load_steps=41 format=3] [gd_resource type="Resource" script_class="ItemCatalog" load_steps=42 format=3]
[ext_resource type="Script" path="res://items/item_catalog.gd" id="1_script"] [ext_resource type="Script" path="res://items/item_catalog.gd" id="1_script"]
[ext_resource type="Resource" path="res://items/catalog/basic_fishing_rod.tres" id="2_rod"] [ext_resource type="Resource" path="res://items/catalog/basic_fishing_rod.tres" id="2_rod"]
@ -40,7 +40,8 @@
[ext_resource type="Resource" path="res://items/catalog/crab_net.tres" id="39_crab_net"] [ext_resource type="Resource" path="res://items/catalog/crab_net.tres" id="39_crab_net"]
[ext_resource type="Resource" path="res://items/catalog/standard_shovel.tres" id="40_shovel"] [ext_resource type="Resource" path="res://items/catalog/standard_shovel.tres" id="40_shovel"]
[ext_resource type="Resource" path="res://items/catalog/fishing_net.tres" id="41_fishing_net"] [ext_resource type="Resource" path="res://items/catalog/fishing_net.tres" id="41_fishing_net"]
[ext_resource type="Resource" path="res://items/catalog/basic_cube.tres" id="42_basic_cube"]
[resource] [resource]
script = ExtResource("1_script") script = ExtResource("1_script")
items = [ExtResource("2_rod"), ExtResource("3_coffee"), ExtResource("4_energy"), ExtResource("5_snack"), ExtResource("6_finder"), ExtResource("8_art_kit"), ExtResource("9_worms"), ExtResource("10_snails"), ExtResource("11_shrimp"), ExtResource("12_squid"), ExtResource("14_sardine"), ExtResource("13_anchovy"), ExtResource("15_roe"), ExtResource("16_standby"), ExtResource("17_batteries"), ExtResource("18_cardboard"), ExtResource("19_pond"), ExtResource("20_river"), ExtResource("21_lake"), ExtResource("22_salt"), ExtResource("23_whisker"), ExtResource("24_reef"), ExtResource("25_moonbeam"), ExtResource("26_sun"), ExtResource("27_rain"), ExtResource("28_fog"), ExtResource("29_guide"), ExtResource("30_small"), ExtResource("31_heavy"), ExtResource("32_rocket"), ExtResource("33_lucky"), ExtResource("34_showboat"), ExtResource("35_deep"), ExtResource("36_oddity"), ExtResource("37_aurora"), ExtResource("38_magnet"), ExtResource("39_crab_net"), ExtResource("40_shovel"), ExtResource("41_fishing_net")] items = [ExtResource("2_rod"), ExtResource("3_coffee"), ExtResource("4_energy"), ExtResource("5_snack"), ExtResource("6_finder"), ExtResource("8_art_kit"), ExtResource("9_worms"), ExtResource("10_snails"), ExtResource("11_shrimp"), ExtResource("12_squid"), ExtResource("14_sardine"), ExtResource("13_anchovy"), ExtResource("15_roe"), ExtResource("16_standby"), ExtResource("17_batteries"), ExtResource("18_cardboard"), ExtResource("19_pond"), ExtResource("20_river"), ExtResource("21_lake"), ExtResource("22_salt"), ExtResource("23_whisker"), ExtResource("24_reef"), ExtResource("25_moonbeam"), ExtResource("26_sun"), ExtResource("27_rain"), ExtResource("28_fog"), ExtResource("29_guide"), ExtResource("30_small"), ExtResource("31_heavy"), ExtResource("32_rocket"), ExtResource("33_lucky"), ExtResource("34_showboat"), ExtResource("35_deep"), ExtResource("36_oddity"), ExtResource("37_aurora"), ExtResource("38_magnet"), ExtResource("39_crab_net"), ExtResource("40_shovel"), ExtResource("41_fishing_net"), ExtResource("42_basic_cube")]

View file

@ -37,6 +37,9 @@ const FishingShopType = preload("res://ui/fishing_shop.gd")
const FishingShopInteractionType = preload( const FishingShopInteractionType = preload(
"res://world/fishing_shop_interaction.gd" "res://world/fishing_shop_interaction.gd"
) )
const DecorShopInteractionType = preload(
"res://world/decor_shop_interaction.gd"
)
const PlayerStorageInteractionType = preload( const PlayerStorageInteractionType = preload(
"res://world/player_storage_interaction.gd" "res://world/player_storage_interaction.gd"
) )
@ -70,6 +73,7 @@ const NetworkFishingServiceType = preload(
"res://network/network_fishing_service.gd" "res://network/network_fishing_service.gd"
) )
const PlayerHotbarType = preload("res://inventory/player_hotbar.gd") const PlayerHotbarType = preload("res://inventory/player_hotbar.gd")
const DecorCatalogType = preload("res://homes/decor_catalog.gd")
const NetworkSaleServiceType = preload( const NetworkSaleServiceType = preload(
"res://network/network_sale_service.gd" "res://network/network_sale_service.gd"
) )
@ -123,6 +127,14 @@ const GatheringControllerType = preload(
const NetworkWorldSpawnServiceType = preload( const NetworkWorldSpawnServiceType = preload(
"res://network/network_world_spawn_service.gd" "res://network/network_world_spawn_service.gd"
) )
const PlayerHomeStateType = preload("res://homes/player_home_state.gd")
const HomeWorldServiceType = preload("res://homes/home_world_service.gd")
const NetworkHomeServiceType = preload(
"res://network/network_home_service.gd"
)
const HomeDecorControllerType = preload(
"res://homes/home_decor_controller.gd"
)
const TITLE_MUSIC_SILENCE_DB: float = -80.0 const TITLE_MUSIC_SILENCE_DB: float = -80.0
const TITLE_MUSIC_PATH: String = ( const TITLE_MUSIC_PATH: String = (
@ -153,6 +165,7 @@ const DEDICATED_ACTIVE_PHYSICS_TICKS_PER_SECOND: int = 30
@onready var _game_ui: GameUIType = %GameUI @onready var _game_ui: GameUIType = %GameUI
@onready var _water_recovery: WaterRecoveryControllerType = %WaterRecovery @onready var _water_recovery: WaterRecoveryControllerType = %WaterRecovery
@onready var _save_manager: PlayerSaveManagerType = %PlayerSaveManager @onready var _save_manager: PlayerSaveManagerType = %PlayerSaveManager
@onready var _home_state: PlayerHomeStateType = %PlayerHomeState
@onready var _settings_manager: PlayerSettingsManagerType = %PlayerSettingsManager @onready var _settings_manager: PlayerSettingsManagerType = %PlayerSettingsManager
@onready var _controller_mapping_manager: ControllerMappingManagerType = ( @onready var _controller_mapping_manager: ControllerMappingManagerType = (
%ControllerMappingManager %ControllerMappingManager
@ -212,6 +225,10 @@ const DEDICATED_ACTIVE_PHYSICS_TICKS_PER_SECOND: int = 30
) )
@onready var _network_sale: NetworkSaleServiceType = %NetworkSaleService @onready var _network_sale: NetworkSaleServiceType = %NetworkSaleService
@onready var _network_shop: NetworkShopServiceType = %NetworkShopService @onready var _network_shop: NetworkShopServiceType = %NetworkShopService
@onready var _network_home: NetworkHomeServiceType = %NetworkHomeService
@onready var _home_decor_controller: HomeDecorControllerType = (
%HomeDecorController
)
@onready var _network_item_use: NetworkItemUseServiceType = ( @onready var _network_item_use: NetworkItemUseServiceType = (
%NetworkItemUseService %NetworkItemUseService
) )
@ -234,6 +251,7 @@ const DEDICATED_ACTIVE_PHYSICS_TICKS_PER_SECOND: int = 30
@onready var _players_root: Node3D = $Players @onready var _players_root: Node3D = $Players
@onready var _surface_drawings_root: Node3D = $SurfaceDrawings @onready var _surface_drawings_root: Node3D = $SurfaceDrawings
@onready var _world_gatherables_root: Node3D = $WorldGatherables @onready var _world_gatherables_root: Node3D = $WorldGatherables
@onready var _home_world: HomeWorldServiceType = %HomeWorldService
@onready var _title_background: ColorRect = %TitleBackground @onready var _title_background: ColorRect = %TitleBackground
@onready var _player_menu_backdrop: ColorRect = %PlayerMenuBackdrop @onready var _player_menu_backdrop: ColorRect = %PlayerMenuBackdrop
@onready var _shop_backdrop: ColorRect = %ShopBackdrop @onready var _shop_backdrop: ColorRect = %ShopBackdrop
@ -241,6 +259,7 @@ const DEDICATED_ACTIVE_PHYSICS_TICKS_PER_SECOND: int = 30
var _gameplay_started: bool = false var _gameplay_started: bool = false
var _save_slots := PlayerSaveSlotCatalogType.new() var _save_slots := PlayerSaveSlotCatalogType.new()
var _shop_interaction: FishingShopInteractionType var _shop_interaction: FishingShopInteractionType
var _decor_shop_interaction: DecorShopInteractionType
var _storage_interaction: PlayerStorageInteractionType var _storage_interaction: PlayerStorageInteractionType
var _title_music_tween: Tween var _title_music_tween: Tween
var _title_music_transition_generation: int = 0 var _title_music_transition_generation: int = 0
@ -277,6 +296,7 @@ var _rain_ambience: RainAmbienceType
func _ready() -> void: func _ready() -> void:
_data_root.prepare_legacy_user_data()
_performance_profile = RuntimePerformanceProfileType.from_environment() _performance_profile = RuntimePerformanceProfileType.from_environment()
_dedicated_runtime = _is_dedicated_server_runtime() _dedicated_runtime = _is_dedicated_server_runtime()
if _dedicated_runtime: if _dedicated_runtime:
@ -294,7 +314,7 @@ func _ready() -> void:
_configure_presentation_performance_profile( _configure_presentation_performance_profile(
_performance_profile.is_light() _performance_profile.is_light()
) )
DisplayServer.window_set_title("NETfishing") DisplayServer.window_set_title("straywild")
_rain_ambience = RainAmbienceType.new() _rain_ambience = RainAmbienceType.new()
_rain_ambience.name = "RainAmbience" _rain_ambience.name = "RainAmbience"
add_child(_rain_ambience) add_child(_rain_ambience)
@ -324,11 +344,12 @@ func _ready() -> void:
func _configure_presentation_performance_profile(light_profile: bool) -> void: func _configure_presentation_performance_profile(light_profile: bool) -> void:
_game_ui.get_title_screen().set_minimal_presentation(light_profile)
_game_ui.set_light_performance_profile(light_profile) _game_ui.set_light_performance_profile(light_profile)
var title_water_material := _title_background.material as ShaderMaterial var title_background_material := (
if title_water_material != null: _title_background.material as ShaderMaterial
title_water_material.set_shader_parameter( )
if title_background_material != null:
title_background_material.set_shader_parameter(
"animation_enabled", "animation_enabled",
not light_profile, not light_profile,
) )
@ -461,7 +482,7 @@ func _print_dedicated_metrics() -> void:
metrics["gatherables"] = _network_world_spawns.call( metrics["gatherables"] = _network_world_spawns.call(
"get_network_metrics" "get_network_metrics"
) )
print("NETfishing server metrics: %s" % JSON.stringify(metrics)) print("straywild server metrics: %s" % JSON.stringify(metrics))
func _start_dedicated_server() -> void: func _start_dedicated_server() -> void:
@ -470,10 +491,7 @@ func _start_dedicated_server() -> void:
_fail_dedicated_server(config.error_message) _fail_dedicated_server(config.error_message)
return return
var data_path: String = config.data_directory var data_path: String = config.data_directory
var manifest_path: String = data_path.path_join( if not _data_root.has_data_manifest(data_path):
PlayerDataRoot.MANIFEST_FILENAME
)
if not FileAccess.file_exists(manifest_path):
var created: Dictionary = _data_root.create_unbound_root(data_path) var created: Dictionary = _data_root.create_unbound_root(data_path)
if not bool(created.get("ok", false)): if not bool(created.get("ok", false)):
_fail_dedicated_server(str(created.get( _fail_dedicated_server(str(created.get(
@ -556,7 +574,7 @@ func _start_dedicated_server() -> void:
_fail_dedicated_server(_discovery.get_host_status_message()) _fail_dedicated_server(_discovery.get_host_status_message())
return return
print( print(
"NETfishing dedicated server ready: %s on %s:%d (%d players, %s, timezone %s)" "straywild dedicated server ready: %s on %s:%d (%d players, %s, timezone %s)"
% [ % [
config.server_name, config.server_name,
config.bind_address, config.bind_address,
@ -741,6 +759,7 @@ func _initialize_application(dedicated: bool) -> void:
_player.inventory_layout, _player.inventory_layout,
) )
_shop_interaction = _test_world.get_fishing_shop() _shop_interaction = _test_world.get_fishing_shop()
_decor_shop_interaction = _test_world.get_decor_shop()
_storage_interaction = _test_world.get_player_storage() _storage_interaction = _test_world.get_player_storage()
if not dedicated: if not dedicated:
_shop_interaction.setup_local_player(_player) _shop_interaction.setup_local_player(_player)
@ -750,6 +769,10 @@ func _initialize_application(dedicated: bool) -> void:
_game_ui.set_shop_npc_player_in_range( _game_ui.set_shop_npc_player_in_range(
_shop_interaction.is_local_player_in_range() _shop_interaction.is_local_player_in_range()
) )
_decor_shop_interaction.setup_local_player(_player)
_decor_shop_interaction.local_player_range_changed.connect(
_on_decor_shop_range_changed
)
_storage_interaction.setup_local_player(_player) _storage_interaction.setup_local_player(_player)
_storage_interaction.local_player_range_changed.connect( _storage_interaction.local_player_range_changed.connect(
_on_storage_range_changed _on_storage_range_changed
@ -771,6 +794,23 @@ func _initialize_application(dedicated: bool) -> void:
_world_weather, _world_weather,
_player_jobs, _player_jobs,
_player, _player,
_home_state,
)
_home_world.setup(
_test_world,
_player_spawn_service,
_network_session,
dedicated,
)
_network_home.setup(
_network_session,
_player_spawn_service,
_home_state,
_save_manager,
_home_world,
_player.bag,
_player.inventory_layout,
_player.hotbar,
) )
_player_jobs.set_save_manager(_save_manager) _player_jobs.set_save_manager(_save_manager)
_save_manager.set_autosave_enabled(false) _save_manager.set_autosave_enabled(false)
@ -904,6 +944,9 @@ func _initialize_application(dedicated: bool) -> void:
_save_manager, _save_manager,
_asset_reservations, _asset_reservations,
_player.inventory_layout, _player.inventory_layout,
_home_state,
_network_home,
_decor_shop_interaction,
) )
_fishing_spot.setup( _fishing_spot.setup(
_player, _player,
@ -921,6 +964,7 @@ func _initialize_application(dedicated: bool) -> void:
_network_fishing, _network_fishing,
_world_time, _world_time,
_world_weather, _world_weather,
Callable(_test_world, "get_fishable_water_regions"),
) )
if dedicated: if dedicated:
return return
@ -942,11 +986,13 @@ func _initialize_application(dedicated: bool) -> void:
_player.fishing_upgrades, _player.fishing_upgrades,
_shop_interaction, _shop_interaction,
_storage_interaction, _storage_interaction,
_decor_shop_interaction,
_player.item_effects, _player.item_effects,
_player.cooler_capacity, _player.cooler_capacity,
_network_session, _network_session,
_network_sale, _network_sale,
_network_shop, _network_shop,
_home_state,
_network_chat, _network_chat,
_network_profile, _network_profile,
_player_spawn_service, _player_spawn_service,
@ -964,6 +1010,21 @@ func _initialize_application(dedicated: bool) -> void:
_test_world.get_world_environment(), _test_world.get_world_environment(),
_test_world.get_sun(), _test_world.get_sun(),
) )
_home_decor_controller.setup(
_player,
_player.hotbar,
_player.bag,
_home_state,
_network_home,
_home_world,
_fishing_spot,
_game_ui.get_home_decor_toolbar(),
)
_game_ui.get_player_storage().decorate_requested.connect(
_home_decor_controller.begin_decorating
)
_network_home.local_space_changed.connect(_on_local_home_space_changed)
_game_ui.rv_storage_requested.connect(_on_rv_storage_requested)
_game_ui.setup_data_and_identity( _game_ui.setup_data_and_identity(
_data_root, _data_root,
_identity_backups, _identity_backups,
@ -972,6 +1033,7 @@ func _initialize_application(dedicated: bool) -> void:
_network_session, _network_session,
_interface_fonts, _interface_fonts,
) )
_game_ui.data_root_changed.connect(_on_data_root_changed)
_game_ui.setup_controller_mapping(_controller_mapping_manager) _game_ui.setup_controller_mapping(_controller_mapping_manager)
_ui_pixelation.setup_controller_mapping(_controller_mapping_manager) _ui_pixelation.setup_controller_mapping(_controller_mapping_manager)
_game_ui.setup_keyboard_mouse_mapping(_keyboard_mouse_mapping_manager) _game_ui.setup_keyboard_mouse_mapping(_keyboard_mouse_mapping_manager)
@ -1006,7 +1068,8 @@ func _initialize_application(dedicated: bool) -> void:
_game_ui, _game_ui,
_game_ui.get_screen_fade(), _game_ui.get_screen_fade(),
_test_world.get_player_water_triggers(), _test_world.get_player_water_triggers(),
_test_world.get_safe_respawn_points() _test_world.get_safe_respawn_points(),
Callable(_test_world, "get_water_recovery_position"),
) )
_ui_pixelation.effective_pixel_size_changed.connect( _ui_pixelation.effective_pixel_size_changed.connect(
_on_effective_ui_pixel_size_changed _on_effective_ui_pixel_size_changed
@ -1096,7 +1159,7 @@ func _initialize_application(dedicated: bool) -> void:
func _show_data_root_setup() -> void: func _show_data_root_setup() -> void:
if _data_setup_dialog == null: if _data_setup_dialog == null:
_data_setup_dialog = ConfirmationDialog.new() _data_setup_dialog = ConfirmationDialog.new()
_data_setup_dialog.title = "NETfishing player data" _data_setup_dialog.title = "straywild player data"
_data_setup_dialog.ok_button_text = "Use This Folder" _data_setup_dialog.ok_button_text = "Use This Folder"
_data_setup_dialog.cancel_button_text = "Quit" _data_setup_dialog.cancel_button_text = "Quit"
_data_setup_dialog.confirmed.connect(_use_default_data_root) _data_setup_dialog.confirmed.connect(_use_default_data_root)
@ -1113,14 +1176,14 @@ func _show_data_root_setup() -> void:
var default_path: String = _data_root.default_visible_path() var default_path: String = _data_root.default_visible_path()
var legacy: bool = PortableDataMigration.legacy_files_present() var legacy: bool = PortableDataMigration.legacy_files_present()
_data_setup_dialog.dialog_text = ( _data_setup_dialog.dialog_text = (
("Move your existing NETfishing data to an easy-to-find folder." ("Move your existing straywild data to an easy-to-find folder."
if legacy else "Choose where NETfishing stores your player data.") if legacy else "Choose where straywild stores your player data.")
+ "\n\n%s\n\nYou can choose a Syncthing folder." + "\n\n%s\n\nYou can choose a Syncthing folder."
% (default_path if not default_path.is_empty() else "Choose a folder") % (default_path if not default_path.is_empty() else "Choose a folder")
) )
_data_setup_dialog.ok_button_text = ( _data_setup_dialog.ok_button_text = (
( (
"Move to Documents/NETFISHING" "Move to Documents/straywild"
if legacy else "Use This Folder" if legacy else "Use This Folder"
) )
if not default_path.is_empty() if not default_path.is_empty()
@ -1129,7 +1192,7 @@ func _show_data_root_setup() -> void:
_data_setup_current_button.visible = not default_path.is_empty() _data_setup_current_button.visible = not default_path.is_empty()
if not _data_root.error_message.is_empty(): if not _data_root.error_message.is_empty():
_data_setup_dialog.dialog_text = ( _data_setup_dialog.dialog_text = (
"Your NETfishing data folder is unavailable.\n\n%s" "Your straywild data folder is unavailable.\n\n%s"
% _data_root.error_message % _data_root.error_message
) )
_data_setup_dialog.popup_centered(Vector2i(640, 360)) _data_setup_dialog.popup_centered(Vector2i(640, 360))
@ -1265,11 +1328,11 @@ func _activate_selected_data_path(path: String, app_data: bool = false) -> void:
func _show_existing_root_choice(path: String) -> void: func _show_existing_root_choice(path: String) -> void:
_pending_existing_root_path = path _pending_existing_root_path = path
var dialog: ConfirmationDialog = ConfirmationDialog.new() var dialog: ConfirmationDialog = ConfirmationDialog.new()
dialog.title = "Existing NETfishing data" dialog.title = "Existing straywild data"
dialog.ok_button_text = "Use Data Already in Selected Folder" dialog.ok_button_text = "Use Data Already in Selected Folder"
dialog.cancel_button_text = "Cancel" dialog.cancel_button_text = "Cancel"
dialog.dialog_text = ( dialog.dialog_text = (
"This folder already contains NETfishing player data.\n\n" "This folder already contains straywild player data.\n\n"
+ "Choose which complete data set to use. Data is never merged automatically." + "Choose which complete data set to use. Data is never merged automatically."
) )
dialog.add_button( dialog.add_button(
@ -1343,6 +1406,18 @@ func _on_data_root_status(message: String) -> void:
_configure_popup_dialog.call_deferred(dialog, dialog.get_ok_button()) _configure_popup_dialog.call_deferred(dialog, dialog.get_ok_button())
func _on_data_root_changed() -> void:
_save_manager.set_autosave_enabled(false)
_save_manager.cancel_pending_autosave()
_configure_portable_stores()
if not _network_profile_service.reload_persisted_profile():
push_warning("The selected data folder's player profile could not be loaded.")
_network_session.set_local_appearance_snapshot(
_appearance_store.get_snapshot()
)
_game_ui.get_title_screen().refresh_after_data_root_change()
func _on_server_trust_required( func _on_server_trust_required(
endpoint: String, endpoint: String,
expected_fingerprint: String, expected_fingerprint: String,
@ -1461,6 +1536,9 @@ func _input(event: InputEvent) -> void:
if _game_ui.get_player_storage().consume_escape(): if _game_ui.get_player_storage().consume_escape():
get_viewport().set_input_as_handled() get_viewport().set_input_as_handled()
return return
if _game_ui.get_decor_shop().consume_escape():
get_viewport().set_input_as_handled()
return
if fishing_shop.consume_escape(): if fishing_shop.consume_escape():
get_viewport().set_input_as_handled() get_viewport().set_input_as_handled()
return return
@ -1559,19 +1637,50 @@ func _is_pause_open_request(event: InputEvent) -> bool:
func _unhandled_input(event: InputEvent) -> void: func _unhandled_input(event: InputEvent) -> void:
if (
_gameplay_started
and event.is_action_pressed("fish_primary")
and not (event is InputEventKey and event.echo)
and _fishing_spot.try_use_active_non_fishing_item()
):
get_viewport().set_input_as_handled()
return
if ( if (
not _gameplay_started not _gameplay_started
or not event.is_action_pressed("interact") or not event.is_action_pressed("interact")
or (event is InputEventKey and event.echo) or (event is InputEventKey and event.echo)
): ):
return return
var home_owner: String = _network_home.get_local_home_owner_fingerprint()
if not home_owner.is_empty():
if _home_world.is_avatar_near_interior_exit(_player, home_owner):
if _network_home.request_exit():
_game_ui.set_storage_prompt_visible(false)
get_viewport().set_input_as_handled()
return
if (
_network_home.can_local_decorate()
and _fishing_spot.can_open_fishing_shop()
and _game_ui.get_player_storage().open_storage_from_home(true)
):
_game_ui.set_storage_prompt_visible(false)
get_viewport().set_input_as_handled()
return
var nearby_home_owner: String = _home_world.find_nearby_exterior_owner(
_player
)
if not nearby_home_owner.is_empty():
if _network_home.request_enter(nearby_home_owner):
_game_ui.set_storage_prompt_visible(false)
get_viewport().set_input_as_handled()
return
if ( if (
_storage_interaction != null _decor_shop_interaction != null
and _storage_interaction.is_local_player_in_range() and _decor_shop_interaction.is_local_player_in_range()
and _fishing_spot.can_open_fishing_shop() and _fishing_spot.can_open_fishing_shop()
and _game_ui.get_player_storage().open_storage() and _game_ui.get_decor_shop().open_shop()
): ):
_game_ui.set_storage_prompt_visible(false) _game_ui.set_shop_prompt_visible(false)
get_viewport().set_input_as_handled() get_viewport().set_input_as_handled()
return return
if ( if (
@ -1594,37 +1703,42 @@ func _process(delta: float) -> void:
) )
_print_dedicated_metrics() _print_dedicated_metrics()
return return
var show_shop_prompt := _can_show_shop_prompt() var show_decor_shop_prompt := _can_show_decor_shop_prompt()
var show_shop_prompt := show_decor_shop_prompt or _can_show_shop_prompt()
var shop_prompt_anchor := ( var shop_prompt_anchor := (
_shop_interaction.get_prompt_anchor_position() _decor_shop_interaction.get_prompt_anchor_position()
if _shop_interaction != null if show_decor_shop_prompt
else Vector3.ZERO else (
_shop_interaction.get_prompt_anchor_position()
if _shop_interaction != null else Vector3.ZERO
)
) )
_game_ui.set_shop_prompt_visible( _game_ui.set_shop_prompt_visible(
show_shop_prompt, show_shop_prompt,
shop_prompt_anchor, shop_prompt_anchor,
) )
var show_storage_prompt := _can_show_storage_prompt() var home_prompt := _get_home_prompt()
var storage_prompt_anchor := (
_storage_interaction.get_prompt_anchor_position()
if _storage_interaction != null
else Vector3.ZERO
)
_game_ui.set_storage_prompt_visible( _game_ui.set_storage_prompt_visible(
show_storage_prompt, not home_prompt.is_empty(),
storage_prompt_anchor, home_prompt.get("anchor", Vector3.ZERO),
str(home_prompt.get("label", "enter RV")),
)
_game_ui.set_rv_storage_button_visible(
_can_show_rv_storage_button()
) )
func _apply_runtime_settings(settings: PlayerSettingsType) -> void: func _apply_runtime_settings(settings: PlayerSettingsType) -> void:
if settings == null: if settings == null:
return return
var requested_window_mode: DisplayServer.WindowMode = ( var current_window_mode: DisplayServer.WindowMode = (
DisplayServer.WINDOW_MODE_FULLSCREEN DisplayServer.window_get_mode()
if settings.fullscreen_enabled
else DisplayServer.WINDOW_MODE_WINDOWED
) )
if DisplayServer.window_get_mode() != requested_window_mode: var requested_window_mode: DisplayServer.WindowMode = resolve_window_mode(
settings.fullscreen_enabled,
current_window_mode,
)
if current_window_mode != requested_window_mode:
DisplayServer.window_set_mode(requested_window_mode) DisplayServer.window_set_mode(requested_window_mode)
_apply_world_pixelation(settings.world_pixel_size) _apply_world_pixelation(settings.world_pixel_size)
_ui_pixelation.set_pixel_size(settings.ui_pixel_size) _ui_pixelation.set_pixel_size(settings.ui_pixel_size)
@ -1651,6 +1765,25 @@ func _apply_runtime_settings(settings: PlayerSettingsType) -> void:
) )
static func resolve_window_mode(
fullscreen_enabled: bool,
current_mode: DisplayServer.WindowMode,
) -> DisplayServer.WindowMode:
var is_fullscreen: bool = current_mode in [
DisplayServer.WINDOW_MODE_FULLSCREEN,
DisplayServer.WINDOW_MODE_EXCLUSIVE_FULLSCREEN,
]
if fullscreen_enabled:
return (
current_mode
if is_fullscreen
else DisplayServer.WINDOW_MODE_FULLSCREEN
)
if is_fullscreen:
return DisplayServer.WINDOW_MODE_WINDOWED
return current_mode
func _apply_world_pixelation(pixel_size: int) -> void: func _apply_world_pixelation(pixel_size: int) -> void:
var root_viewport: Viewport = get_viewport() var root_viewport: Viewport = get_viewport()
root_viewport.scaling_3d_mode = Viewport.SCALING_3D_MODE_NEAREST root_viewport.scaling_3d_mode = Viewport.SCALING_3D_MODE_NEAREST
@ -2332,17 +2465,9 @@ func _on_remote_recovery_requested(
peer_id, peer_id,
"Fishing attempt ended." "Fishing attempt ended."
) )
var target_position: Vector3 = _test_world.get_player_spawn_transform().origin var target_position := _test_world.get_water_recovery_position(
var nearest_distance: float = INF entry_position
for point: SafeRespawnPoint in _test_world.get_safe_respawn_points(): )
if point == null or not point.enabled:
continue
var distance: float = point.get_horizontal_distance_squared(
entry_position
)
if distance < nearest_distance:
nearest_distance = distance
target_position = point.global_position
target_position.y += _water_recovery.respawn_height_offset target_position.y += _water_recovery.respawn_height_offset
avatar.global_position = target_position avatar.global_position = target_position
avatar.velocity = Vector3.ZERO avatar.velocity = Vector3.ZERO
@ -2415,11 +2540,13 @@ func _apply_world(
if _application_initialized: if _application_initialized:
_refresh_active_world_bindings() _refresh_active_world_bindings()
_network_world_spawns.refresh_world_context() _network_world_spawns.refresh_world_context()
_network_home.refresh_world_presentations()
if _application_initialized and not _dedicated_runtime: if _application_initialized and not _dedicated_runtime:
_water_recovery.update_world_context( _water_recovery.update_world_context(
spawn_transform, spawn_transform,
_test_world.get_player_water_triggers(), _test_world.get_player_water_triggers(),
_test_world.get_safe_respawn_points(), _test_world.get_safe_respawn_points(),
Callable(_test_world, "get_water_recovery_position"),
) )
_shoreline_ambience.configure( _shoreline_ambience.configure(
_player, _player,
@ -2443,10 +2570,19 @@ func _refresh_active_world_bindings() -> void:
_storage_interaction.local_player_range_changed.disconnect( _storage_interaction.local_player_range_changed.disconnect(
_on_storage_range_changed _on_storage_range_changed
) )
if is_instance_valid(_decor_shop_interaction):
if _decor_shop_interaction.local_player_range_changed.is_connected(
_on_decor_shop_range_changed
):
_decor_shop_interaction.local_player_range_changed.disconnect(
_on_decor_shop_range_changed
)
_shop_interaction = _test_world.get_fishing_shop() _shop_interaction = _test_world.get_fishing_shop()
_decor_shop_interaction = _test_world.get_decor_shop()
_storage_interaction = _test_world.get_player_storage() _storage_interaction = _test_world.get_player_storage()
_network_sale.set_shop_interaction(_shop_interaction) _network_sale.set_shop_interaction(_shop_interaction)
_network_shop.set_shop_interaction(_shop_interaction) _network_shop.set_shop_interaction(_shop_interaction)
_network_shop.set_decor_shop_interaction(_decor_shop_interaction)
if _dedicated_runtime: if _dedicated_runtime:
return return
if _shop_interaction != null: if _shop_interaction != null:
@ -2459,9 +2595,15 @@ func _refresh_active_world_bindings() -> void:
_storage_interaction.local_player_range_changed.connect( _storage_interaction.local_player_range_changed.connect(
_on_storage_range_changed _on_storage_range_changed
) )
if _decor_shop_interaction != null:
_decor_shop_interaction.setup_local_player(_player)
_decor_shop_interaction.local_player_range_changed.connect(
_on_decor_shop_range_changed
)
_game_ui.set_world_interactions( _game_ui.set_world_interactions(
_shop_interaction, _shop_interaction,
_storage_interaction, _storage_interaction,
_decor_shop_interaction,
) )
_on_shop_range_changed( _on_shop_range_changed(
_shop_interaction != null _shop_interaction != null
@ -2471,6 +2613,10 @@ func _refresh_active_world_bindings() -> void:
_storage_interaction != null _storage_interaction != null
and _storage_interaction.is_local_player_in_range() and _storage_interaction.is_local_player_in_range()
) )
_on_decor_shop_range_changed(
_decor_shop_interaction != null
and _decor_shop_interaction.is_local_player_in_range()
)
func _on_water_recovery_starting() -> void: func _on_water_recovery_starting() -> void:
@ -2484,6 +2630,7 @@ func _on_water_recovery_starting() -> void:
_game_ui.close_player_menu_for_water_recovery() _game_ui.close_player_menu_for_water_recovery()
_game_ui.get_pause_menu().close_for_water_recovery() _game_ui.get_pause_menu().close_for_water_recovery()
_game_ui.get_fishing_shop().close_for_water_recovery() _game_ui.get_fishing_shop().close_for_water_recovery()
_game_ui.get_decor_shop().close_for_water_recovery()
_game_ui.get_player_storage().close_for_water_recovery() _game_ui.get_player_storage().close_for_water_recovery()
@ -2545,6 +2692,12 @@ func _on_active_hotbar_item_changed(
and item.is_available() and item.is_available()
and _player.bag.owns_item(item_id) and _player.bag.owns_item(item_id)
) )
var active_is_decor: bool = (
DecorCatalogType.has_product(item_id)
and item != null
and item.is_available()
and _player.bag.owns_item(item_id)
)
_player.set_active_fishing_rod( _player.set_active_fishing_rod(
item as FishingRodDataType if active_is_rod else null, item as FishingRodDataType if active_is_rod else null,
true, true,
@ -2555,6 +2708,7 @@ func _on_active_hotbar_item_changed(
) )
_player.set_active_catching_net(active_is_catching_net) _player.set_active_catching_net(active_is_catching_net)
_player.set_active_shovel(active_is_shovel) _player.set_active_shovel(active_is_shovel)
_player.set_active_decor_package(active_is_decor)
_game_ui.set_surface_drawing_hotbar_selected(active_is_art_kit) _game_ui.set_surface_drawing_hotbar_selected(active_is_art_kit)
_network_item_use.submit_local_equipped(item_id, active_is_rod or ( _network_item_use.submit_local_equipped(item_id, active_is_rod or (
item != null and _player.bag.owns_item(item_id) item != null and _player.bag.owns_item(item_id)
@ -2691,7 +2845,13 @@ func _exit_tree() -> void:
func _on_shop_range_changed(in_range: bool) -> void: func _on_shop_range_changed(in_range: bool) -> void:
_game_ui.set_shop_npc_player_in_range(in_range) _game_ui.set_shop_npc_player_in_range(
in_range
or (
_decor_shop_interaction != null
and _decor_shop_interaction.is_local_player_in_range()
)
)
if not in_range: if not in_range:
_game_ui.get_fishing_shop().close_for_range_exit() _game_ui.get_fishing_shop().close_for_range_exit()
_game_ui.set_shop_prompt_visible(_can_show_shop_prompt()) _game_ui.set_shop_prompt_visible(_can_show_shop_prompt())
@ -2700,7 +2860,50 @@ func _on_shop_range_changed(in_range: bool) -> void:
func _on_storage_range_changed(in_range: bool) -> void: func _on_storage_range_changed(in_range: bool) -> void:
if not in_range: if not in_range:
_game_ui.get_player_storage().close_for_range_exit() _game_ui.get_player_storage().close_for_range_exit()
_game_ui.set_storage_prompt_visible(_can_show_storage_prompt()) var prompt := _get_home_prompt()
_game_ui.set_storage_prompt_visible(
not prompt.is_empty(),
prompt.get("anchor", Vector3.ZERO),
str(prompt.get("label", "enter RV")),
)
_game_ui.set_rv_storage_button_visible(
_can_show_rv_storage_button()
)
func _on_decor_shop_range_changed(in_range: bool) -> void:
_game_ui.set_shop_npc_player_in_range(
in_range
or (
_shop_interaction != null
and _shop_interaction.is_local_player_in_range()
)
)
if not in_range:
_game_ui.get_decor_shop().close_for_range_exit()
_game_ui.set_shop_prompt_visible(
_can_show_shop_prompt() or _can_show_decor_shop_prompt()
)
func _on_local_home_space_changed(
space_id: StringName,
_owner_fingerprint: String,
) -> void:
var inside_rv: bool = String(space_id).begins_with("rv:")
_fishing_spot.set_fishing_environment_enabled(not inside_rv)
_game_ui.set_rv_interior_camera_mode(
inside_rv
)
_game_ui.set_rv_storage_button_visible(
_can_show_rv_storage_button()
)
func _on_rv_storage_requested() -> void:
if not _can_show_rv_storage_button():
return
_game_ui.get_player_storage().open_storage_from_home(true)
func _can_show_shop_prompt() -> bool: func _can_show_shop_prompt() -> bool:
@ -2709,18 +2912,66 @@ func _can_show_shop_prompt() -> bool:
and _shop_interaction != null and _shop_interaction != null
and _shop_interaction.is_local_player_in_range() and _shop_interaction.is_local_player_in_range()
and not _game_ui.get_fishing_shop().visible and not _game_ui.get_fishing_shop().visible
and not _game_ui.get_decor_shop().visible
and not _water_recovery.is_recovery_active()
and _fishing_spot.can_open_fishing_shop()
)
func _can_show_decor_shop_prompt() -> bool:
return (
_gameplay_started
and _decor_shop_interaction != null
and _decor_shop_interaction.is_local_player_in_range()
and not _game_ui.get_decor_shop().visible
and not _game_ui.get_fishing_shop().visible
and not _water_recovery.is_recovery_active() and not _water_recovery.is_recovery_active()
and _fishing_spot.can_open_fishing_shop() and _fishing_spot.can_open_fishing_shop()
) )
func _can_show_storage_prompt() -> bool: func _can_show_storage_prompt() -> bool:
return not _get_home_prompt().is_empty()
func _can_show_rv_storage_button() -> bool:
return ( return (
_gameplay_started _gameplay_started
and _storage_interaction != null and not _network_home.get_local_home_owner_fingerprint().is_empty()
and _storage_interaction.is_local_player_in_range() and _network_home.can_local_decorate()
and not _game_ui.get_player_storage().visible
and not _game_ui.get_fishing_shop().visible
and not _water_recovery.is_recovery_active() and not _water_recovery.is_recovery_active()
and _fishing_spot.can_open_fishing_shop() and _fishing_spot.can_open_fishing_shop()
) )
func _get_home_prompt() -> Dictionary:
if (
not _gameplay_started
or _game_ui.get_player_storage().visible
or _game_ui.get_fishing_shop().visible
or _game_ui.get_decor_shop().visible
or _water_recovery.is_recovery_active()
or not _fishing_spot.can_open_fishing_shop()
):
return {}
var owner_fingerprint: String = (
_network_home.get_local_home_owner_fingerprint()
)
if not owner_fingerprint.is_empty():
if _home_world.is_avatar_near_interior_exit(
_player, owner_fingerprint
):
return {
"label": "leave RV",
"anchor": _home_world.get_interior_prompt_position(
owner_fingerprint
),
}
return {}
var nearby_owner: String = _home_world.find_nearby_exterior_owner(_player)
if nearby_owner.is_empty():
return {}
return {
"label": "enter RV",
"anchor": _home_world.get_exterior_prompt_position(nearby_owner),
}

View file

@ -38,7 +38,7 @@
[ext_resource type="Script" uid="uid://dpiv6wttry0j4" path="res://network/player_data_root.gd" id="37_data_root"] [ext_resource type="Script" uid="uid://dpiv6wttry0j4" path="res://network/player_data_root.gd" id="37_data_root"]
[ext_resource type="Script" uid="uid://bu5q4a8jfvxkf" path="res://network/identity_backup_service.gd" id="38_identity_backup"] [ext_resource type="Script" uid="uid://bu5q4a8jfvxkf" path="res://network/identity_backup_service.gd" id="38_identity_backup"]
[ext_resource type="Script" uid="uid://djmxy6jibi5y3" path="res://ui/interface_font_controller.gd" id="39_fonts"] [ext_resource type="Script" uid="uid://djmxy6jibi5y3" path="res://ui/interface_font_controller.gd" id="39_fonts"]
[ext_resource type="Shader" uid="uid://c8s1g7tn771du" path="res://ui/title_water_background.gdshader" id="40_title_water"] [ext_resource type="Texture2D" uid="uid://doju2lbj2ghwa" path="res://world/generation/chunks/assets/chunk_0000_grass_lite.png" id="40_title_grass"]
[ext_resource type="Texture2D" uid="uid://f8a0cs0s7ler" path="res://art/patterns/pattern_tealdot.png" id="41_menu_pattern"] [ext_resource type="Texture2D" uid="uid://f8a0cs0s7ler" path="res://art/patterns/pattern_tealdot.png" id="41_menu_pattern"]
[ext_resource type="Shader" uid="uid://byw3iub66qvog" path="res://ui/player_menu_pattern.gdshader" id="42_menu_pattern"] [ext_resource type="Shader" uid="uid://byw3iub66qvog" path="res://ui/player_menu_pattern.gdshader" id="42_menu_pattern"]
[ext_resource type="Texture2D" uid="uid://b6xws1d2dnbnn" path="res://art/patterns/pattern_moneyfish.png" id="43_shop_pattern"] [ext_resource type="Texture2D" uid="uid://b6xws1d2dnbnn" path="res://art/patterns/pattern_moneyfish.png" id="43_shop_pattern"]
@ -58,19 +58,17 @@
[ext_resource type="Resource" path="res://gathering/catalog/gatherable_catalog.tres" id="58_gatherable_catalog"] [ext_resource type="Resource" path="res://gathering/catalog/gatherable_catalog.tres" id="58_gatherable_catalog"]
[ext_resource type="Script" path="res://network/network_world_spawn_service.gd" id="59_world_spawns"] [ext_resource type="Script" path="res://network/network_world_spawn_service.gd" id="59_world_spawns"]
[ext_resource type="Script" path="res://gathering/gathering_controller.gd" id="60_gathering_controller"] [ext_resource type="Script" path="res://gathering/gathering_controller.gd" id="60_gathering_controller"]
[ext_resource type="Script" path="res://homes/player_home_state.gd" id="61_home_state"]
[ext_resource type="Script" path="res://homes/home_world_service.gd" id="62_home_world"]
[ext_resource type="Script" path="res://network/network_home_service.gd" id="63_network_home"]
[ext_resource type="Script" path="res://homes/home_decor_controller.gd" id="64_home_decor_controller"]
[sub_resource type="ShaderMaterial" id="ShaderMaterial_title_water_native"] [sub_resource type="ShaderMaterial" id="ShaderMaterial_title_grass_native"]
shader = ExtResource("40_title_water") shader = ExtResource("42_menu_pattern")
shader_parameter/surface_color = Color(0.1, 0.45, 0.62, 1) shader_parameter/pattern_texture = ExtResource("40_title_grass")
shader_parameter/middle_color = Color(0.055, 0.285, 0.4, 1) shader_parameter/source_tile_size = Vector2(128, 128)
shader_parameter/depth_color = Color(0.02, 0.16, 0.27, 1) shader_parameter/display_scale = 1.0
shader_parameter/virtual_pixel_density = Vector2(320, 180) shader_parameter/scroll_velocity_pixels = Vector2(-7.5, -7.5)
shader_parameter/color_step_count = 14.0
shader_parameter/continuous_depth_blend = 0.12
shader_parameter/wave_speed = 0.1
shader_parameter/wave_scale = 2.4
shader_parameter/wave_strength = 0.012
shader_parameter/caustic_strength = 0.018
[sub_resource type="ShaderMaterial" id="ShaderMaterial_player_menu_pattern"] [sub_resource type="ShaderMaterial" id="ShaderMaterial_player_menu_pattern"]
shader = ExtResource("42_menu_pattern") shader = ExtResource("42_menu_pattern")
@ -139,7 +137,7 @@ layer = -1
[node name="TitleBackground" type="ColorRect" parent="TitleBackgroundLayer" unique_id=1768697365] [node name="TitleBackground" type="ColorRect" parent="TitleBackgroundLayer" unique_id=1768697365]
unique_name_in_owner = true unique_name_in_owner = true
material = SubResource("ShaderMaterial_title_water_native") material = SubResource("ShaderMaterial_title_grass_native")
anchors_preset = 15 anchors_preset = 15
anchor_right = 1.0 anchor_right = 1.0
anchor_bottom = 1.0 anchor_bottom = 1.0
@ -265,6 +263,14 @@ script = ExtResource("22_network_sale")
unique_name_in_owner = true unique_name_in_owner = true
script = ExtResource("23_network_shop") script = ExtResource("23_network_shop")
[node name="NetworkHomeService" type="Node" parent="."]
unique_name_in_owner = true
script = ExtResource("63_network_home")
[node name="HomeDecorController" type="Node" parent="."]
unique_name_in_owner = true
script = ExtResource("64_home_decor_controller")
[node name="NetworkItemUseService" type="Node" parent="." unique_id=255154670] [node name="NetworkItemUseService" type="Node" parent="." unique_id=255154670]
unique_name_in_owner = true unique_name_in_owner = true
script = ExtResource("24_network_item") script = ExtResource("24_network_item")
@ -328,6 +334,10 @@ transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 3.3999481, 12.07081)
[node name="WorldGatherables" type="Node3D" parent="."] [node name="WorldGatherables" type="Node3D" parent="."]
[node name="HomeWorldService" type="Node3D" parent="."]
unique_name_in_owner = true
script = ExtResource("62_home_world")
[node name="GatheringController" type="Node3D" parent="."] [node name="GatheringController" type="Node3D" parent="."]
unique_name_in_owner = true unique_name_in_owner = true
script = ExtResource("60_gathering_controller") script = ExtResource("60_gathering_controller")
@ -344,6 +354,10 @@ script = ExtResource("8_recovery")
unique_name_in_owner = true unique_name_in_owner = true
script = ExtResource("9_save") script = ExtResource("9_save")
[node name="PlayerHomeState" type="Node" parent="."]
unique_name_in_owner = true
script = ExtResource("61_home_state")
[node name="PlayerSettingsManager" type="Node" parent="." unique_id=906991991] [node name="PlayerSettingsManager" type="Node" parent="." unique_id=906991991]
unique_name_in_owner = true unique_name_in_owner = true
script = ExtResource("10_settings") script = ExtResource("10_settings")

View file

@ -2,9 +2,13 @@ class_name RuntimePerformanceProfile
extends RefCounted extends RefCounted
const PROFILE_ENVIRONMENT_VARIABLE: String = ( const PROFILE_ENVIRONMENT_VARIABLE: String = (
"straywild_PERFORMANCE_PROFILE"
)
const LEGACY_LIGHT_ENVIRONMENT_VARIABLE: String = "straywild_LOW_END"
const NETFISHING_PROFILE_ENVIRONMENT_VARIABLE := (
"NETFISHING_PERFORMANCE_PROFILE" "NETFISHING_PERFORMANCE_PROFILE"
) )
const LEGACY_LIGHT_ENVIRONMENT_VARIABLE: String = "NETFISHING_LOW_END" const NETFISHING_LIGHT_ENVIRONMENT_VARIABLE := "NETFISHING_LOW_END"
const NORMAL_PROFILE: StringName = &"normal" const NORMAL_PROFILE: StringName = &"normal"
const LIGHT_PROFILE: StringName = &"light" const LIGHT_PROFILE: StringName = &"light"
const NORMAL_WORLD_RENDER_SCALE: float = 1.0 const NORMAL_WORLD_RENDER_SCALE: float = 1.0
@ -14,9 +18,19 @@ var _profile_name: StringName = NORMAL_PROFILE
static func from_environment() -> RuntimePerformanceProfile: static func from_environment() -> RuntimePerformanceProfile:
var profile_name: String = OS.get_environment(
PROFILE_ENVIRONMENT_VARIABLE
)
if profile_name.is_empty():
profile_name = OS.get_environment(
NETFISHING_PROFILE_ENVIRONMENT_VARIABLE
)
return from_name( return from_name(
StringName(OS.get_environment(PROFILE_ENVIRONMENT_VARIABLE)), StringName(profile_name),
OS.get_environment(LEGACY_LIGHT_ENVIRONMENT_VARIABLE) == "1", (
OS.get_environment(LEGACY_LIGHT_ENVIRONMENT_VARIABLE) == "1"
or OS.get_environment(NETFISHING_LIGHT_ENVIRONMENT_VARIABLE) == "1"
),
) )

View file

@ -1,6 +1,6 @@
extends Node extends Node
## Enforces NETfishing's nearest-neighbor-only artwork policy at runtime. ## Enforces straywild's nearest-neighbor-only artwork policy at runtime.
## ##
## Godot uses separate texture-filter enums for CanvasItem and 3D materials. ## Godot uses separate texture-filter enums for CanvasItem and 3D materials.
## Applying both here prevents imported models, dynamically created controls, ## Applying both here prevents imported models, dynamically created controls,
@ -15,8 +15,12 @@ func _enter_tree() -> void:
func _on_node_added(node: Node) -> void: func _on_node_added(node: Node) -> void:
enforce_node(node) enforce_node(node)
# Some runtime presenters assign their mesh or material immediately after # Some runtime presenters assign their mesh or material immediately after
# add_child(). Recheck once deferred so those resources are covered too. # add_child(). Recheck those 3D resource owners once deferred so the newly
call_deferred("_enforce_deferred_node", weakref(node)) # assigned resources are covered too. CanvasItems keep their filter setting
# when their texture changes, so deferring every dynamic UI node only floods
# the message queue during large inventory and catalog refreshes.
if _needs_deferred_resource_check(node):
call_deferred("_enforce_deferred_node", weakref(node))
func _enforce_existing_tree() -> void: func _enforce_existing_tree() -> void:
@ -29,6 +33,15 @@ func _enforce_deferred_node(node_ref: WeakRef) -> void:
enforce_node(node) enforce_node(node)
static func _needs_deferred_resource_check(node: Node) -> bool:
return (
node is GeometryInstance3D
or node is MultiMeshInstance3D
or node is GPUParticles3D
or node is CPUParticles3D
)
static func enforce_subtree(node: Node) -> void: static func enforce_subtree(node: Node) -> void:
enforce_node(node) enforce_node(node)
for child: Node in node.get_children(): for child: Node in node.get_children():

View file

@ -20,12 +20,13 @@ signal friend_invite_received(
signal friend_invite_finished(success: bool, message: String) signal friend_invite_finished(success: bool, message: String)
const BASE_URL_SETTING: String = "network/discovery/base_url" const BASE_URL_SETTING: String = "network/discovery/base_url"
const BASE_URL_ENVIRONMENT: String = "NETFISHING_DISCOVERY_URL" const BASE_URL_ENVIRONMENT: String = "straywild_DISCOVERY_URL"
const LEGACY_BASE_URL_ENVIRONMENT: String = "NETFISHING_DISCOVERY_URL"
const SETTINGS_PATH: String = "user://network_discovery.cfg" const SETTINGS_PATH: String = "user://network_discovery.cfg"
const DEFAULT_ROOM_NAME: String = "Player's Server" const DEFAULT_ROOM_NAME: String = "Player's Server"
const LEGACY_DEFAULT_ROOM_NAME: String = "NETfishing Room" const LEGACY_DEFAULT_ROOM_NAME: String = "straywild Room"
const LEGACY_DEDICATED_DEFAULT_ROOM_NAME: String = ( const LEGACY_DEDICATED_DEFAULT_ROOM_NAME: String = (
"NETfishing Dedicated Server" "straywild Dedicated Server"
) )
const ROOM_NAME_SUFFIX: String = "'s Server" const ROOM_NAME_SUFFIX: String = "'s Server"
const MAX_ROOM_NAME_LENGTH: int = 48 const MAX_ROOM_NAME_LENGTH: int = 48
@ -33,7 +34,7 @@ const HEARTBEAT_INTERVAL_SECONDS: float = 15.0
const REQUEST_TIMEOUT_SECONDS: float = 8.0 const REQUEST_TIMEOUT_SECONDS: float = 8.0
const TRAVERSAL_POLL_INTERVAL_SECONDS: float = 1.0 const TRAVERSAL_POLL_INTERVAL_SECONDS: float = 1.0
const JOIN_PROBE_INTERVAL_SECONDS: float = 0.35 const JOIN_PROBE_INTERVAL_SECONDS: float = 0.35
const TRAVERSAL_PACKET_PREFIX: String = "NETFISHING_TRAVERSAL_V1 " const TRAVERSAL_PACKET_PREFIX: String = "straywild_TRAVERSAL_V1 "
const UPNP_MAPPING_DURATION_SECONDS: int = 3600 const UPNP_MAPPING_DURATION_SECONDS: int = 3600
const UPNP_RENEW_INTERVAL_SECONDS: float = 2700.0 const UPNP_RENEW_INTERVAL_SECONDS: float = 2700.0
const UPNP_RETRY_INTERVAL_SECONDS: float = 300.0 const UPNP_RETRY_INTERVAL_SECONDS: float = 300.0
@ -553,6 +554,10 @@ func is_own_room(room: Dictionary) -> bool:
func _configured_base_url() -> String: func _configured_base_url() -> String:
var value: String = OS.get_environment(BASE_URL_ENVIRONMENT).strip_edges() var value: String = OS.get_environment(BASE_URL_ENVIRONMENT).strip_edges()
if value.is_empty():
value = OS.get_environment(
LEGACY_BASE_URL_ENVIRONMENT
).strip_edges()
if value.is_empty(): if value.is_empty():
value = str(ProjectSettings.get_setting(BASE_URL_SETTING, "")).strip_edges() value = str(ProjectSettings.get_setting(BASE_URL_SETTING, "")).strip_edges()
while value.ends_with("/"): while value.ends_with("/"):
@ -560,7 +565,7 @@ func _configured_base_url() -> String:
if not value.is_empty() and not ( if not value.is_empty() and not (
value.begins_with("https://") or value.begins_with("http://") value.begins_with("https://") or value.begins_with("http://")
): ):
push_warning("Ignored invalid NETfishing discovery URL.") push_warning("Ignored invalid straywild discovery URL.")
return "" return ""
return value return value
@ -809,7 +814,7 @@ func _on_traversal_request_completed(
var port: int = int(endpoint.get("port", 0)) var port: int = int(endpoint.get("port", 0))
if address.is_empty() or port < 1 or port > 65535: if address.is_empty() or port < 1 or port > 65535:
continue continue
var punch: PackedByteArray = "NETFISHING_PUNCH_V1".to_utf8_buffer() var punch: PackedByteArray = "straywild_PUNCH_V1".to_utf8_buffer()
for _attempt: int in 3: for _attempt: int in 3:
_session.send_traversal_packet(address, port, punch) _session.send_traversal_packet(address, port, punch)
@ -895,13 +900,13 @@ func _configure_upnp_mapping(port: int) -> Dictionary:
var mapping_result: int = upnp.add_port_mapping( var mapping_result: int = upnp.add_port_mapping(
port, port,
port, port,
"NETfishing", "straywild",
"UDP", "UDP",
UPNP_MAPPING_DURATION_SECONDS, UPNP_MAPPING_DURATION_SECONDS,
) )
if mapping_result == UPNP.UPNP_RESULT_ONLY_PERMANENT_LEASE_SUPPORTED: if mapping_result == UPNP.UPNP_RESULT_ONLY_PERMANENT_LEASE_SUPPORTED:
mapping_result = upnp.add_port_mapping( mapping_result = upnp.add_port_mapping(
port, port, "NETfishing", "UDP", 0 port, port, "straywild", "UDP", 0
) )
return { return {
"success": mapping_result == UPNP.UPNP_RESULT_SUCCESS, "success": mapping_result == UPNP.UPNP_RESULT_SUCCESS,
@ -1374,18 +1379,18 @@ func _discovery_version_mismatch_message(required_version: String) -> String:
match rejection: match rejection:
NetworkProtocol.RejectionCode.CLIENT_OUTDATED: NetworkProtocol.RejectionCode.CLIENT_OUTDATED:
return ( return (
"Your NETfishing version is out of date. Update to %s to " "Your straywild version is out of date. Update to %s to "
+ "enable public discovery. This room will not be listed " + "enable public discovery. This room will not be listed "
+ "until you update." + "until you update."
) % required_version ) % required_version
NetworkProtocol.RejectionCode.SERVER_OUTDATED: NetworkProtocol.RejectionCode.SERVER_OUTDATED:
return ( return (
"Public discovery is still on NETfishing %s. This room " "Public discovery is still on straywild %s. This room "
+ "will not be listed until discovery is updated for %s." + "will not be listed until discovery is updated for %s."
) % [required_version, local_version] ) % [required_version, local_version]
_: _:
return ( return (
"Public discovery requires NETfishing %s. This room will " "Public discovery requires straywild %s. This room will "
+ "not be listed until both versions match." + "not be listed until both versions match."
) % required_version ) % required_version
@ -1577,4 +1582,4 @@ func _save_settings() -> void:
config.set_value("social", "share_presence", _presence_sharing) config.set_value("social", "share_presence", _presence_sharing)
var error: Error = config.save(SETTINGS_PATH) var error: Error = config.save(SETTINGS_PATH)
if error != OK: if error != OK:
push_warning("Could not save local NETfishing discovery settings.") push_warning("Could not save local straywild discovery settings.")

View file

@ -17,6 +17,10 @@ var _data_root: PlayerDataRoot
func configure_storage(path: String, data_root: PlayerDataRoot) -> void: func configure_storage(path: String, data_root: PlayerDataRoot) -> void:
_store_path = path _store_path = path
_data_root = data_root _data_root = data_root
_namespaces.clear()
_loaded = false
_write_blocked = false
_expected_hash = ""
func is_banned(host_fingerprint: String, target_fingerprint: String) -> bool: func is_banned(host_fingerprint: String, target_fingerprint: String) -> bool:

View file

@ -3,7 +3,8 @@ extends Node
signal operation_finished(success: bool, message: String) signal operation_finished(success: bool, message: String)
const MAGIC := "NETFISHING_IDENTITY_BACKUP" const MAGIC := "straywild_IDENTITY_BACKUP"
const LEGACY_MAGIC := "NETFISHING_IDENTITY_BACKUP"
const FORMAT_VERSION := 1 const FORMAT_VERSION := 1
const MAX_BACKUP_BYTES := 256 * 1024 const MAX_BACKUP_BYTES := 256 * 1024
const MIN_PASSPHRASE_LENGTH := 12 const MIN_PASSPHRASE_LENGTH := 12
@ -110,8 +111,9 @@ func inspect_backup(
if json.parse(text) != OK or typeof(json.data) != TYPE_DICTIONARY: if json.parse(text) != OK or typeof(json.data) != TYPE_DICTIONARY:
return {"ok": false} return {"ok": false}
var data: Dictionary = json.data var data: Dictionary = json.data
var legacy_backup: bool = data.get("magic") == LEGACY_MAGIC
if ( if (
data.get("magic") != MAGIC data.get("magic") not in [MAGIC, LEGACY_MAGIC]
or data.get("format_version") != FORMAT_VERSION or data.get("format_version") != FORMAT_VERSION
or data.get("identity_type") != expected_type or data.get("identity_type") != expected_type
or data.get("algorithm") != NetworkIdentityCrypto.ALGORITHM or data.get("algorithm") != NetworkIdentityCrypto.ALGORITHM
@ -134,12 +136,23 @@ func inspect_backup(
var signature: PackedByteArray = Marshalls.base64_to_raw( var signature: PackedByteArray = Marshalls.base64_to_raw(
str(data.get("self_signature", "")) str(data.get("self_signature", ""))
) )
if not NetworkIdentityCrypto.verify_fields( var public_key: CryptoKey = NetworkIdentityCrypto.load_public_key(public_pem)
NetworkIdentityCrypto.load_public_key(public_pem), var backup_signature_valid: bool = (
"identity_backup_self_test", NetworkIdentityCrypto.verify_legacy_fields(
[expected_type, fingerprint], public_key,
signature, "identity_backup_self_test",
): [expected_type, fingerprint],
signature,
)
if legacy_backup
else NetworkIdentityCrypto.verify_fields(
public_key,
"identity_backup_self_test",
[expected_type, fingerprint],
signature,
)
)
if not backup_signature_valid:
return {"ok": false} return {"ok": false}
var fresh: PackedByteArray = NetworkIdentityCrypto.sign_fields( var fresh: PackedByteArray = NetworkIdentityCrypto.sign_fields(
key, "identity_import_self_test", [fingerprint] key, "identity_import_self_test", [fingerprint]
@ -189,7 +202,7 @@ func import_backup(
) )
_finish( _finish(
bool(result.get("ok", false)), bool(result.get("ok", false)),
"Identity imported. Restart NETfishing before multiplayer." "Identity imported. Restart straywild before multiplayer."
if bool(result.get("ok", false)) if bool(result.get("ok", false))
else "Could not install this identity backup.", else "Could not install this identity backup.",
) )

View file

@ -15,6 +15,10 @@ var _data_root: PlayerDataRoot
func configure_storage(path: String, data_root: PlayerDataRoot) -> void: func configure_storage(path: String, data_root: PlayerDataRoot) -> void:
_store_path = path _store_path = path
_data_root = data_root _data_root = data_root
_records.clear()
_loaded = false
_write_blocked = false
_expected_hash = ""
func observe(fingerprint: String, display_name: String) -> String: func observe(fingerprint: String, display_name: String) -> String:

View file

@ -0,0 +1,188 @@
class_name NetworkHomeProtocol
extends RefCounted
const PlayerHomeStateType = preload("res://homes/player_home_state.gd")
const CAPABILITY: StringName = &"portable_home_v1"
const RELIABLE_CHANNEL: int = NetworkProtocol.HOME_RELIABLE_CHANNEL
const MAX_ID_LENGTH: int = 96
const MAX_MESSAGE_LENGTH: int = 160
const MAX_TRANSITION_COORDINATE: float = 10000.0
const WORLD_SPACE: StringName = &"world"
enum TransitionAction {
ENTER,
EXIT,
}
static func make_space_id(owner_fingerprint: String) -> StringName:
return StringName("rv:%s" % owner_fingerprint)
static func manifest_digest(home_data: Dictionary) -> String:
var sanitized: Dictionary = PlayerHomeStateType.sanitize_save_data(home_data)
if sanitized.is_empty():
return ""
var canonical_owned: Array[Dictionary] = []
var owned: Dictionary = sanitized["owned_decor"]
var owned_ids: Array[String] = []
for value: Variant in owned.keys():
owned_ids.append(str(value))
owned_ids.sort()
for decor_id: String in owned_ids:
canonical_owned.append({
"id": decor_id,
"quantity": int(owned[decor_id]),
})
var placements: Array[Dictionary] = []
for value: Variant in sanitized["placements"]:
placements.append((value as Dictionary).duplicate(true))
placements.sort_custom(func(first: Dictionary, second: Dictionary) -> bool:
return str(first["instance_id"]) < str(second["instance_id"])
)
var canonical: Dictionary = {
"schema_version": int(sanitized["schema_version"]),
"tier": int(sanitized["tier"]),
"exterior_id": str(sanitized["exterior_id"]),
"privacy": int(sanitized["privacy"]),
"owned": canonical_owned,
"placements": placements,
"revision": int(sanitized["revision"]),
"next_instance_sequence": int(sanitized["next_instance_sequence"]),
}
return JSON.stringify(canonical).sha256_text()
static func manifest_signature_fields(data: Dictionary) -> Array:
return [
str(data.get("session_id", "")),
str(data.get("request_id", "")),
str(data.get("owner_fingerprint", "")),
int((data.get("home", {}) as Dictionary).get("revision", -1)),
str(data.get("digest", "")),
]
static func validate_manifest_request(value: Variant) -> bool:
if typeof(value) != TYPE_DICTIONARY:
return false
var data: Dictionary = value
if (
not _valid_id(data.get("request_id"))
or not _valid_id(data.get("session_id"))
or not NetworkIdentityCrypto.valid_fingerprint(
data.get("owner_fingerprint")
)
or typeof(data.get("home")) != TYPE_DICTIONARY
or typeof(data.get("digest")) != TYPE_STRING
or str(data["digest"]).length() != 64
or typeof(data.get("sender_signature")) != TYPE_PACKED_BYTE_ARRAY
):
return false
var sanitized: Dictionary = PlayerHomeStateType.sanitize_save_data(
data["home"]
)
return (
not sanitized.is_empty()
and manifest_digest(sanitized) == str(data["digest"])
)
static func validate_manifest_broadcast(value: Variant) -> bool:
if typeof(value) != TYPE_DICTIONARY:
return false
var data: Dictionary = value
return (
typeof(data.get("owner_peer_id")) == TYPE_INT
and int(data["owner_peer_id"]) > 0
and NetworkIdentityCrypto.valid_fingerprint(
data.get("owner_fingerprint")
)
and typeof(data.get("home")) == TYPE_DICTIONARY
and not PlayerHomeStateType.sanitize_save_data(data["home"]).is_empty()
and _valid_transform(data.get("exterior_transform"))
and typeof(data.get("interior_index")) == TYPE_INT
and int(data["interior_index"]) >= 0
and int(data["interior_index"]) < 128
)
static func transition_signature_fields(data: Dictionary) -> Array:
return [
str(data.get("session_id", "")),
str(data.get("request_id", "")),
int(data.get("action", -1)),
str(data.get("owner_fingerprint", "")),
]
static func validate_transition_request(value: Variant) -> bool:
if typeof(value) != TYPE_DICTIONARY:
return false
var data: Dictionary = value
return (
_valid_id(data.get("request_id"))
and _valid_id(data.get("session_id"))
and typeof(data.get("action")) == TYPE_INT
and int(data["action"]) >= TransitionAction.ENTER
and int(data["action"]) <= TransitionAction.EXIT
and NetworkIdentityCrypto.valid_fingerprint(
data.get("owner_fingerprint")
)
and typeof(data.get("sender_signature")) == TYPE_PACKED_BYTE_ARRAY
)
static func transform_to_array(value: Transform3D) -> Array[float]:
return [
value.basis.x.x, value.basis.x.y, value.basis.x.z,
value.basis.y.x, value.basis.y.y, value.basis.y.z,
value.basis.z.x, value.basis.z.y, value.basis.z.z,
value.origin.x, value.origin.y, value.origin.z,
]
static func array_to_transform(value: Variant) -> Transform3D:
if not validate_transition_transform(value):
return Transform3D()
var fields: Array = value
return Transform3D(
Basis(
Vector3(float(fields[0]), float(fields[1]), float(fields[2])),
Vector3(float(fields[3]), float(fields[4]), float(fields[5])),
Vector3(float(fields[6]), float(fields[7]), float(fields[8])),
),
Vector3(float(fields[9]), float(fields[10]), float(fields[11])),
)
static func validate_transition_transform(value: Variant) -> bool:
if not _valid_transform(value):
return false
var fields: Array = value
return (
absf(float(fields[9])) <= MAX_TRANSITION_COORDINATE
and absf(float(fields[10])) <= MAX_TRANSITION_COORDINATE
and absf(float(fields[11])) <= MAX_TRANSITION_COORDINATE
)
static func _valid_transform(value: Variant) -> bool:
if typeof(value) != TYPE_ARRAY or (value as Array).size() != 12:
return false
for component: Variant in value:
if (
typeof(component) not in [TYPE_FLOAT, TYPE_INT]
or not is_finite(float(component))
):
return false
return true
static func _valid_id(value: Variant) -> bool:
return (
typeof(value) == TYPE_STRING
and not str(value).is_empty()
and str(value).length() <= MAX_ID_LENGTH
)

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

@ -5,7 +5,8 @@ const ALGORITHM: String = "RSA-3072-SHA256"
const FINGERPRINT_LENGTH: int = 64 const FINGERPRINT_LENGTH: int = 64
const MAX_PUBLIC_KEY_BYTES: int = 8192 const MAX_PUBLIC_KEY_BYTES: int = 8192
const MAX_SIGNATURE_BYTES: int = 1024 const MAX_SIGNATURE_BYTES: int = 1024
const DOMAIN_PREFIX: String = "NETFISHING" const DOMAIN_PREFIX: String = "straywild"
const LEGACY_DOMAIN_PREFIX: String = "NETFISHING"
const IDENTITY_VERSION: String = "identity_v1" const IDENTITY_VERSION: String = "identity_v1"
@ -50,8 +51,16 @@ static func secure_id(byte_count: int = 16) -> String:
static func canonical_bytes(domain: String, fields: Array) -> PackedByteArray: static func canonical_bytes(domain: String, fields: Array) -> PackedByteArray:
return canonical_bytes_for_prefix(DOMAIN_PREFIX, domain, fields)
static func canonical_bytes_for_prefix(
prefix: String,
domain: String,
fields: Array,
) -> PackedByteArray:
var output := PackedByteArray() var output := PackedByteArray()
_append_string(output, DOMAIN_PREFIX) _append_string(output, prefix)
_append_string(output, IDENTITY_VERSION) _append_string(output, IDENTITY_VERSION)
_append_string(output, domain) _append_string(output, domain)
for value: Variant in fields: for value: Variant in fields:
@ -117,6 +126,37 @@ static func verify_fields(
) )
static func verify_legacy_fields(
public_key: CryptoKey,
domain: String,
fields: Array,
signature: PackedByteArray,
) -> bool:
if (
public_key == null
or signature.is_empty()
or signature.size() > MAX_SIGNATURE_BYTES
):
return false
var bytes: PackedByteArray = canonical_bytes_for_prefix(
LEGACY_DOMAIN_PREFIX,
domain,
fields,
)
if bytes.is_empty():
return false
var context := HashingContext.new()
if context.start(HashingContext.HASH_SHA256) != OK:
return false
context.update(bytes)
return Crypto.new().verify(
HashingContext.HASH_SHA256,
context.finish(),
signature,
public_key,
)
static func load_public_key(public_pem: String) -> CryptoKey: static func load_public_key(public_pem: String) -> CryptoKey:
var normalized := normalize_public_pem(public_pem) var normalized := normalize_public_pem(public_pem)
if normalized.is_empty() or normalized.to_utf8_buffer().size() > MAX_PUBLIC_KEY_BYTES: if normalized.is_empty() or normalized.to_utf8_buffer().size() > MAX_PUBLIC_KEY_BYTES:

View file

@ -5,6 +5,7 @@ const FishingRodDataType = preload("res://items/fishing_rod_data.gd")
const ArtShopStockType = preload("res://economy/art_shop_stock.gd") const ArtShopStockType = preload("res://economy/art_shop_stock.gd")
const FishingShopStockType = preload("res://economy/fishing_shop_stock.gd") const FishingShopStockType = preload("res://economy/fishing_shop_stock.gd")
const DecorCatalogType = preload("res://homes/decor_catalog.gd")
const MAX_LEDGER_ENTRIES: int = 64 const MAX_LEDGER_ENTRIES: int = 64
@ -394,6 +395,10 @@ func _apply_equipped(data: Dictionary) -> void:
item_id == FishingShopStockType.STANDARD_SHOVEL_ID item_id == FishingShopStockType.STANDARD_SHOVEL_ID
and bool(data["owns_item"]), and bool(data["owns_item"]),
) )
avatar.set_active_decor_package(
DecorCatalogType.has_product(item_id)
and bool(data["owns_item"]),
)
equipped_state_changed.emit( equipped_state_changed.emit(
peer_id, StringName(str(data["item_id"])), int(data["category"]) peer_id, StringName(str(data["item_id"])), int(data["category"])
) )

View file

@ -21,6 +21,8 @@ var _future_version := false
func configure_storage(path: String, data_root: PlayerDataRoot) -> void: func configure_storage(path: String, data_root: PlayerDataRoot) -> void:
_profile_path = path _profile_path = path
_data_root = data_root _data_root = data_root
_expected_hash = ""
_future_version = false
func _temp_path() -> String: func _temp_path() -> String:

View file

@ -51,6 +51,17 @@ func setup(
_apply_local_voice_to_avatar() _apply_local_voice_to_avatar()
func reload_persisted_profile() -> bool:
if (
not _preferences.load_or_create()
or not _appearance_store.load_preferences()
):
return false
_apply_to_avatar(1, _appearance_store.get_snapshot())
_apply_local_voice_to_avatar()
return true
func get_persisted_name() -> String: func get_persisted_name() -> String:
return _preferences.display_name return _preferences.display_name

View file

@ -3,7 +3,7 @@ extends RefCounted
const WorldLayoutType = preload("res://world/world_layout.gd") const WorldLayoutType = preload("res://world/world_layout.gd")
const PROTOCOL_VERSION: int = 10 const PROTOCOL_VERSION: int = 12
const GAME_BUILD: String = "prealpha" const GAME_BUILD: String = "prealpha"
const MAX_GAME_VERSION_LENGTH: int = 64 const MAX_GAME_VERSION_LENGTH: int = 64
const MAX_DISPLAY_NAME_LENGTH: int = 24 const MAX_DISPLAY_NAME_LENGTH: int = 24
@ -17,9 +17,9 @@ const MAX_SIGNATURE_LENGTH: int = 2048
# 8 reliable ordered session chat, 9 reliable private session mail, # 8 reliable ordered session chat, 9 reliable private session mail,
# 10 observer-only fishing summaries, 11 reliable movement animation state, # 10 observer-only fishing summaries, 11 reliable movement animation state,
# 12 reliable fish showcase state, 13 reliable world-spawn events, # 12 reliable fish showcase state, 13 reliable world-spawn events,
# 14 reliable artwork state, 15 world-spawn snapshots, and 16 reliable # 14 reliable artwork state, 15 world-spawn snapshots, 16 reliable fishing
# fishing lifecycle. Bulk world/art payloads must not head-of-line block held # lifecycle, and 17 reliable portable-home state. Bulk world/art payloads must
# items, fishing, or animation presentation. # not head-of-line block held items, fishing, animation, or home transitions.
const SALE_RELIABLE_CHANNEL: int = 5 const SALE_RELIABLE_CHANNEL: int = 5
const SHOP_RELIABLE_CHANNEL: int = 6 const SHOP_RELIABLE_CHANNEL: int = 6
const ITEM_RELIABLE_CHANNEL: int = 7 const ITEM_RELIABLE_CHANNEL: int = 7
@ -30,7 +30,8 @@ const SHOWCASE_RELIABLE_CHANNEL: int = 12
const WORLD_SPAWN_RELIABLE_CHANNEL: int = 13 const WORLD_SPAWN_RELIABLE_CHANNEL: int = 13
const DRAWING_RELIABLE_CHANNEL: int = 14 const DRAWING_RELIABLE_CHANNEL: int = 14
const FISHING_RELIABLE_CHANNEL: int = 16 const FISHING_RELIABLE_CHANNEL: int = 16
const ENET_CHANNEL_COUNT: int = 17 const HOME_RELIABLE_CHANNEL: int = 17
const ENET_CHANNEL_COUNT: int = 18
const SURFACE_DRAWING_CAPABILITY: String = "surface_drawing_v2" const SURFACE_DRAWING_CAPABILITY: String = "surface_drawing_v2"
const ART_SHOP_CAPABILITY: String = "art_shop_v1" const ART_SHOP_CAPABILITY: String = "art_shop_v1"
const BACKPACK_SHOP_CAPABILITY: String = "backpack_shop_v1" const BACKPACK_SHOP_CAPABILITY: String = "backpack_shop_v1"
@ -45,6 +46,7 @@ const WORLD_LAYOUT_CAPABILITY: String = "world_layout_v1"
const FRIENDS_CAPABILITY: String = "friends_v1" const FRIENDS_CAPABILITY: String = "friends_v1"
const MOVEMENT_RECONCILIATION_CAPABILITY: String = "movement_reconciliation_v2" const MOVEMENT_RECONCILIATION_CAPABILITY: String = "movement_reconciliation_v2"
const FISHING_REPLICATION_CAPABILITY: String = "fishing_replication_v2" const FISHING_REPLICATION_CAPABILITY: String = "fishing_replication_v2"
const HOME_CAPABILITY: String = "portable_home_v1"
const DEFAULT_WORLD_SEED: int = 13001 const DEFAULT_WORLD_SEED: int = 13001
const MAX_WORLD_SEED: int = 2147483646 const MAX_WORLD_SEED: int = 2147483646
@ -202,6 +204,7 @@ static func make_client_hello(
FRIENDS_CAPABILITY, FRIENDS_CAPABILITY,
MOVEMENT_RECONCILIATION_CAPABILITY, MOVEMENT_RECONCILIATION_CAPABILITY,
FISHING_REPLICATION_CAPABILITY, FISHING_REPLICATION_CAPABILITY,
HOME_CAPABILITY,
]), ]),
"cosmetic_snapshot": cosmetic_snapshot, "cosmetic_snapshot": cosmetic_snapshot,
"identity_fingerprint": identity_fingerprint, "identity_fingerprint": identity_fingerprint,
@ -304,7 +307,7 @@ static func make_server_hello(
assigned_peer_id: int, assigned_peer_id: int,
player_count: int, player_count: int,
max_players: int, max_players: int,
server_display_name: String = "NETfishing", server_display_name: String = "straywild",
world_seed: int = DEFAULT_WORLD_SEED, world_seed: int = DEFAULT_WORLD_SEED,
world_layout: StringName = WorldLayoutType.GENERATED, world_layout: StringName = WorldLayoutType.GENERATED,
) -> Dictionary: ) -> Dictionary:
@ -342,6 +345,7 @@ static func make_server_hello(
FRIENDS_CAPABILITY, FRIENDS_CAPABILITY,
MOVEMENT_RECONCILIATION_CAPABILITY, MOVEMENT_RECONCILIATION_CAPABILITY,
FISHING_REPLICATION_CAPABILITY, FISHING_REPLICATION_CAPABILITY,
HOME_CAPABILITY,
"chat_v1", "chat_v1",
"mail_v1", "mail_v1",
"profile_v1", "profile_v1",
@ -372,17 +376,17 @@ static func rejection_text(code: int) -> String:
return "You are not permitted to join this server." return "You are not permitted to join this server."
RejectionCode.CLIENT_OUTDATED: RejectionCode.CLIENT_OUTDATED:
return ( return (
"Your NETfishing version is out of date. " "Your straywild version is out of date. "
+ "Update the game to join this server." + "Update the game to join this server."
) )
RejectionCode.SERVER_OUTDATED: RejectionCode.SERVER_OUTDATED:
return ( return (
"This server is out of date. " "This server is out of date. "
+ "The host needs to update NETfishing." + "The host needs to update straywild."
) )
RejectionCode.VERSION_MISMATCH: RejectionCode.VERSION_MISMATCH:
return ( return (
"This game and server use different NETfishing versions. " "This game and server use different straywild versions. "
+ "Update both to the latest release." + "Update both to the latest release."
) )
_: _:

View file

@ -28,6 +28,7 @@ const MAX_PENDING_MOVEMENT_INPUTS: int = 96
const ANIMATION_REFRESH_INTERVAL: float = 1.0 const ANIMATION_REFRESH_INTERVAL: float = 1.0
const MAX_MOVEMENT_INPUT_SEQUENCE: int = 2147483647 const MAX_MOVEMENT_INPUT_SEQUENCE: int = 2147483647
const MAX_MOVEMENT_ONE_WAY_TRANSIT_SECONDS: float = 0.25 const MAX_MOVEMENT_ONE_WAY_TRANSIT_SECONDS: float = 0.25
const SPACE_TRANSITION_SNAPSHOT_GUARD_DISTANCE: float = 12.0
const MOVEMENT_FLAG_JUMP: int = 1 << 0 const MOVEMENT_FLAG_JUMP: int = 1 << 0
const MOVEMENT_FLAG_SPRINT: int = 1 << 1 const MOVEMENT_FLAG_SPRINT: int = 1 << 1
@ -124,6 +125,9 @@ var _pending_movement_inputs: Array[Dictionary] = []
var _snapshot_accumulator: float = 0.0 var _snapshot_accumulator: float = 0.0
var _movement_snapshot_tick: int = 0 var _movement_snapshot_tick: int = 0
var _last_local_snapshot_received_msec: int = 0 var _last_local_snapshot_received_msec: int = 0
var _local_space_transition_guard_active: bool = false
var _local_space_transition_guard_position: Vector3 = Vector3.ZERO
var _local_space_transition_minimum_ack: int = 0
var _animation_refresh_accumulator: float = 0.0 var _animation_refresh_accumulator: float = 0.0
var _last_animation_state_by_peer: Dictionary[int, Dictionary] = {} var _last_animation_state_by_peer: Dictionary[int, Dictionary] = {}
var _pending_animation_state_by_peer: Dictionary[int, Dictionary] = {} var _pending_animation_state_by_peer: Dictionary[int, Dictionary] = {}
@ -141,6 +145,7 @@ var _last_server_protocol_version: int = 0
var _last_server_world_seed: int = NetworkProtocol.DEFAULT_WORLD_SEED var _last_server_world_seed: int = NetworkProtocol.DEFAULT_WORLD_SEED
var _last_server_world_layout: StringName = WorldLayout.GENERATED var _last_server_world_layout: StringName = WorldLayout.GENERATED
var _server_capabilities: PackedStringArray = PackedStringArray() var _server_capabilities: PackedStringArray = PackedStringArray()
var _peer_space_ids: Dictionary[int, StringName] = {}
var _profile_ready: bool = false var _profile_ready: bool = false
var _player_identity: PlayerIdentityStore var _player_identity: PlayerIdentityStore
var _host_identity: HostIdentityStore var _host_identity: HostIdentityStore
@ -160,7 +165,7 @@ var _local_appearance_snapshot: Dictionary = (
) )
var _moderation_disconnect_message := "" var _moderation_disconnect_message := ""
var _host_port: int = 0 var _host_port: int = 0
var _session_display_name: String = "NETfishing Room" var _session_display_name: String = "straywild Room"
var _dedicated_host: bool = false var _dedicated_host: bool = false
var _configured_operator_fingerprints: Dictionary[String, bool] = {} var _configured_operator_fingerprints: Dictionary[String, bool] = {}
var _session_operator_fingerprints: Dictionary[String, bool] = {} var _session_operator_fingerprints: Dictionary[String, bool] = {}
@ -334,6 +339,7 @@ func _register_player_host() -> void:
NetworkProtocol.FRIENDS_CAPABILITY, NetworkProtocol.FRIENDS_CAPABILITY,
NetworkProtocol.MOVEMENT_RECONCILIATION_CAPABILITY, NetworkProtocol.MOVEMENT_RECONCILIATION_CAPABILITY,
NetworkProtocol.FISHING_REPLICATION_CAPABILITY, NetworkProtocol.FISHING_REPLICATION_CAPABILITY,
NetworkProtocol.HOME_CAPABILITY,
]), ]),
) )
_registry.update_appearance(1, _local_appearance_snapshot) _registry.update_appearance(1, _local_appearance_snapshot)
@ -432,7 +438,7 @@ func set_peer_operator(
func set_session_display_name(value: String) -> void: func set_session_display_name(value: String) -> void:
var cleaned: String = value.strip_edges().left(48) var cleaned: String = value.strip_edges().left(48)
if cleaned.is_empty(): if cleaned.is_empty():
cleaned = "NETfishing Room" cleaned = "straywild Room"
if cleaned == _session_display_name: if cleaned == _session_display_name:
return return
_session_display_name = cleaned _session_display_name = cleaned
@ -661,6 +667,7 @@ func supports_server_capability(capability: StringName) -> bool:
NetworkProtocol.FRIENDS_CAPABILITY, NetworkProtocol.FRIENDS_CAPABILITY,
NetworkProtocol.MOVEMENT_RECONCILIATION_CAPABILITY, NetworkProtocol.MOVEMENT_RECONCILIATION_CAPABILITY,
NetworkProtocol.FISHING_REPLICATION_CAPABILITY, NetworkProtocol.FISHING_REPLICATION_CAPABILITY,
NetworkProtocol.HOME_CAPABILITY,
"chat_v1", "chat_v1",
"mail_v1", "mail_v1",
"profile_v1", "profile_v1",
@ -685,6 +692,55 @@ func get_authenticated_peer_ids() -> Array[int]:
return _registry.get_peer_ids() return _registry.get_peer_ids()
func set_peer_space(peer_id: int, space_id: StringName) -> bool:
if not _registry.has_peer(peer_id) or space_id.is_empty():
return false
_peer_space_ids[peer_id] = space_id
return true
func get_peer_space(peer_id: int) -> StringName:
return _peer_space_ids.get(peer_id, &"world")
func peers_share_space(first_peer_id: int, second_peer_id: int) -> bool:
return get_peer_space(first_peer_id) == get_peer_space(second_peer_id)
func apply_authoritative_space_transition(
peer_id: int,
target_transform: Transform3D,
acknowledged_input: int,
) -> bool:
if not target_transform.origin.is_finite():
return false
var local_peer_id: int = get_local_peer_id()
var local_transition: bool = peer_id == local_peer_id
var avatar: Player = _spawn_service.get_avatar(peer_id)
# Remote home transitions can arrive before their player-spawn message. Keep
# the new space assignment so the later spawn inherits correct visibility;
# only a missing local avatar is a transition failure.
if avatar == null:
return not local_transition
avatar.apply_network_space_transition(target_transform, local_transition)
if (
local_transition
and state == State.JOINED_CLIENT
):
# Movement snapshots use another ENet channel and a pre-transition audit
# can therefore arrive after the reliable home transition. Clear local
# prediction and reject those old-world snapshots until the host publishes
# its first audit from the new space.
_pending_movement_inputs.clear()
_last_local_snapshot_received_msec = 0
_last_input_state_hash = 0
_idle_input_accumulator = IDLE_INPUT_INTERVAL
_local_space_transition_guard_active = true
_local_space_transition_guard_position = target_transform.origin
_local_space_transition_minimum_ack = maxi(acknowledged_input, 0)
return true
func get_peer_rtt_ms(peer_id: int) -> int: func get_peer_rtt_ms(peer_id: int) -> int:
if peer_id == 1: if peer_id == 1:
return 0 return 0
@ -1030,6 +1086,7 @@ func _on_peer_disconnected(peer_id: int) -> void:
_pending_identity_challenges.erase(peer_id) _pending_identity_challenges.erase(peer_id)
_authenticated_identity_cache.erase(peer_id) _authenticated_identity_cache.erase(peer_id)
_operator_peer_ids.erase(peer_id) _operator_peer_ids.erase(peer_id)
_peer_space_ids.erase(peer_id)
_last_animation_state_by_peer.erase(peer_id) _last_animation_state_by_peer.erase(peer_id)
_pending_animation_state_by_peer.erase(peer_id) _pending_animation_state_by_peer.erase(peer_id)
var recovery_attempt: String = _recovery_attempts.get(peer_id, "") var recovery_attempt: String = _recovery_attempts.get(peer_id, "")
@ -1187,7 +1244,7 @@ func resolve_server_trust(accepted: bool) -> void:
if not _server_trust.trust( if not _server_trust.trust(
get_current_endpoint(), get_current_endpoint(),
str(_pending_server_proof["server_fingerprint"]), str(_pending_server_proof["server_fingerprint"]),
"NETfishing", "straywild",
): ):
_fail_identity("The server identity could not be pinned.") _fail_identity("The server identity could not be pinned.")
return return
@ -1313,6 +1370,7 @@ func submit_client_hello(data: Dictionary) -> void:
for required_capability: String in [ for required_capability: String in [
NetworkProtocol.MOVEMENT_RECONCILIATION_CAPABILITY, NetworkProtocol.MOVEMENT_RECONCILIATION_CAPABILITY,
NetworkProtocol.FISHING_REPLICATION_CAPABILITY, NetworkProtocol.FISHING_REPLICATION_CAPABILITY,
NetworkProtocol.HOME_CAPABILITY,
]: ]:
if required_capability not in client_capabilities: if required_capability not in client_capabilities:
_reject_peer( _reject_peer(
@ -1546,6 +1604,7 @@ func receive_server_hello(data: Dictionary) -> void:
for required_capability: String in [ for required_capability: String in [
NetworkProtocol.MOVEMENT_RECONCILIATION_CAPABILITY, NetworkProtocol.MOVEMENT_RECONCILIATION_CAPABILITY,
NetworkProtocol.FISHING_REPLICATION_CAPABILITY, NetworkProtocol.FISHING_REPLICATION_CAPABILITY,
NetworkProtocol.HOME_CAPABILITY,
]: ]:
if required_capability not in _server_capabilities: if required_capability not in _server_capabilities:
_teardown_peer() _teardown_peer()
@ -1578,6 +1637,7 @@ func receive_server_hello(data: Dictionary) -> void:
NetworkProtocol.FRIENDS_CAPABILITY, NetworkProtocol.FRIENDS_CAPABILITY,
NetworkProtocol.MOVEMENT_RECONCILIATION_CAPABILITY, NetworkProtocol.MOVEMENT_RECONCILIATION_CAPABILITY,
NetworkProtocol.FISHING_REPLICATION_CAPABILITY, NetworkProtocol.FISHING_REPLICATION_CAPABILITY,
NetworkProtocol.HOME_CAPABILITY,
]), ]),
) )
_registry.update_appearance(local_peer_id, _local_appearance_snapshot) _registry.update_appearance(local_peer_id, _local_appearance_snapshot)
@ -1623,6 +1683,7 @@ func receive_peer_despawn(peer_id: int) -> void:
if state != State.JOINED_CLIENT: if state != State.JOINED_CLIENT:
return return
_operator_peer_ids.erase(peer_id) _operator_peer_ids.erase(peer_id)
_peer_space_ids.erase(peer_id)
_last_animation_state_by_peer.erase(peer_id) _last_animation_state_by_peer.erase(peer_id)
_pending_animation_state_by_peer.erase(peer_id) _pending_animation_state_by_peer.erase(peer_id)
_registry.remove_peer(peer_id) _registry.remove_peer(peer_id)
@ -2247,6 +2308,8 @@ func _broadcast_movement_snapshots() -> void:
continue continue
var snapshots: Array = [] var snapshots: Array = []
for subject_id: int in peer_ids: for subject_id: int in peer_ids:
if not peers_share_space(recipient_id, subject_id):
continue
var subject_avatar: Player = _spawn_service.get_avatar(subject_id) var subject_avatar: Player = _spawn_service.get_avatar(subject_id)
if subject_avatar == null: if subject_avatar == null:
continue continue
@ -2315,7 +2378,7 @@ func _broadcast_movement_animation_updates(peer_ids: Array[int]) -> void:
_animation_refresh_accumulator, _animation_refresh_accumulator,
ANIMATION_REFRESH_INTERVAL, ANIMATION_REFRESH_INTERVAL,
) )
var updates: Array = [] var updates_by_subject: Dictionary[int, Array] = {}
for subject_id: int in peer_ids: for subject_id: int in peer_ids:
var avatar: Player = _spawn_service.get_avatar(subject_id) var avatar: Player = _spawn_service.get_avatar(subject_id)
if avatar == null: if avatar == null:
@ -2333,12 +2396,18 @@ func _broadcast_movement_animation_updates(peer_ids: Array[int]) -> void:
_last_animation_state_by_peer[subject_id] = state.duplicate(true) _last_animation_state_by_peer[subject_id] = state.duplicate(true)
var encoded: Array = _encode_movement_animation(subject_id, state) var encoded: Array = _encode_movement_animation(subject_id, state)
if not encoded.is_empty(): if not encoded.is_empty():
updates.append(encoded) updates_by_subject[subject_id] = encoded
if updates.is_empty(): if updates_by_subject.is_empty():
return return
for recipient_id: int in peer_ids: for recipient_id: int in peer_ids:
if recipient_id == 1: if recipient_id == 1:
continue continue
var updates: Array = []
for subject_id: int in updates_by_subject:
if peers_share_space(recipient_id, subject_id):
updates.append(updates_by_subject[subject_id])
if updates.is_empty():
continue
receive_movement_animations.rpc_id(recipient_id, updates) receive_movement_animations.rpc_id(recipient_id, updates)
_movement_animation_packets_sent += 1 _movement_animation_packets_sent += 1
_movement_animation_states_sent += updates.size() _movement_animation_states_sent += updates.size()
@ -2538,6 +2607,8 @@ func receive_movement_snapshots(encoded_snapshots: Array) -> void:
if avatar == null: if avatar == null:
continue continue
if peer_id == local_peer_id: if peer_id == local_peer_id:
if _reject_stale_space_transition_snapshot(snapshot):
continue
_discard_acknowledged_movement_inputs( _discard_acknowledged_movement_inputs(
int(snapshot.get("acknowledged_input", 0)) int(snapshot.get("acknowledged_input", 0))
) )
@ -2555,6 +2626,33 @@ func receive_movement_snapshots(encoded_snapshots: Array) -> void:
) )
func _reject_stale_space_transition_snapshot(snapshot: Dictionary) -> bool:
if not _local_space_transition_guard_active:
return false
var position_values: Array = snapshot.get("position", [])
if position_values.size() != 3:
return true
var position := Vector3(
float(position_values[0]),
float(position_values[1]),
float(position_values[2]),
)
var acknowledged_input: int = int(
snapshot.get("acknowledged_input", 0)
)
if acknowledged_input < _local_space_transition_minimum_ack:
return true
if acknowledged_input > _local_space_transition_minimum_ack:
_local_space_transition_guard_active = false
return false
if position.distance_to(_local_space_transition_guard_position) > (
SPACE_TRANSITION_SNAPSHOT_GUARD_DISTANCE
):
return true
_local_space_transition_guard_active = false
return false
func _discard_acknowledged_movement_inputs(acknowledged_sequence: int) -> void: func _discard_acknowledged_movement_inputs(acknowledged_sequence: int) -> void:
while ( while (
not _pending_movement_inputs.is_empty() not _pending_movement_inputs.is_empty()
@ -2883,11 +2981,15 @@ func _teardown_peer() -> void:
_snapshot_accumulator = 0.0 _snapshot_accumulator = 0.0
_movement_snapshot_tick = 0 _movement_snapshot_tick = 0
_last_local_snapshot_received_msec = 0 _last_local_snapshot_received_msec = 0
_local_space_transition_guard_active = false
_local_space_transition_guard_position = Vector3.ZERO
_local_space_transition_minimum_ack = 0
_animation_refresh_accumulator = 0.0 _animation_refresh_accumulator = 0.0
_last_animation_state_by_peer.clear() _last_animation_state_by_peer.clear()
_pending_animation_state_by_peer.clear() _pending_animation_state_by_peer.clear()
_last_local_animation_action_signature.clear() _last_local_animation_action_signature.clear()
_server_capabilities = PackedStringArray() _server_capabilities = PackedStringArray()
_peer_space_ids.clear()
_server_identity_fingerprint = "" _server_identity_fingerprint = ""
_server_identity_public_key = "" _server_identity_public_key = ""
_session_identity_keys.clear() _session_identity_keys.clear()

View file

@ -4,6 +4,7 @@ extends RefCounted
const CAPABILITY: StringName = &"shop_v1" const CAPABILITY: StringName = &"shop_v1"
const ART_CAPABILITY: StringName = &"art_shop_v1" const ART_CAPABILITY: StringName = &"art_shop_v1"
const BACKPACK_CAPABILITY: StringName = &"backpack_shop_v1" const BACKPACK_CAPABILITY: StringName = &"backpack_shop_v1"
const DECOR_CAPABILITY: StringName = &"portable_home_v1"
const RELIABLE_CHANNEL: int = NetworkProtocol.SHOP_RELIABLE_CHANNEL const RELIABLE_CHANNEL: int = NetworkProtocol.SHOP_RELIABLE_CHANNEL
const MAX_ID_LENGTH: int = 96 const MAX_ID_LENGTH: int = 96
const MAX_MESSAGE_LENGTH: int = 160 const MAX_MESSAGE_LENGTH: int = 160
@ -18,6 +19,7 @@ enum ProductCategory {
ART_KIT, ART_KIT,
ART_UPGRADE, ART_UPGRADE,
BACKPACK_CAPACITY_UPGRADE, BACKPACK_CAPACITY_UPGRADE,
DECOR,
} }

View file

@ -8,8 +8,10 @@ const ItemDataType = preload("res://items/item_data.gd")
const FishingRodDataType = preload("res://items/fishing_rod_data.gd") const FishingRodDataType = preload("res://items/fishing_rod_data.gd")
const OwnedItemType = preload("res://items/owned_item.gd") const OwnedItemType = preload("res://items/owned_item.gd")
const ArtShopStockType = preload("res://economy/art_shop_stock.gd") const ArtShopStockType = preload("res://economy/art_shop_stock.gd")
const DecorCatalogType = preload("res://homes/decor_catalog.gd")
const SHOP_ID: StringName = &"main_fishing_shop" const FISHING_SHOP_ID: StringName = &"main_fishing_shop"
const DECOR_SHOP_ID: StringName = &"rv_decor_shop"
const REEL_PRODUCT_ID: StringName = &"reel_speed_upgrade" const REEL_PRODUCT_ID: StringName = &"reel_speed_upgrade"
const BARRIER_PRODUCT_ID: StringName = &"barrier_power_upgrade" const BARRIER_PRODUCT_ID: StringName = &"barrier_power_upgrade"
const COOLER_PRODUCT_ID: StringName = &"cooler_capacity_upgrade" const COOLER_PRODUCT_ID: StringName = &"cooler_capacity_upgrade"
@ -31,6 +33,7 @@ var _session: NetworkSession
var _spawn_service: PlayerSpawnService var _spawn_service: PlayerSpawnService
var _network_fishing: NetworkFishingService var _network_fishing: NetworkFishingService
var _interaction: FishingShopInteraction var _interaction: FishingShopInteraction
var _decor_interaction: DecorShopInteraction
var _wallet: PlayerWallet var _wallet: PlayerWallet
var _bag: PlayerBag var _bag: PlayerBag
var _item_catalog: ItemCatalog var _item_catalog: ItemCatalog
@ -47,6 +50,8 @@ var _received_results: Dictionary[String, bool] = {}
var _applied_results: Dictionary[String, bool] = {} var _applied_results: Dictionary[String, bool] = {}
var _pending_local_request: Dictionary = {} var _pending_local_request: Dictionary = {}
var _reservations: PlayerAssetReservationService var _reservations: PlayerAssetReservationService
var _home_state: PlayerHomeState
var _network_home: NetworkHomeService
func setup( func setup(
@ -63,6 +68,9 @@ func setup(
save_manager: PlayerSaveManager, save_manager: PlayerSaveManager,
reservations: PlayerAssetReservationService, reservations: PlayerAssetReservationService,
inventory_layout: PlayerInventoryLayout = null, inventory_layout: PlayerInventoryLayout = null,
home_state: PlayerHomeState = null,
network_home: NetworkHomeService = null,
decor_interaction: DecorShopInteraction = null,
) -> void: ) -> void:
_session = session _session = session
_spawn_service = spawn_service _spawn_service = spawn_service
@ -77,6 +85,9 @@ func setup(
_save_manager = save_manager _save_manager = save_manager
_reservations = reservations _reservations = reservations
_inventory_layout = inventory_layout _inventory_layout = inventory_layout
_home_state = home_state
_network_home = network_home
_decor_interaction = decor_interaction
if not _session.peer_removed.is_connected(_on_peer_removed): if not _session.peer_removed.is_connected(_on_peer_removed):
_session.peer_removed.connect(_on_peer_removed) _session.peer_removed.connect(_on_peer_removed)
if not _session.state_changed.is_connected(_on_session_state_changed): if not _session.state_changed.is_connected(_on_session_state_changed):
@ -87,6 +98,12 @@ func set_shop_interaction(interaction: FishingShopInteraction) -> void:
_interaction = interaction _interaction = interaction
func set_decor_shop_interaction(
interaction: DecorShopInteraction,
) -> void:
_decor_interaction = interaction
func can_request_purchase() -> bool: func can_request_purchase() -> bool:
return ( return (
_session != null _session != null
@ -124,6 +141,19 @@ func can_request_backpack_purchase() -> bool:
) )
func can_request_decor_purchase() -> bool:
return (
can_request_purchase()
and _home_state != null
and (
_session.is_host()
or _session.supports_server_capability(
NetworkShopProtocol.DECOR_CAPABILITY
)
)
)
func is_local_purchase_pending() -> bool: func is_local_purchase_pending() -> bool:
return not _pending_local_request.is_empty() return not _pending_local_request.is_empty()
@ -193,6 +223,15 @@ func request_backpack_capacity_upgrade() -> String:
) )
func request_decor(product_id: StringName) -> String:
return _request_purchase(
product_id,
NetworkShopProtocol.ProductCategory.DECOR,
1,
_home_state.get_owned_count(product_id) if _home_state != null else 0,
)
func request_art_kit() -> String: func request_art_kit() -> String:
return _request_purchase( return _request_purchase(
ArtShopStockType.ART_KIT_ITEM_ID, ArtShopStockType.ART_KIT_ITEM_ID,
@ -269,11 +308,25 @@ func _request_purchase(
product_id, category, 0, 0, product_id, category, 0, 0,
) )
return "" return ""
if (
category == NetworkShopProtocol.ProductCategory.DECOR
and not can_request_decor_purchase()
):
local_purchase_finished.emit(
"", false, "Decor require a newer server.",
product_id, category, 0, 0,
)
return ""
var request_id: String = _new_id("shop") var request_id: String = _new_id("shop")
var shop_id: StringName = (
DECOR_SHOP_ID
if category == NetworkShopProtocol.ProductCategory.DECOR
else FISHING_SHOP_ID
)
var request: Dictionary = { var request: Dictionary = {
"request_id": request_id, "request_id": request_id,
"session_id": _session.get_session_id(), "session_id": _session.get_session_id(),
"shop_id": str(SHOP_ID), "shop_id": str(shop_id),
"product_id": str(product_id), "product_id": str(product_id),
"category": category, "category": category,
"quantity": quantity, "quantity": quantity,
@ -345,16 +398,34 @@ func _handle_purchase_request(peer_id: int, data: Dictionary) -> void:
_rejected_result(data, "A purchase is already pending.") _rejected_result(data, "A purchase is already pending.")
) )
return return
if not _is_shop_available_for_peer(peer_id): var requested_shop_id := StringName(str(data["shop_id"]))
if not _is_shop_available_for_peer(peer_id, requested_shop_id):
var avatar: Player = _spawn_service.get_avatar(peer_id) var avatar: Player = _spawn_service.get_avatar(peer_id)
var requested_interaction: FishingShopInteraction = (
_decor_interaction
if requested_shop_id == DECOR_SHOP_ID
else _interaction
)
var message: String = ( var message: String = (
"The shop is unavailable." "The shop is unavailable."
if avatar == null or _interaction == null if avatar == null or requested_interaction == null
else "Move closer to the shop." else "Move closer to the shop."
) )
_record_and_send(peer_id, _rejected_result(data, message)) _record_and_send(peer_id, _rejected_result(data, message))
return return
var result: Dictionary = _build_authoritative_result(peer_id, data) var result: Dictionary = _build_authoritative_result(peer_id, data)
if (
bool(result.get("accepted", false))
and int(result.get("category", -1))
== NetworkShopProtocol.ProductCategory.DECOR
and _network_home != null
):
_network_home.authorize_decor_purchase(
peer_id,
StringName(str(result["product_id"])),
int(result["resulting_state"]),
str(result["result_id"]),
)
_pending_by_peer[peer_id] = request_id _pending_by_peer[peer_id] = request_id
_record_and_send(peer_id, result) _record_and_send(peer_id, result)
@ -368,10 +439,24 @@ func _build_authoritative_result(
var quantity: int = request["quantity"] var quantity: int = request["quantity"]
var wallet_balance: int = request["wallet_balance"] var wallet_balance: int = request["wallet_balance"]
var current_state: int = request["current_state"] var current_state: int = request["current_state"]
if (
category == NetworkShopProtocol.ProductCategory.DECOR
and _network_home != null
):
var authoritative_count: int = (
_network_home.get_authoritative_owned_count(peer_id, product_id)
)
if authoritative_count >= 0:
current_state = authoritative_count
var cost: int = -1 var cost: int = -1
var resulting_state: int = current_state var resulting_state: int = current_state
var rejection: String = "" var rejection: String = ""
if StringName(str(request["shop_id"])) != SHOP_ID: var expected_shop_id: StringName = (
DECOR_SHOP_ID
if category == NetworkShopProtocol.ProductCategory.DECOR
else FISHING_SHOP_ID
)
if StringName(str(request["shop_id"])) != expected_shop_id:
rejection = "The shop is unavailable." rejection = "The shop is unavailable."
elif quantity < 1: elif quantity < 1:
rejection = "Purchase could not be completed." rejection = "Purchase could not be completed."
@ -545,6 +630,19 @@ func _build_authoritative_result(
resulting_state = PlayerArtUnlocks.resulting_mask( resulting_state = PlayerArtUnlocks.resulting_mask(
current_state, product_id current_state, product_id
) )
NetworkShopProtocol.ProductCategory.DECOR:
cost = DecorCatalogType.get_price(product_id)
if (
not DecorCatalogType.has_product(product_id)
or quantity != 1
or current_state < 0
or current_state
>= DecorCatalogType.MAX_OWNED_PER_PRODUCT
or cost < 0
):
rejection = "Decor is unavailable."
else:
resulting_state = current_state + 1
if rejection.is_empty() and (cost < 0 or wallet_balance < cost): if rejection.is_empty() and (cost < 0 or wallet_balance < cost):
rejection = "Insufficient funds." rejection = "Insufficient funds."
if not rejection.is_empty(): if not rejection.is_empty():
@ -667,6 +765,9 @@ func _apply_purchase_result(data: Dictionary) -> void:
if _inventory_layout != null else 0 if _inventory_layout != null else 0
) )
var art_snapshot: int = _art_unlocks.get_unlock_mask() var art_snapshot: int = _art_unlocks.get_unlock_mask()
var home_snapshot: Dictionary = (
_home_state.to_save_data() if _home_state != null else {}
)
var applied: bool = _apply_local_product(data) var applied: bool = _apply_local_product(data)
if not applied or not _save_manager.save_if_dirty(): if not applied or not _save_manager.save_if_dirty():
_bag.replace_all_items(bag_snapshot) _bag.replace_all_items(bag_snapshot)
@ -676,6 +777,8 @@ func _apply_purchase_result(data: Dictionary) -> void:
if _inventory_layout != null: if _inventory_layout != null:
_inventory_layout.restore_backpack_level(backpack_snapshot) _inventory_layout.restore_backpack_level(backpack_snapshot)
_art_unlocks.restore_mask(art_snapshot) _art_unlocks.restore_mask(art_snapshot)
if _home_state != null and not home_snapshot.is_empty():
_home_state.restore_from_save_data(home_snapshot)
_wallet.restore_balance(wallet_snapshot) _wallet.restore_balance(wallet_snapshot)
_save_manager.save_if_dirty() _save_manager.save_if_dirty()
_fail_local_apply(data, "Purchase could not be completed.") _fail_local_apply(data, "Purchase could not be completed.")
@ -696,6 +799,7 @@ func _validate_local_result(data: Dictionary) -> String:
or _cooler_capacity == null or _cooler_capacity == null
or _art_unlocks == null or _art_unlocks == null
or _save_manager == null or _save_manager == null
or _home_state == null
or _wallet.get_balance() != int(data["expected_wallet"]) or _wallet.get_balance() != int(data["expected_wallet"])
or str(data["product_id"]) or str(data["product_id"])
!= str(_pending_local_request.get("product_id", "")) != str(_pending_local_request.get("product_id", ""))
@ -806,6 +910,20 @@ func _validate_local_result(data: Dictionary) -> String:
or cost != ArtShopStockType.UPGRADE_PRICE or cost != ArtShopStockType.UPGRADE_PRICE
): ):
return "Purchase could not be completed." return "Purchase could not be completed."
NetworkShopProtocol.ProductCategory.DECOR:
if (
not DecorCatalogType.has_product(product_id)
or _home_state.get_owned_count(product_id) != expected_state
or resulting_state != expected_state + 1
or cost != DecorCatalogType.get_price(product_id)
or expected_state >= DecorCatalogType.MAX_OWNED_PER_PRODUCT
or not _bag.can_add_item(product_id, 1)
):
return (
"Your inventory is full."
if not _bag.can_add_item(product_id, 1)
else "Purchase could not be completed."
)
_: _:
return "Purchase could not be completed." return "Purchase could not be completed."
return "" return ""
@ -845,6 +963,12 @@ func _apply_local_product(data: Dictionary) -> bool:
return _art_unlocks.purchase_product( return _art_unlocks.purchase_product(
product_id, _wallet, int(data["total_cost"]) product_id, _wallet, int(data["total_cost"])
) )
NetworkShopProtocol.ProductCategory.DECOR:
return (
_wallet.debit(int(data["total_cost"]))
and _bag.add_item(product_id, 1)
and _home_state.add_owned_decor(product_id, 1)
)
return false return false
@ -921,7 +1045,7 @@ func _handle_acknowledgement(
peer_id: int, peer_id: int,
result_id: String, result_id: String,
request_id: String, request_id: String,
_applied: bool, applied: bool,
message: String, message: String,
) -> void: ) -> void:
if ( if (
@ -933,20 +1057,32 @@ func _handle_acknowledgement(
or _result_owners.get(result_id, 0) != peer_id or _result_owners.get(result_id, 0) != peer_id
): ):
return return
if _network_home != null:
_network_home.finalize_decor_purchase(
peer_id, result_id, applied
)
_acknowledged_results[result_id] = true _acknowledged_results[result_id] = true
_bound_dictionary(_acknowledged_results) _bound_dictionary(_acknowledged_results)
if _pending_by_peer.get(peer_id, "") == request_id: if _pending_by_peer.get(peer_id, "") == request_id:
_pending_by_peer.erase(peer_id) _pending_by_peer.erase(peer_id)
func _is_shop_available_for_peer(peer_id: int) -> bool: func _is_shop_available_for_peer(
peer_id: int,
shop_id: StringName,
) -> bool:
var interaction: FishingShopInteraction = (
_decor_interaction
if shop_id == DECOR_SHOP_ID
else _interaction
)
if ( if (
_session == null _session == null
or not _session.is_host() or not _session.is_host()
or not _session.is_gameplay_session_active() or not _session.is_gameplay_session_active()
or _spawn_service == null or _spawn_service == null
or _interaction == null or interaction == null
or not is_instance_valid(_interaction) or not is_instance_valid(interaction)
): ):
return false return false
var avatar: Player = _spawn_service.get_avatar(peer_id) var avatar: Player = _spawn_service.get_avatar(peer_id)
@ -957,7 +1093,7 @@ func _is_shop_available_for_peer(peer_id: int) -> bool:
_network_fishing == null _network_fishing == null
or not _network_fishing.has_peer_attempt(peer_id) or not _network_fishing.has_peer_attempt(peer_id)
) )
and _interaction.is_avatar_in_range(avatar) and interaction.is_avatar_in_range(avatar)
) )

View file

@ -250,7 +250,11 @@ func activate(
_pointer_screen_position = _clamped_pointer_position( _pointer_screen_position = _clamped_pointer_position(
initial_pointer_position initial_pointer_position
) )
_prior_mouse_mode = Input.mouse_mode # Art-kit activation can occur while the prior right-click camera drag is
# still held. Stop that drag before recording a return mode, then restore
# normal camera availability so right-click continues to work while drawing.
_stop_camera_drag_for_art_kit()
_prior_mouse_mode = _get_restorable_mouse_mode(Input.mouse_mode)
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
_update_aim() _update_aim()
_refresh_stencil_visibility() _refresh_stencil_visibility()
@ -266,6 +270,10 @@ func deactivate() -> void:
_eraser_mode = false _eraser_mode = false
_clear_armed_guide_action(false) _clear_armed_guide_action(false)
_camera_look_active = false _camera_look_active = false
# Do not leave a drag captured when drawing is closed by a menu, hotbar
# change, or session transition. The next camera drag must start on a fresh
# physical right-click press.
_stop_camera_drag_for_art_kit()
_reset_stroke() _reset_stroke()
_selected_canvas_id = "" _selected_canvas_id = ""
_hovered_canvas_id = "" _hovered_canvas_id = ""
@ -273,10 +281,24 @@ func deactivate() -> void:
_aim_hit.clear() _aim_hit.clear()
_hide_previews() _hide_previews()
_refresh_stencil_visibility() _refresh_stencil_visibility()
Input.mouse_mode = _prior_mouse_mode Input.mouse_mode = _get_restorable_mouse_mode(_prior_mouse_mode)
_emit_hud_state("") _emit_hud_state("")
func _stop_camera_drag_for_art_kit() -> void:
if _local_player == null or not is_instance_valid(_local_player):
return
var camera_input_enabled: bool = _local_player.is_camera_input_enabled()
_local_player.set_camera_input_enabled(false)
_local_player.set_camera_input_enabled(camera_input_enabled)
func _get_restorable_mouse_mode(mouse_mode: Input.MouseMode) -> Input.MouseMode:
if mouse_mode == Input.MOUSE_MODE_CAPTURED:
return Input.MOUSE_MODE_VISIBLE
return mouse_mode
func is_active() -> bool: func is_active() -> bool:
return _active return _active
@ -879,7 +901,7 @@ func export_canvas_png(canvas_id: String) -> String:
_emit_hud_state("paint something before exporting") _emit_hud_state("paint something before exporting")
return "" return ""
if _data_root == null or _data_root.root_path.is_empty(): if _data_root == null or _data_root.root_path.is_empty():
_emit_hud_state("choose a NETfishing data folder before exporting") _emit_hud_state("choose a straywild data folder before exporting")
return "" return ""
var export_directory: String = _artwork_directory() var export_directory: String = _artwork_directory()
if DirAccess.make_dir_recursive_absolute(export_directory) != OK: if DirAccess.make_dir_recursive_absolute(export_directory) != OK:
@ -990,7 +1012,7 @@ func _stamp_entry_is_unlocked(entry: Dictionary) -> bool:
func _next_artwork_export_path(directory: String) -> String: func _next_artwork_export_path(directory: String) -> String:
var date_time: Dictionary = Time.get_datetime_dict_from_system() var date_time: Dictionary = Time.get_datetime_dict_from_system()
var stem: String = "netfishing-artwork-%04d%02d%02d-%02d%02d%02d" % [ var stem: String = "straywild-artwork-%04d%02d%02d-%02d%02d%02d" % [
int(date_time.get("year", 0)), int(date_time.get("year", 0)),
int(date_time.get("month", 0)), int(date_time.get("month", 0)),
int(date_time.get("day", 0)), int(date_time.get("day", 0)),

View file

@ -518,7 +518,12 @@ func _sample_surface_position(
) )
if triangles.is_empty() or total_area <= 0.0: if triangles.is_empty() or total_area <= 0.0:
return Vector3(INF, INF, INF) return Vector3(INF, INF, INF)
var attempts: int = 48 if is_finite(maximum_distance) else 1 var validates_diggable_surface := not entry.diggable_area_id.is_empty()
var attempts: int = (
48
if validates_diggable_surface or is_finite(maximum_distance)
else 1
)
var fallback: Vector3 = Vector3(INF, INF, INF) var fallback: Vector3 = Vector3(INF, INF, INF)
for _attempt: int in attempts: for _attempt: int in attempts:
var roll: float = _rng.randf() * total_area var roll: float = _rng.randf() * total_area
@ -534,12 +539,22 @@ func _sample_surface_position(
+ triangle[2] * barycentric_c + triangle[2] * barycentric_c
) )
fallback = point fallback = point
if ( var within_requested_distance := (
not is_finite(maximum_distance) not is_finite(maximum_distance)
or Vector2(point.x - origin.x, point.z - origin.z).length() or Vector2(point.x - origin.x, point.z - origin.z).length()
<= maximum_distance <= maximum_distance
): )
var valid_diggable_surface := (
not validates_diggable_surface
or _world.is_diggable_surface_point_valid(
entry.diggable_area_id,
point,
)
)
if within_requested_distance and valid_diggable_surface:
return point return point
if validates_diggable_surface:
return Vector3(INF, INF, INF)
if origin.is_finite() and is_finite(maximum_distance): if origin.is_finite() and is_finite(maximum_distance):
return origin return origin
return fallback return fallback

View file

@ -9,12 +9,42 @@ const BOOTSTRAP_TEMP_PATH := "user://data_root_bootstrap.json.tmp"
const BOOTSTRAP_VERSION := 1 const BOOTSTRAP_VERSION := 1
const MANIFEST_VERSION := 1 const MANIFEST_VERSION := 1
const LAYOUT_VERSION := 1 const LAYOUT_VERSION := 1
const MANIFEST_FILENAME := "netfishing_data.json" const MANIFEST_FILENAME := "straywild_data.json"
const LEGACY_MANIFEST_FILENAME := "netfishing_data.json"
const README_FILENAME := "README.txt" const README_FILENAME := "README.txt"
const ENVIRONMENT_VARIABLE := "NETFISHING_DATA_DIR" const ENVIRONMENT_VARIABLE := "straywild_DATA_DIR"
const CREATE_ENVIRONMENT_VARIABLE := "NETFISHING_CREATE_DATA_DIR" const CREATE_ENVIRONMENT_VARIABLE := "straywild_CREATE_DATA_DIR"
const APPLICATION_ID := "netfishing" const LEGACY_ENVIRONMENT_VARIABLE := "NETFISHING_DATA_DIR"
const LEGACY_CREATE_ENVIRONMENT_VARIABLE := "NETFISHING_CREATE_DATA_DIR"
const APPLICATION_ID := "straywild"
const LEGACY_APPLICATION_ID := "netfishing"
const APP_DATA_PORTABLE_PATH := "user://portable-data" const APP_DATA_PORTABLE_PATH := "user://portable-data"
const LEGACY_USER_DIRECTORY_NAME := "NETFISHING"
const LEGACY_USER_FILES := [
"data_root_bootstrap.json",
"data_root_bootstrap.json.backup",
"network_discovery.cfg",
"player_settings.json",
"player_settings.json.backup",
"controller_mappings.json",
"controller_mappings.json.backup",
"keyboard_mouse_bindings.json",
"keyboard_mouse_bindings.json.backup",
"player_save.json",
"network_profile.json",
"player_appearance.json",
"saved_servers.json",
"known_players.json",
"player_relationships.json",
"server_trust.json",
"host_bans.json",
"player_identity.key",
"player_identity.pub",
"player_identity.json",
"host_identity.key",
"host_identity.pub",
"host_identity.json",
]
const PATH_ALLOWED: StringName = &"allowed" const PATH_ALLOWED: StringName = &"allowed"
const PATH_SOURCE_PROJECT: StringName = &"source_project" const PATH_SOURCE_PROJECT: StringName = &"source_project"
const PATH_INSTALLATION: StringName = &"installation" const PATH_INSTALLATION: StringName = &"installation"
@ -37,23 +67,29 @@ var override_active := false
func resolve() -> bool: func resolve() -> bool:
prepare_legacy_user_data()
_load_bootstrap_identity() _load_bootstrap_identity()
var command_line: String = _command_line_override() var command_line: String = _command_line_override()
if not command_line.is_empty(): if not command_line.is_empty():
override_active = true override_active = true
mode = Mode.COMMAND_LINE_OVERRIDE mode = Mode.COMMAND_LINE_OVERRIDE
return _activate_existing(command_line, "", false) return _activate_existing(command_line, "", false)
if OS.has_environment(ENVIRONMENT_VARIABLE): var environment_variable: String = _available_environment_variable(
ENVIRONMENT_VARIABLE,
LEGACY_ENVIRONMENT_VARIABLE,
)
if not environment_variable.is_empty():
override_active = true override_active = true
mode = Mode.ENVIRONMENT_OVERRIDE mode = Mode.ENVIRONMENT_OVERRIDE
var environment_path: String = OS.get_environment(ENVIRONMENT_VARIABLE) var environment_path: String = OS.get_environment(environment_variable)
if not environment_path.is_absolute_path(): if not environment_path.is_absolute_path():
return _fail("NETFISHING_DATA_DIR must be an absolute path.") return _fail("straywild_DATA_DIR must be an absolute path.")
if ( if (
OS.get_environment(CREATE_ENVIRONMENT_VARIABLE) == "1" _environment_flag(
and not FileAccess.file_exists( CREATE_ENVIRONMENT_VARIABLE,
environment_path.path_join(MANIFEST_FILENAME) LEGACY_CREATE_ENVIRONMENT_VARIABLE,
) )
and not has_data_manifest(environment_path)
): ):
var created: Dictionary = create_unbound_root(environment_path) var created: Dictionary = create_unbound_root(environment_path)
if not bool(created.get("ok", false)): if not bool(created.get("ok", false)):
@ -80,11 +116,35 @@ func resolve() -> bool:
return false return false
func prepare_legacy_user_data() -> void:
var current_root: String = _normalize(
ProjectSettings.globalize_path("user://")
)
if current_root.is_empty():
return
var legacy_root: String = _normalize(
current_root.get_base_dir().path_join(LEGACY_USER_DIRECTORY_NAME)
)
if legacy_root == current_root or not DirAccess.dir_exists_absolute(legacy_root):
return
for filename: String in LEGACY_USER_FILES:
var destination: String = current_root.path_join(filename)
if FileAccess.file_exists(destination):
continue
var source: String = legacy_root.path_join(filename)
if FileAccess.file_exists(source):
_copy_legacy_user_file(source, destination)
func has_data_manifest(path: String) -> bool:
return not _existing_manifest_path(_normalize(path)).is_empty()
func default_visible_path() -> String: func default_visible_path() -> String:
var documents: String = OS.get_system_dir(OS.SYSTEM_DIR_DOCUMENTS) var documents: String = OS.get_system_dir(OS.SYSTEM_DIR_DOCUMENTS)
if documents.is_empty(): if documents.is_empty():
return "" return ""
var candidate: String = _normalize(documents.path_join("NETFISHING")) var candidate: String = _normalize(documents.path_join("straywild"))
if not candidate.is_absolute_path(): if not candidate.is_absolute_path():
return "" return ""
var classification: StringName = classify_candidate_path( var classification: StringName = classify_candidate_path(
@ -105,20 +165,22 @@ func select_new_root(path: String, app_data: bool = false) -> bool:
normalized = ProjectSettings.globalize_path(APP_DATA_PORTABLE_PATH) normalized = ProjectSettings.globalize_path(APP_DATA_PORTABLE_PATH)
if not _validate_candidate(normalized, true): if not _validate_candidate(normalized, true):
return false return false
var manifest_path: String = normalized.path_join(MANIFEST_FILENAME) var manifest_path: String = _existing_manifest_path(normalized)
if FileAccess.file_exists(manifest_path): if not manifest_path.is_empty():
var manifest: Dictionary = _read_json(manifest_path) var manifest: Dictionary = _read_json(manifest_path)
if not _valid_manifest(manifest): if not _valid_manifest(manifest):
return _fail("The selected folder has a malformed NETfishing manifest.") return _fail("The selected folder has a malformed straywild manifest.")
root_id = str(manifest["root_id"]) root_id = str(manifest["root_id"])
if not _upgrade_legacy_manifest(normalized, manifest, manifest_path):
return _fail("The legacy data-folder manifest could not be upgraded.")
else: else:
if not _directory_is_empty(normalized) and not app_data: if not _directory_is_empty(normalized) and not app_data:
return _fail( return _fail(
"Choose an empty folder or an existing NETfishing data folder." "Choose an empty folder or an existing straywild data folder."
) )
root_id = Crypto.new().generate_random_bytes(16).hex_encode() root_id = Crypto.new().generate_random_bytes(16).hex_encode()
if not _create_layout(normalized, root_id): if not _create_layout(normalized, root_id):
return _fail("The NETfishing data folder could not be created.") return _fail("The straywild data folder could not be created.")
if not _write_bootstrap(normalized, root_id): if not _write_bootstrap(normalized, root_id):
return _fail("The data-folder pointer could not be saved.") return _fail("The data-folder pointer could not be saved.")
root_path = normalized root_path = normalized
@ -145,14 +207,14 @@ func create_app_data_layout_for_migration(path: String) -> Dictionary:
var normalized: String = _normalize(path) var normalized: String = _normalize(path)
if not _validate_candidate(normalized, false): if not _validate_candidate(normalized, false):
return {"ok": false, "message": error_message} return {"ok": false, "message": error_message}
var manifest_path: String = normalized.path_join(MANIFEST_FILENAME) var manifest_path: String = _existing_manifest_path(normalized)
if FileAccess.file_exists(manifest_path): if not manifest_path.is_empty():
var manifest: Dictionary = _read_json(manifest_path) var manifest: Dictionary = _read_json(manifest_path)
return ( if not _valid_manifest(manifest):
{"ok": true, "root_id": str(manifest.get("root_id", ""))} return {"ok": false, "message": "The app-data manifest is malformed."}
if _valid_manifest(manifest) if not _upgrade_legacy_manifest(normalized, manifest, manifest_path):
else {"ok": false, "message": "The app-data manifest is malformed."} return {"ok": false, "message": "The app-data manifest could not be upgraded."}
) return {"ok": true, "root_id": str(manifest.get("root_id", ""))}
var id: String = Crypto.new().generate_random_bytes(16).hex_encode() var id: String = Crypto.new().generate_random_bytes(16).hex_encode()
if not _create_layout(normalized, id): if not _create_layout(normalized, id):
return {"ok": false, "message": "The app-data layout could not be created."} return {"ok": false, "message": "The app-data layout could not be created."}
@ -236,17 +298,19 @@ func _activate_existing(path: String, expected_id: String, permit_creation: bool
var normalized: String = _normalize(path) var normalized: String = _normalize(path)
if not _validate_candidate(normalized, permit_creation): if not _validate_candidate(normalized, permit_creation):
return false return false
var manifest_path: String = normalized.path_join(MANIFEST_FILENAME) var manifest_path: String = _existing_manifest_path(normalized)
if not FileAccess.file_exists(manifest_path): if manifest_path.is_empty():
return _fail("The selected folder is not a NETfishing data folder.") return _fail("The selected folder is not a straywild data folder.")
var manifest: Dictionary = _read_json(manifest_path) var manifest: Dictionary = _read_json(manifest_path)
if not _valid_manifest(manifest): if not _valid_manifest(manifest):
return _fail("The NETfishing data-folder manifest is malformed.") return _fail("The straywild data-folder manifest is malformed.")
if not _upgrade_legacy_manifest(normalized, manifest, manifest_path):
return _fail("The legacy data-folder manifest could not be upgraded.")
var found_id: String = str(manifest["root_id"]) var found_id: String = str(manifest["root_id"])
if not expected_id.is_empty() and expected_id != found_id: if not expected_id.is_empty() and expected_id != found_id:
return _fail("The selected data folder does not match this device pointer.") return _fail("The selected data folder does not match this device pointer.")
if not _test_writable(normalized): if not _test_writable(normalized):
return _fail("The NETfishing data folder is unavailable or unwritable.") return _fail("The straywild data folder is unavailable or unwritable.")
root_path = normalized root_path = normalized
root_id = found_id root_id = found_id
requires_selection = false requires_selection = false
@ -339,7 +403,7 @@ static func _is_same_or_child_path(candidate: String, reference: String) -> bool
func _test_writable(path: String) -> bool: func _test_writable(path: String) -> bool:
var probe: String = path.path_join( var probe: String = path.path_join(
".netfishing-write-%s.tmp" % device_id.left(12) ".straywild-write-%s.tmp" % device_id.left(12)
) )
var file: FileAccess = FileAccess.open(probe, FileAccess.WRITE) var file: FileAccess = FileAccess.open(probe, FileAccess.WRITE)
if file == null: if file == null:
@ -370,7 +434,7 @@ func _create_layout(path: String, id: String) -> bool:
): ):
return false return false
var readme: String = ( var readme: String = (
"NETfishing player data\n\n" "straywild player data\n\n"
+ "This folder is safe to synchronize with tools such as Syncthing.\n" + "This folder is safe to synchronize with tools such as Syncthing.\n"
+ "Active private identity keys remain device-local.\n" + "Active private identity keys remain device-local.\n"
+ "Encrypted identity backups require their passphrase.\n" + "Encrypted identity backups require their passphrase.\n"
@ -422,12 +486,71 @@ func _valid_manifest(data: Dictionary) -> bool:
return ( return (
data.get("format_version") == MANIFEST_VERSION data.get("format_version") == MANIFEST_VERSION
and data.get("layout_version") == LAYOUT_VERSION and data.get("layout_version") == LAYOUT_VERSION
and data.get("application") == APPLICATION_ID and data.get("application") in [APPLICATION_ID, LEGACY_APPLICATION_ID]
and typeof(data.get("root_id")) == TYPE_STRING and typeof(data.get("root_id")) == TYPE_STRING
and str(data["root_id"]).length() == 32 and str(data["root_id"]).length() == 32
) )
func _existing_manifest_path(path: String) -> String:
var current: String = path.path_join(MANIFEST_FILENAME)
if FileAccess.file_exists(current):
return current
var legacy: String = path.path_join(LEGACY_MANIFEST_FILENAME)
return legacy if FileAccess.file_exists(legacy) else ""
func _upgrade_legacy_manifest(
path: String,
manifest: Dictionary,
manifest_path: String,
) -> bool:
if (
manifest_path.get_file() == MANIFEST_FILENAME
and manifest.get("application") == APPLICATION_ID
):
return true
var upgraded: Dictionary = manifest.duplicate(true)
upgraded["application"] = APPLICATION_ID
upgraded["last_opened_at_unix"] = int(Time.get_unix_time_from_system())
return _write_text_atomic(
path.path_join(MANIFEST_FILENAME),
JSON.stringify(upgraded, "\t"),
)
func _available_environment_variable(primary: String, legacy: String) -> String:
if OS.has_environment(primary):
return primary
return legacy if OS.has_environment(legacy) else ""
func _environment_flag(primary: String, legacy: String) -> bool:
var variable: String = _available_environment_variable(primary, legacy)
return not variable.is_empty() and OS.get_environment(variable) == "1"
func _copy_legacy_user_file(source: String, destination: String) -> bool:
var bytes: PackedByteArray = PortableFileGuard.read_bytes(
source,
16 * 1024 * 1024,
)
if bytes.is_empty() and FileAccess.get_open_error() != OK:
return false
if DirAccess.make_dir_recursive_absolute(destination.get_base_dir()) != OK:
return false
var file: FileAccess = FileAccess.open(destination, FileAccess.WRITE)
if file == null:
return false
file.store_buffer(bytes)
file.flush()
var copied: bool = file.get_error() == OK
file.close()
if copied and source.ends_with(".key"):
FileAccess.set_unix_permissions(destination, 384)
return copied
func _directory_is_empty(path: String) -> bool: func _directory_is_empty(path: String) -> bool:
var access: DirAccess = DirAccess.open(path) var access: DirAccess = DirAccess.open(path)
if access == null: if access == null:

View file

@ -7,8 +7,8 @@ const FORMAT_VERSION := 2
const MAX_RECORDS := 500 const MAX_RECORDS := 500
const MAX_FRIENDS := 200 const MAX_FRIENDS := 200
const SOCIAL_TOKEN_LENGTH := 64 const SOCIAL_TOKEN_LENGTH := 64
const PRESENCE_CHANNEL_DOMAIN := "NETFISHING_PRESENCE_V1:" const PRESENCE_CHANNEL_DOMAIN := "straywild_PRESENCE_V1:"
const INVITE_INBOX_DOMAIN := "NETFISHING_INVITE_V1:" const INVITE_INBOX_DOMAIN := "straywild_INVITE_V1:"
var _records: Dictionary = {} var _records: Dictionary = {}
var _loaded := false var _loaded := false
@ -21,6 +21,10 @@ var _data_root: PlayerDataRoot
func configure_storage(path: String, data_root: PlayerDataRoot) -> void: func configure_storage(path: String, data_root: PlayerDataRoot) -> void:
_store_path = path _store_path = path
_data_root = data_root _data_root = data_root
_records.clear()
_loaded = false
_write_blocked = false
_expected_hash = ""
func is_muted(fingerprint: String) -> bool: func is_muted(fingerprint: String) -> bool:

View file

@ -26,19 +26,16 @@ static func migrate_legacy_to(
) -> Dictionary: ) -> Dictionary:
var normalized: String = destination.simplify_path().trim_suffix("/") var normalized: String = destination.simplify_path().trim_suffix("/")
if DirAccess.dir_exists_absolute(normalized): if DirAccess.dir_exists_absolute(normalized):
var existing_manifest: String = normalized.path_join( if data_root.has_data_manifest(normalized):
PlayerDataRoot.MANIFEST_FILENAME
)
if FileAccess.file_exists(existing_manifest):
return { return {
"ok": false, "ok": false,
"requires_existing_root_decision": true, "requires_existing_root_decision": true,
"message": "The selected folder already contains NETfishing data.", "message": "The selected folder already contains straywild data.",
} }
if not _directory_empty(normalized): if not _directory_empty(normalized):
return { return {
"ok": false, "ok": false,
"message": "Choose an empty folder or create a NETfishing subfolder.", "message": "Choose an empty folder or create a straywild subfolder.",
} }
var staging: String = "%s.migration-%s" % [ var staging: String = "%s.migration-%s" % [
normalized, Crypto.new().generate_random_bytes(8).hex_encode() normalized, Crypto.new().generate_random_bytes(8).hex_encode()
@ -106,16 +103,16 @@ static func migrate_active_to(
"message": "This data folder is already active.", "message": "This data folder is already active.",
} }
if DirAccess.dir_exists_absolute(normalized) and not _directory_empty(normalized): if DirAccess.dir_exists_absolute(normalized) and not _directory_empty(normalized):
if FileAccess.file_exists(normalized.path_join(PlayerDataRoot.MANIFEST_FILENAME)): if data_root.has_data_manifest(normalized):
return { return {
"ok": false, "ok": false,
"requires_existing_root_decision": true, "requires_existing_root_decision": true,
"message": ( "message": (
"The selected folder already contains NETfishing data. " "The selected folder already contains straywild data. "
+ "Choose it through the existing-data recovery flow." + "Choose it through the existing-data recovery flow."
), ),
} }
return {"ok": false, "message": "Choose an empty folder or a NETfishing subfolder."} return {"ok": false, "message": "Choose an empty folder or a straywild subfolder."}
var staging: String = "%s.migration-%s" % [ var staging: String = "%s.migration-%s" % [
normalized, Crypto.new().generate_random_bytes(8).hex_encode() normalized, Crypto.new().generate_random_bytes(8).hex_encode()
] ]
@ -207,9 +204,8 @@ static func replace_existing_with_legacy(
destination: String, destination: String,
) -> Dictionary: ) -> Dictionary:
var normalized: String = destination.simplify_path().trim_suffix("/") var normalized: String = destination.simplify_path().trim_suffix("/")
var manifest: String = normalized.path_join(PlayerDataRoot.MANIFEST_FILENAME) if not data_root.has_data_manifest(normalized):
if not FileAccess.file_exists(manifest): return {"ok": false, "message": "The selected folder is not a straywild data root."}
return {"ok": false, "message": "The selected folder is not a NETfishing data root."}
var recovery: String = ProjectSettings.globalize_path( var recovery: String = ProjectSettings.globalize_path(
"user://migration-recovery" "user://migration-recovery"
).path_join( ).path_join(
@ -229,8 +225,8 @@ static func replace_existing_with_active(
destination: String, destination: String,
) -> Dictionary: ) -> Dictionary:
var normalized: String = destination.simplify_path().trim_suffix("/") var normalized: String = destination.simplify_path().trim_suffix("/")
if not FileAccess.file_exists(normalized.path_join(PlayerDataRoot.MANIFEST_FILENAME)): if not data_root.has_data_manifest(normalized):
return {"ok": false, "message": "The selected folder is not a NETfishing data root."} return {"ok": false, "message": "The selected folder is not a straywild data root."}
var recovery: String = ProjectSettings.globalize_path( var recovery: String = ProjectSettings.globalize_path(
"user://migration-recovery" "user://migration-recovery"
).path_join( ).path_join(

View file

@ -22,6 +22,12 @@ var _data_root: PlayerDataRoot
func configure_storage(path: String, data_root: PlayerDataRoot) -> void: func configure_storage(path: String, data_root: PlayerDataRoot) -> void:
_store_path = path _store_path = path
_data_root = data_root _data_root = data_root
_saved_entries.clear()
_recent_entries.clear()
_loaded = false
_recovery_warning = ""
_write_blocked = false
_expected_hash = ""
func _temp_path() -> String: func _temp_path() -> String:

View file

@ -19,6 +19,10 @@ var _data_root: PlayerDataRoot
func configure_storage(path: String, data_root: PlayerDataRoot) -> void: func configure_storage(path: String, data_root: PlayerDataRoot) -> void:
_store_path = path _store_path = path
_data_root = data_root _data_root = data_root
_records.clear()
_loaded = false
_write_blocked = false
_expected_hash = ""
func verify(endpoint: ConnectionEndpoint, fingerprint: String) -> Verification: func verify(endpoint: ConnectionEndpoint, fingerprint: String) -> Verification:

View file

@ -1,7 +1,88 @@
extends "res://player/tool_socket_attachment.gd" extends "res://player/tool_socket_attachment.gd"
@onready var _impact_marker: Marker3D = %ImpactMarker const TextureSamplingPolicyScript := preload(
"res://main/texture_sampling_policy.gd"
)
const SPRING_ROOT_BONE: StringName = &"net_mid"
const SPRING_END_BONE: StringName = &"net_tip"
const IMPACT_BONE: StringName = &"net_rim"
@export_range(0.0, 10.0, 0.05) var spring_stiffness: float = 1.8
@export_range(0.0, 1.0, 0.01) var spring_drag: float = 0.68
@export_range(0.0, 10.0, 0.05) var spring_gravity: float = 0.3
@export_range(0.01, 0.5, 0.01) var spring_end_length: float = 0.1
@onready var _catching_net: Node3D = %CatchingNet
@onready var _net_model: Node3D = %NetModel
var _spring: SpringBoneSimulator3D
var _spring_was_visible: bool = false
var _net_skeleton: Skeleton3D
func _ready() -> void:
super._ready()
TextureSamplingPolicyScript.enforce_subtree(_net_model)
_configure_spring()
_sync_spring_activity()
func _process(_delta: float) -> void:
_sync_spring_activity()
func _configure_spring() -> void:
_net_skeleton = _net_model.find_child(
"Skeleton3D", true, false
) as Skeleton3D
if _net_skeleton == null:
push_error("Imported net model is missing its Skeleton3D.")
return
if _net_skeleton.find_bone(SPRING_ROOT_BONE) < 0:
push_error("Imported net model is missing bone %s." % SPRING_ROOT_BONE)
return
if _net_skeleton.find_bone(SPRING_END_BONE) < 0:
push_error("Imported net model is missing bone %s." % SPRING_END_BONE)
return
if _net_skeleton.find_bone(IMPACT_BONE) < 0:
push_error("Imported net model is missing bone %s." % IMPACT_BONE)
return
_spring = SpringBoneSimulator3D.new()
_spring.name = "NetSpring"
_spring.set_setting_count(1)
_spring.set_root_bone_name(0, SPRING_ROOT_BONE)
_spring.set_end_bone_name(0, SPRING_END_BONE)
_spring.set_extend_end_bone(0, true)
_spring.set_end_bone_direction(
0, SkeletonModifier3D.BONE_DIRECTION_FROM_PARENT
)
_spring.set_end_bone_length(0, spring_end_length)
_spring.set_stiffness(0, spring_stiffness)
_spring.set_drag(0, spring_drag)
_spring.set_gravity(0, spring_gravity)
_spring.set_gravity_direction(0, Vector3.DOWN)
_net_skeleton.add_child(_spring)
func get_impact_world_position() -> Vector3: func get_impact_world_position() -> Vector3:
return _impact_marker.global_position if _net_skeleton == null:
return _catching_net.global_position
var impact_bone_index := _net_skeleton.find_bone(IMPACT_BONE)
if impact_bone_index < 0:
return _catching_net.global_position
return _net_skeleton.global_transform * (
_net_skeleton.get_bone_global_pose(impact_bone_index).origin
)
func _sync_spring_activity() -> void:
if _spring == null:
return
var is_visible := _catching_net.is_visible_in_tree()
if is_visible and not _spring_was_visible:
_spring.active = true
_spring.reset()
elif not is_visible:
_spring.active = false
_spring_was_visible = is_visible

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