Compare commits
26 commits
v0.18.0-al
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 014c064fae | |||
| 3be79fa3fc | |||
| 6578d3e870 | |||
| ee992d9f0c | |||
| 9b96a3bf53 | |||
| 826f62c352 | |||
| b48cb73f3a | |||
| d2dce7f900 | |||
| 983e4d0ec3 | |||
| fe099f47dd | |||
| 81decf2b71 | |||
| 4575830857 | |||
| ae802493a0 | |||
| f910e6bdbb | |||
| ed2f3f7abc | |||
| 2b06b79bbf | |||
| 98a27e2f53 | |||
| 737d1cf2e0 | |||
| 7ae20789a9 | |||
| 17fef2c5d4 | |||
| aaa5803e6b | |||
| 0cc3ae9709 | |||
| 046f5e0fdb | |||
| 135fd9110f | |||
| 1ab2a291d2 | |||
| da7f072365 |
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
28
README.md
|
|
@ -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).
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
@ -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]
|
||||||
|
|
||||||
|
Before Width: | Height: | Size: 7 KiB After Width: | Height: | Size: 7 KiB |
|
|
@ -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]
|
||||||
|
|
||||||
|
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 6.1 KiB |
|
|
@ -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]
|
||||||
|
|
||||||
|
Before Width: | Height: | Size: 7 KiB After Width: | Height: | Size: 7 KiB |
|
|
@ -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]
|
||||||
|
|
||||||
BIN
art/exported/items/equipment/net.glb
Normal file
45
art/exported/items/equipment/net.glb.import
Normal 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
|
||||||
BIN
art/exported/items/equipment/net_Material.png
Normal file
|
After Width: | Height: | Size: 276 KiB |
45
art/exported/items/equipment/net_Material.png.import
Normal 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
|
||||||
BIN
art/exported/items/equipment/net_Plane Base Color.png
Normal file
|
After Width: | Height: | Size: 42 KiB |
45
art/exported/items/equipment/net_Plane Base Color.png.import
Normal 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
|
||||||
|
Before Width: | Height: | Size: 362 KiB |
|
Before Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 47 KiB |
BIN
art/exported/system_icons/straywild.ico
Normal file
|
After Width: | Height: | Size: 362 KiB |
BIN
art/exported/system_icons/straywild_1024.png
Normal file
|
After Width: | Height: | Size: 78 KiB |
|
|
@ -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]
|
||||||
|
|
||||||
BIN
art/exported/system_icons/straywild_256.png
Normal file
|
After Width: | Height: | Size: 51 KiB |
|
|
@ -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]
|
||||||
|
|
||||||
|
|
@ -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]
|
||||||
|
|
||||||
|
|
@ -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]
|
||||||
|
|
||||||
|
|
@ -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]
|
||||||
|
|
||||||
|
|
@ -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]
|
||||||
|
|
||||||
|
Before Width: | Height: | Size: 8.2 KiB After Width: | Height: | Size: 8.2 KiB |
|
|
@ -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]
|
||||||
|
|
||||||
|
Before Width: | Height: | Size: 7 KiB After Width: | Height: | Size: 7 KiB |
|
|
@ -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]
|
||||||
|
|
||||||
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
|
@ -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]
|
||||||
|
|
||||||
|
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 6.1 KiB |
|
|
@ -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]
|
||||||
|
|
||||||
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
|
@ -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]
|
||||||
|
|
||||||
|
Before Width: | Height: | Size: 7 KiB After Width: | Height: | Size: 7 KiB |
|
|
@ -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]
|
||||||
|
|
||||||
BIN
art/source/items/equipment/net.blend
Normal 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. |
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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=""
|
||||||
|
|
|
||||||
|
|
@ -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 []
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
BIN
homes/assets/starter_motorcoach.glb
Normal file
45
homes/assets/starter_motorcoach.glb.import
Normal 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
|
|
@ -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))
|
||||||
1
homes/decor_catalog.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://bvgwmexas2gps
|
||||||
499
homes/home_decor_controller.gd
Normal 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()
|
||||||
1
homes/home_decor_controller.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://b5j0mqcvhmi54
|
||||||
1209
homes/home_world_service.gd
Normal file
1
homes/home_world_service.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://cy2kjoxhwxujd
|
||||||
786
homes/player_home_state.gd
Normal 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)]
|
||||||
1
homes/player_home_state.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://ggfdoak2ixxx
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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]
|
||||||
|
|
|
||||||
17
items/catalog/basic_cube.tres
Normal 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
|
||||||
|
|
@ -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")]
|
||||||
|
|
|
||||||
365
main/main.gd
|
|
@ -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),
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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():
|
||||||
|
|
|
||||||
|
|
@ -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.")
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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.",
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
188
network/network_home_protocol.gd
Normal 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
|
||||||
|
)
|
||||||
1
network/network_home_protocol.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://fl3qwiluejhm
|
||||||
1390
network/network_home_service.gd
Normal file
1
network/network_home_service.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://ong8kug36ku7
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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"])
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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."
|
||||||
)
|
)
|
||||||
_:
|
_:
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)),
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||