Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da29308522 | ||
|
|
79061d8572 | ||
|
|
1f67d58b92 | ||
|
|
c2c86b5f78 | ||
|
|
04b9d17826 |
19
README.md
19
README.md
@@ -16,8 +16,10 @@ addon SDK with guard rails.
|
|||||||
## Status
|
## Status
|
||||||
See `docs/ROADMAP.md` for milestones and `docs/ARCHITECTURE.md` for the design.
|
See `docs/ROADMAP.md` for milestones and `docs/ARCHITECTURE.md` for the design.
|
||||||
|
|
||||||
**Emberwild 0.6.0** is released (download page and player guide:
|
**Cinderwild 0.6.1** is released (download page and player guide:
|
||||||
https://emberwild-95k.pages.dev, builds on the Gitea releases). Built so far:
|
https://cinderwild.psbailey.uk, builds on the Gitea releases). Up to 0.6.0 it
|
||||||
|
was called Emberwild (a name already taken on Steam); before that, Valheimlike.
|
||||||
|
Built so far:
|
||||||
- M0-M1: the island, survival, crafting, building, weather, wildlife, hunting, fishing.
|
- M0-M1: the island, survival, crafting, building, weather, wildlife, hunting, fishing.
|
||||||
- M2: self-hosted multiplayer (Host Game, dedicated servers, rules, chat, admin).
|
- M2: self-hosted multiplayer (Host Game, dedicated servers, rules, chat, admin).
|
||||||
- P1-P5 (`docs/PROGRESSION_DESIGN.md`): iron, stone building, the wolf moon and
|
- P1-P5 (`docs/PROGRESSION_DESIGN.md`): iron, stone building, the wolf moon and
|
||||||
@@ -60,7 +62,7 @@ godot --headless --path . -- --server
|
|||||||
|
|
||||||
## Packaged builds
|
## Packaged builds
|
||||||
```bash
|
```bash
|
||||||
tools/export.sh # build/Emberwild-<version>-macos.zip and -windows.zip
|
tools/export.sh # build/Cinderwild-<version>-macos.zip and -windows.zip
|
||||||
tools/export.sh macOS # or just one ("macOS" / "Windows")
|
tools/export.sh macOS # or just one ("macOS" / "Windows")
|
||||||
```
|
```
|
||||||
Needs the Godot export templates for your Godot version (Editor > Manage
|
Needs the Godot export templates for your Godot version (Editor > Manage
|
||||||
@@ -69,18 +71,19 @@ Export Templates). The version number is `application/short_version` in
|
|||||||
- **Mac:** a universal .app, ad-hoc signed but not notarised. On another Mac,
|
- **Mac:** a universal .app, ad-hoc signed but not notarised. On another Mac,
|
||||||
the first launch needs right-click > Open (or System Settings > Privacy &
|
the first launch needs right-click > Open (or System Settings > Privacy &
|
||||||
Security > Open Anyway).
|
Security > Open Anyway).
|
||||||
- **Windows:** unzip and run `Emberwild.exe` (keep `Emberwild.pck` beside
|
- **Windows:** unzip and run `Cinderwild.exe` (keep `Cinderwild.pck` beside
|
||||||
it). It's unsigned, so SmartScreen says "Windows protected your PC":
|
it). It's unsigned, so SmartScreen says "Windows protected your PC":
|
||||||
More info > Run anyway.
|
More info > Run anyway.
|
||||||
- A packaged copy is also a dedicated server:
|
- A packaged copy is also a dedicated server:
|
||||||
`Emberwild.app/Contents/MacOS/Emberwild --headless -- --server` (Mac) or
|
`Cinderwild.app/Contents/MacOS/Cinderwild --headless -- --server` (Mac) or
|
||||||
`Emberwild.exe --headless -- --server` (Windows). Saves and settings live in
|
`Cinderwild.exe --headless -- --server` (Windows). Saves and settings live in
|
||||||
the same user folder as a copy run from source.
|
the same user folder as a copy run from source.
|
||||||
|
|
||||||
## Trailer and download page
|
## Trailer and download page
|
||||||
```bash
|
```bash
|
||||||
tools/trailer/make_trailer.sh # build/Emberwild-trailer.mp4: recorded in-game (Movie Maker), needs ffmpeg
|
tools/trailer/make_trailer.sh # build/Cinderwild-trailer.mp4: recorded in-game (Movie Maker), needs ffmpeg
|
||||||
tools/site/make_site.sh # build/site/: index.html + poster.jpg + trailer.mp4 for the current release
|
tools/site/make_shots.sh # build/shots/*.jpg: the page's screenshots, rendered in a window
|
||||||
|
tools/site/make_site.sh # build/site/: index.html, guide.html, the shots, poster and trailer for the current release
|
||||||
```
|
```
|
||||||
The page's download links point at the Gitea release named in
|
The page's download links point at the Gitea release named in
|
||||||
`build/version.json`, so rebuild it after each `tools/export.sh` release.
|
`build/version.json`, so rebuild it after each `tools/export.sh` release.
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ made a little older (deeper, slower, warmer) by `tools/voice/make_voice.py`.
|
|||||||
The recordings are generated from our own text; rebuild them with
|
The recordings are generated from our own text; rebuild them with
|
||||||
`tools/voice/make_voice.sh` after changing any line.
|
`tools/voice/make_voice.sh` after changing any line.
|
||||||
|
|
||||||
## Emberwild's own (CC0)
|
## Cinderwild's own (CC0)
|
||||||
`music/`, `ambience/` and `sfx/synth/` are synthesised from scratch by
|
`music/`, `ambience/` and `sfx/synth/` are synthesised from scratch by
|
||||||
`tools/audio/make_audio.py` (no samples): the five music tracks (calm,
|
`tools/audio/make_audio.py` (no samples): the five music tracks (calm,
|
||||||
night, storm, danger, boss), the fire crackle, the creature voices, and the
|
night, storm, danger, boss), the fire crackle, the creature voices, and the
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ elsewhere, use **Tailscale** (recommended) or port forwarding (below).
|
|||||||
- **Leaving** (Disconnect, or quitting the game) saves the island and
|
- **Leaving** (Disconnect, or quitting the game) saves the island and
|
||||||
stops the server. Next time, Host Game carries on with the same island
|
stops the server. Next time, Host Game carries on with the same island
|
||||||
(the seed field is locked once it exists).
|
(the seed field is locked once it exists).
|
||||||
- Its files: `~/Library/Application Support/Godot/app_userdata/Emberwild/host/`
|
- Its files: `~/Library/Application Support/Godot/app_userdata/Cinderwild/host/`
|
||||||
(macOS; `%APPDATA%\Godot\app_userdata\Emberwild\host\` on Windows):
|
(macOS; `%APPDATA%\Godot\app_userdata\Cinderwild\host\` on Windows):
|
||||||
`server.json`, `server.log`, and the world in `worlds/server-world/`.
|
`server.json`, `server.log`, and the world in `worlds/server-world/`.
|
||||||
- macOS may ask once whether Godot may accept incoming network
|
- macOS may ask once whether Godot may accept incoming network
|
||||||
connections: say **Allow**, or friends can't connect.
|
connections: say **Allow**, or friends can't connect.
|
||||||
@@ -47,14 +47,14 @@ The first player to join a new server becomes its admin (so join it
|
|||||||
yourself before telling friends the address). More admins: `/admin <name>`.
|
yourself before telling friends the address). More admins: `/admin <name>`.
|
||||||
|
|
||||||
**As a service** (starts on boot, restarts if it stops):
|
**As a service** (starts on boot, restarts if it stops):
|
||||||
- **macOS:** `server/com.emberwild.server.plist`: edit the paths in it,
|
- **macOS:** `server/com.cinderwild.server.plist`: edit the paths in it,
|
||||||
then `cp server/com.emberwild.server.plist ~/Library/LaunchAgents/` and
|
then `cp server/com.cinderwild.server.plist ~/Library/LaunchAgents/` and
|
||||||
`launchctl load -w ~/Library/LaunchAgents/com.emberwild.server.plist`.
|
`launchctl load -w ~/Library/LaunchAgents/com.cinderwild.server.plist`.
|
||||||
It runs while you're logged in (set the Mac to log in automatically for an
|
It runs while you're logged in (set the Mac to log in automatically for an
|
||||||
always-on server). Log: `~/Library/Logs/emberwild-server.log`.
|
always-on server). Log: `~/Library/Logs/cinderwild-server.log`.
|
||||||
- **Linux:** `server/emberwild-server.service`: the steps are in the file
|
- **Linux:** `server/cinderwild-server.service`: the steps are in the file
|
||||||
(`sudo systemctl enable --now emberwild-server`; logs with
|
(`sudo systemctl enable --now cinderwild-server`; logs with
|
||||||
`journalctl -u emberwild-server -f`).
|
`journalctl -u cinderwild-server -f`).
|
||||||
|
|
||||||
As a service there's no terminal to type commands into: use chat in game,
|
As a service there's no terminal to type commands into: use chat in game,
|
||||||
or edit `server.json` and restart.
|
or edit `server.json` and restart.
|
||||||
@@ -76,7 +76,7 @@ the ones you'll want first:
|
|||||||
| `difficulty` | normal | easy / normal / hard |
|
| `difficulty` | normal | easy / normal / hard |
|
||||||
| `sleep` | all_online | or `half`, `anyone`, `off` |
|
| `sleep` | all_online | or `half`, `anyone`, `off` |
|
||||||
| `backups_kept`, `backup_hours` | 10, 6 | see below |
|
| `backups_kept`, `backup_hours` | 10, 6 | see below |
|
||||||
| `check_updates` | true | at start-up, say in the log if a newer Emberwild is out (and whether its protocol differs, so new players couldn't join) |
|
| `check_updates` | true | at start-up, say in the log if a newer Cinderwild is out (and whether its protocol differs, so new players couldn't join) |
|
||||||
| `allowlist`, `banned`, `admins` | | player ids (use `/ban`, `/admin` rather than editing) |
|
| `allowlist`, `banned`, `admins` | | player ids (use `/ban`, `/admin` rather than editing) |
|
||||||
|
|
||||||
## Backups
|
## Backups
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
# Emberwild 0.6.0 playtest
|
# Cinderwild 0.6.0 playtest
|
||||||
|
|
||||||
Thanks for playing. This build is the **Valley** and the **Cove**, and
|
Thanks for playing. This build is the **Valley** and the **Cove**, and
|
||||||
we'd like to know what breaks, what's confusing, and what's no fun. The
|
we'd like to know what breaks, what's confusing, and what's no fun. The
|
||||||
game's player guide is on the download page: https://emberwild-95k.pages.dev
|
game's player guide is on the download page: https://cinderwild.psbailey.uk
|
||||||
|
|
||||||
## Reporting a bug
|
## Reporting a bug
|
||||||
|
|
||||||
Press **Esc** in the game, then **Report a bug**:
|
Press **Esc** in the game, then **Report a bug**:
|
||||||
|
|
||||||
- **Open the bug report page** opens a new issue on the Emberwild Gitea page
|
- **Open the bug report page** opens a new issue on the Cinderwild Gitea page
|
||||||
in your browser, already filled in with your version, computer, mode,
|
in your browser, already filled in with your version, computer, mode,
|
||||||
land, the in-game time and where you are. Say what happened, what you
|
land, the in-game time and where you are. Say what happened, what you
|
||||||
expected, and how to make it happen again, then press **Create Issue**.
|
expected, and how to make it happen again, then press **Create Issue**.
|
||||||
@@ -25,9 +25,9 @@ Where the log is:
|
|||||||
|
|
||||||
| | |
|
| | |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Mac | `~/Library/Application Support/Godot/app_userdata/Emberwild/logs/godot.log` |
|
| Mac | `~/Library/Application Support/Godot/app_userdata/Cinderwild/logs/godot.log` |
|
||||||
| Windows | `%APPDATA%\Godot\app_userdata\Emberwild\logs\godot.log` |
|
| Windows | `%APPDATA%\Godot\app_userdata\Cinderwild\logs\godot.log` |
|
||||||
| Linux | `~/.local/share/godot/app_userdata/Emberwild/logs/godot.log` |
|
| Linux | `~/.local/share/godot/app_userdata/Cinderwild/logs/godot.log` |
|
||||||
|
|
||||||
A screenshot helps for anything that looks wrong (Cmd+Shift+4 on a Mac,
|
A screenshot helps for anything that looks wrong (Cmd+Shift+4 on a Mac,
|
||||||
Win+Shift+S on Windows).
|
Win+Shift+S on Windows).
|
||||||
|
|||||||
@@ -114,6 +114,66 @@ phases M2.0-M2.7).
|
|||||||
game icon on the .exe (rcedit), Linux, and the update check below.
|
game icon on the .exe (rcedit), Linux, and the update check below.
|
||||||
- **Updates**: the game (and a dedicated server) needs to notice when a new
|
- **Updates**: the game (and a dedicated server) needs to notice when a new
|
||||||
version is out, download what's needed and install it. See below.
|
version is out, download what's needed and install it. See below.
|
||||||
|
- **A single-player web version** (Paul, Nov 2026: after the playtest):
|
||||||
|
play straight from the download page, with "Download for the full game
|
||||||
|
and multiplayer" beside it. Godot's web export, single-threaded, on the
|
||||||
|
Compatibility renderer (WebGL 2). What we found (0.6.1):
|
||||||
|
- *The look:* rendered with that renderer (`build/web_look/`, from
|
||||||
|
`tools/site/shots.gd` run with `--rendering-method gl_compatibility`),
|
||||||
|
it's still clearly the game, but harsher: no volumetric fog or haze,
|
||||||
|
no SSAO/SSIL (darker, sharper shadows), deeper greens. Night and fire
|
||||||
|
light hold up. **The sea renders flat white**: `shaders/water.gdshader`
|
||||||
|
needs a Compatibility path. Ordinary distance fog would bring some
|
||||||
|
atmosphere back; grass density and tree counts likely need a lower
|
||||||
|
setting for browsers (speed not measured yet).
|
||||||
|
- *Single-player only:* browsers can't use ENet (UDP), and can't start
|
||||||
|
the Host Game or per-land server processes. Joining from a browser
|
||||||
|
would mean a WebSocket transport on the server, served over wss://
|
||||||
|
(a certificate per server): a separate, bigger job, if ever.
|
||||||
|
- *Saves* go in the browser's storage (IndexedDB): kept, but per browser
|
||||||
|
and lost if site data is cleared. Maybe an export/import button.
|
||||||
|
- *Input:* the pointer lock needs a click, and Esc releases it in the
|
||||||
|
browser, so the pause menu needs another key too (P).
|
||||||
|
- *Size and hosting:* the engine (godot.wasm, 39.5 MB) plus the game data
|
||||||
|
(about 22 MB) download first, then cache. Cloudflare Pages refuses files
|
||||||
|
over 25 MiB, so the .wasm has to live elsewhere (the Gitea release, or
|
||||||
|
another host) or be split.
|
||||||
|
- *Size of the job:* one step to build it and see how it runs, another to
|
||||||
|
fix the water, tune the graphics and controls, and put it on the page.
|
||||||
|
- **Cloud saves** (Paul, Nov 2026: with the web version, and useful before
|
||||||
|
it, desktop to desktop): carry a single-player world between the desktop
|
||||||
|
game and the browser, **both ways**.
|
||||||
|
- *In the game:* pause menu **Save to cloud** (a cloud name and a password
|
||||||
|
the first time; after that it can upload on each save: autosave, sleep,
|
||||||
|
quit); title screen **Load from cloud** (name and password, pick a
|
||||||
|
world, it downloads and you carry on). What travels: the world folder
|
||||||
|
(`meta.json`, `world.json`), its explored map (`maps/`), and which of
|
||||||
|
Old Tam's tips you've seen. Not online worlds (they live on their server).
|
||||||
|
- *Size:* a world is 8-32 KB on disk, a few KB compressed: uploads are
|
||||||
|
instant and storage is next to free.
|
||||||
|
- *Where:* a Cloudflare Worker beside the site (`/api/...`), saves in R2
|
||||||
|
(or D1). Free tier is plenty. (Paul's own server would do, but is one
|
||||||
|
more service to keep running.)
|
||||||
|
- *Safety:* passwords never stored, only a salted hash (PBKDF2, WebCrypto
|
||||||
|
in the Worker); HTTPS only; wrong guesses lock a name out for a while;
|
||||||
|
a size cap and a world count per name. Option: encrypt the save with
|
||||||
|
the password in the game before upload, so the server holds only
|
||||||
|
ciphertext (but a forgotten password loses the cloud copy; the local
|
||||||
|
copy is unaffected).
|
||||||
|
- *Playing in two places:* each upload carries a counter. If the cloud's
|
||||||
|
copy is newer than the one you last downloaded, the game asks ("The
|
||||||
|
cloud has a newer save (from the web, 20 minutes ago): use that, or
|
||||||
|
keep this one?") instead of overwriting it.
|
||||||
|
- *Versions:* the same save format on desktop and web; a save from a
|
||||||
|
newer version than the game says so rather than loading it badly.
|
||||||
|
- *Size of the job:* one step for the Worker and storage (with tests), one
|
||||||
|
for the game side (name and password, the buttons, the newer-copy
|
||||||
|
question). Paul creates the Worker and its storage in his Cloudflare
|
||||||
|
account; the code and setup steps come with it.
|
||||||
|
- **Later, maybe:** Android tablets (Nov 2026: left for now). Most input is
|
||||||
|
already named actions, so a touch layer (stick, look, contextual buttons)
|
||||||
|
is possible; the Mobile renderer has the same lost effects as the web, and
|
||||||
|
a tablet can join but not host (no server processes).
|
||||||
|
|
||||||
### Updates (to design with the packaged builds)
|
### Updates (to design with the packaged builds)
|
||||||
Needed because every network change bumps the protocol (12 as of P5), and
|
Needed because every network change bumps the protocol (12 as of P5), and
|
||||||
@@ -121,7 +181,7 @@ an out-of-date game can't join an up-to-date server: today that means
|
|||||||
`git pull` and a restart on every machine.
|
`git pull` and a restart on every machine.
|
||||||
|
|
||||||
*Built so far (the check):* `scripts/core/updates.gd` asks the feed in
|
*Built so far (the check):* `scripts/core/updates.gd` asks the feed in
|
||||||
Project Settings (`emberwild/updates/feed`, now the git server's
|
Project Settings (`cinderwild/updates/feed`, now the git server's
|
||||||
`releases/latest` API; Gitea, Forgejo and GitHub answer alike) once at
|
`releases/latest` API; Gitea, Forgejo and GitHub answer alike) once at
|
||||||
start-up. It reads the release's `version.json` asset (written by
|
start-up. It reads the release's `version.json` asset (written by
|
||||||
`tools/export.sh`: version, protocol, per-platform file, sha256 and size).
|
`tools/export.sh`: version, protocol, per-platform file, sha256 and size).
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ server/run_server.sh --server-name "Paul's island" --password secret --port 2424
|
|||||||
It checks the Godot version, imports a fresh copy, then starts the server on
|
It checks the Godot version, imports a fresh copy, then starts the server on
|
||||||
UDP 24242 with no window. Settings live in `server.json` (its path is printed
|
UDP 24242 with no window. Settings live in `server.json` (its path is printed
|
||||||
at start-up; on macOS it's under
|
at start-up; on macOS it's under
|
||||||
`~/Library/Application Support/Godot/app_userdata/Emberwild/server/`).
|
`~/Library/Application Support/Godot/app_userdata/Cinderwild/server/`).
|
||||||
The shared island is saved in `worlds/server-world/` in that same folder,
|
The shared island is saved in `worlds/server-world/` in that same folder,
|
||||||
within 10 seconds of any change (and when someone leaves). Stop it with
|
within 10 seconds of any change (and when someone leaves). Stop it with
|
||||||
Ctrl+C. Players join with **Join Game** on the title screen. Over
|
Ctrl+C. Players join with **Join Game** on the title screen. Over
|
||||||
@@ -82,13 +82,13 @@ godot --headless --path . --script res://tests/test_survival_stats.gd
|
|||||||
|
|
||||||
## Save files
|
## Save files
|
||||||
Worlds are per machine, under
|
Worlds are per machine, under
|
||||||
`~/Library/Application Support/Godot/app_userdata/Emberwild/worlds/` on
|
`~/Library/Application Support/Godot/app_userdata/Cinderwild/worlds/` on
|
||||||
macOS (`%APPDATA%\Godot\app_userdata\Emberwild\worlds\` on Windows). Copy a
|
macOS (`%APPDATA%\Godot\app_userdata\Cinderwild\worlds\` on Windows). Copy a
|
||||||
world's folder across to move it to another machine.
|
world's folder across to move it to another machine.
|
||||||
|
|
||||||
The game used to be called Valheimlike, and its data lived in
|
The game used to be called Valheimlike, and its data lived in
|
||||||
`app_userdata/Valheimlike/`. The first time Emberwild starts it copies worlds,
|
`app_userdata/Valheimlike/`. The first time Cinderwild starts it copies worlds,
|
||||||
your identity and server settings across to `app_userdata/Emberwild/`
|
your identity and server settings across to `app_userdata/Cinderwild/`
|
||||||
(`scripts/core/boot.gd`). The old folder is left alone as a backup, and you
|
(`scripts/core/boot.gd`). The old folder is left alone as a backup, and you
|
||||||
can delete it once you're happy.
|
can delete it once you're happy.
|
||||||
|
|
||||||
|
|||||||
@@ -275,7 +275,7 @@ small Diet bars, and a low group blinks.
|
|||||||
- **Autosave** every 2 minutes, on Save & Quit (to the title or desktop) and
|
- **Autosave** every 2 minutes, on Save & Quit (to the title or desktop) and
|
||||||
when the window is closed.
|
when the window is closed.
|
||||||
- Saves live in `user://worlds/<id>/` (on macOS:
|
- Saves live in `user://worlds/<id>/` (on macOS:
|
||||||
`~/Library/Application Support/Godot/app_userdata/Emberwild/worlds/`) as
|
`~/Library/Application Support/Godot/app_userdata/Cinderwild/worlds/`) as
|
||||||
`meta.json` + `world.json`. Writes are atomic, and the previous save is
|
`meta.json` + `world.json`. Writes are atomic, and the previous save is
|
||||||
kept as `world.json.bak`.
|
kept as `world.json.bak`.
|
||||||
- A save stores only what the player changed: player position and look,
|
- A save stores only what the player changed: player position and look,
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ custom_features=""
|
|||||||
export_filter="all_resources"
|
export_filter="all_resources"
|
||||||
include_filter="data/*.json, data/worldlets/*.json"
|
include_filter="data/*.json, data/worldlets/*.json"
|
||||||
exclude_filter="tests/*, docs/*, tools/*, server/*"
|
exclude_filter="tests/*, docs/*, tools/*, server/*"
|
||||||
export_path="build/macos/Emberwild.zip"
|
export_path="build/macos/Cinderwild.zip"
|
||||||
patches=PackedStringArray()
|
patches=PackedStringArray()
|
||||||
encryption_include_filters=""
|
encryption_include_filters=""
|
||||||
encryption_exclude_filters=""
|
encryption_exclude_filters=""
|
||||||
@@ -26,11 +26,11 @@ custom_template/release=""
|
|||||||
debug/export_console_wrapper=0
|
debug/export_console_wrapper=0
|
||||||
application/icon=""
|
application/icon=""
|
||||||
application/icon_interpolation=4
|
application/icon_interpolation=4
|
||||||
application/bundle_identifier="com.pbailey.emberwild"
|
application/bundle_identifier="com.pbailey.cinderwild"
|
||||||
application/signature=""
|
application/signature=""
|
||||||
application/app_category="Games"
|
application/app_category="Games"
|
||||||
application/short_version="0.6.0"
|
application/short_version="0.6.1"
|
||||||
application/version="0.6.0"
|
application/version="0.6.1"
|
||||||
application/copyright=""
|
application/copyright=""
|
||||||
display/high_res=true
|
display/high_res=true
|
||||||
codesign/codesign=1
|
codesign/codesign=1
|
||||||
@@ -66,7 +66,7 @@ custom_features=""
|
|||||||
export_filter="all_resources"
|
export_filter="all_resources"
|
||||||
include_filter="data/*.json, data/worldlets/*.json"
|
include_filter="data/*.json, data/worldlets/*.json"
|
||||||
exclude_filter="tests/*, docs/*, tools/*, server/*"
|
exclude_filter="tests/*, docs/*, tools/*, server/*"
|
||||||
export_path="build/windows/Emberwild/Emberwild.exe"
|
export_path="build/windows/Cinderwild/Cinderwild.exe"
|
||||||
patches=PackedStringArray()
|
patches=PackedStringArray()
|
||||||
encryption_include_filters=""
|
encryption_include_filters=""
|
||||||
encryption_exclude_filters=""
|
encryption_exclude_filters=""
|
||||||
@@ -89,11 +89,11 @@ application/modify_resources=false
|
|||||||
application/icon=""
|
application/icon=""
|
||||||
application/console_wrapper_icon=""
|
application/console_wrapper_icon=""
|
||||||
application/icon_interpolation=4
|
application/icon_interpolation=4
|
||||||
application/file_version="0.6.0"
|
application/file_version="0.6.1"
|
||||||
application/product_version="0.6.0"
|
application/product_version="0.6.1"
|
||||||
application/company_name=""
|
application/company_name=""
|
||||||
application/product_name="Emberwild"
|
application/product_name="Cinderwild"
|
||||||
application/file_description="Emberwild"
|
application/file_description="Cinderwild"
|
||||||
application/copyright=""
|
application/copyright=""
|
||||||
application/trademarks=""
|
application/trademarks=""
|
||||||
application/export_angle=0
|
application/export_angle=0
|
||||||
|
|||||||
2
play.sh
2
play.sh
@@ -1,5 +1,5 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
# Starts Emberwild (the game, with its window and title screen).
|
# Starts Cinderwild (the game, with its window and title screen).
|
||||||
#
|
#
|
||||||
# ./play.sh
|
# ./play.sh
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -14,12 +14,12 @@ compatibility/default_parent_skeleton_in_mesh_instance_3d=true
|
|||||||
|
|
||||||
[application]
|
[application]
|
||||||
|
|
||||||
config/name="Emberwild"
|
config/name="Cinderwild"
|
||||||
config/description="Low-poly survival game — Valheim look, Rust depth, self-hosted, moddable."
|
config/description="Low-poly survival game — Valheim look, Rust depth, self-hosted, moddable."
|
||||||
run/main_scene="res://scenes/title.tscn"
|
run/main_scene="res://scenes/title.tscn"
|
||||||
config/features=PackedStringArray("4.7", "Forward Plus")
|
config/features=PackedStringArray("4.7", "Forward Plus")
|
||||||
config/icon="res://icon.svg"
|
config/icon="res://icon.svg"
|
||||||
config/version="0.6.0"
|
config/version="0.6.1"
|
||||||
|
|
||||||
[autoload]
|
[autoload]
|
||||||
|
|
||||||
@@ -93,9 +93,9 @@ weather_wind={
|
|||||||
"value": 1.0
|
"value": 1.0
|
||||||
}
|
}
|
||||||
|
|
||||||
[emberwild]
|
[cinderwild]
|
||||||
|
|
||||||
updates/feed="https://git.psbailey.uk/api/v1/repos/Admin/Emberwild/releases/latest"
|
updates/feed="https://git.psbailey.uk/api/v1/repos/Admin/Cinderwild/releases/latest"
|
||||||
|
|
||||||
[rendering]
|
[rendering]
|
||||||
|
|
||||||
|
|||||||
@@ -2,16 +2,17 @@ extends Node
|
|||||||
## Autoload "Boot", first in the list: one-off start-up housekeeping that has
|
## Autoload "Boot", first in the list: one-off start-up housekeeping that has
|
||||||
## to happen before anything reads user://.
|
## to happen before anything reads user://.
|
||||||
##
|
##
|
||||||
## The game was called "Valheimlike" until it became "Emberwild". Godot names
|
## The game has been renamed twice: "Valheimlike", then "Emberwild" (0.1.0 to
|
||||||
## the user-data folder after the project, so the rename moved it from
|
## 0.6.0), now "Cinderwild". Godot names the user-data folder after the
|
||||||
## .../app_userdata/Valheimlike to .../app_userdata/Emberwild. The first time
|
## project, so each rename moved it (.../app_userdata/<name>). The first time
|
||||||
## Emberwild starts, this copies saved worlds, your identity (key, name, look)
|
## Cinderwild starts, this copies saved worlds, your identity (key, name,
|
||||||
## and server settings across. It copies rather than moves, so the old folder
|
## look), settings and server settings across from the newest old folder there
|
||||||
## stays as a backup, and it never overwrites anything already in the new
|
## is (it already holds what came from the one before). It copies rather than moves, so the old folders stay
|
||||||
## folder. Test folders (test_*) and logs are skipped.
|
## as a backup, and it never overwrites anything already in the new folder.
|
||||||
|
## Test folders (test_*) and logs are skipped.
|
||||||
|
|
||||||
const OLD_NAME := "Valheimlike"
|
const OLD_NAMES := ["Emberwild", "Valheimlike"] ## newest first
|
||||||
const MARKER := "migrated_from_valheimlike.txt"
|
const MARKER := "migrated_from_%s.txt"
|
||||||
const SKIP := ["logs", "shader_cache", "vulkan", "test_worlds", "test_net"]
|
const SKIP := ["logs", "shader_cache", "vulkan", "test_worlds", "test_net"]
|
||||||
|
|
||||||
var copied := 0
|
var copied := 0
|
||||||
@@ -22,14 +23,25 @@ func _enter_tree() -> void:
|
|||||||
## Returns the number of files copied (0 if there was nothing to do).
|
## Returns the number of files copied (0 if there was nothing to do).
|
||||||
func migrate_user_data() -> int:
|
func migrate_user_data() -> int:
|
||||||
var new_dir := OS.get_user_data_dir()
|
var new_dir := OS.get_user_data_dir()
|
||||||
var old_dir := new_dir.get_base_dir().path_join(OLD_NAME)
|
for old_name in OLD_NAMES:
|
||||||
|
var old_dir := new_dir.get_base_dir().path_join(old_name)
|
||||||
|
if DirAccess.dir_exists_absolute(old_dir) and old_dir != new_dir:
|
||||||
|
# Only the newest: it already holds whatever came across from the
|
||||||
|
# one before (a world deleted since shouldn't come back).
|
||||||
|
return migrate_from(old_dir, new_dir, old_name)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
## Copies `old_dir` into `new_dir` once (a marker file in `new_dir` says it's
|
||||||
|
## been done). Returns the number of files copied.
|
||||||
|
func migrate_from(old_dir: String, new_dir: String, old_name: String) -> int:
|
||||||
if new_dir == old_dir or not DirAccess.dir_exists_absolute(old_dir):
|
if new_dir == old_dir or not DirAccess.dir_exists_absolute(old_dir):
|
||||||
return 0
|
return 0
|
||||||
if FileAccess.file_exists(new_dir.path_join(MARKER)):
|
var marker := new_dir.path_join(MARKER % old_name.to_lower())
|
||||||
|
if FileAccess.file_exists(marker):
|
||||||
return 0
|
return 0
|
||||||
copied = 0
|
copied = 0
|
||||||
_copy_tree(old_dir, new_dir)
|
_copy_tree(old_dir, new_dir)
|
||||||
var f := FileAccess.open(new_dir.path_join(MARKER), FileAccess.WRITE)
|
var f := FileAccess.open(marker, FileAccess.WRITE)
|
||||||
if f != null:
|
if f != null:
|
||||||
f.store_string("Copied %d files from %s on %s. The old folder was left as it was.\n" % [
|
f.store_string("Copied %d files from %s on %s. The old folder was left as it was.\n" % [
|
||||||
copied, old_dir, Time.get_datetime_string_from_system()])
|
copied, old_dir, Time.get_datetime_string_from_system()])
|
||||||
@@ -43,6 +55,8 @@ func _copy_tree(from: String, to: String) -> void:
|
|||||||
if da == null:
|
if da == null:
|
||||||
return
|
return
|
||||||
for f in da.get_files():
|
for f in da.get_files():
|
||||||
|
if f.begins_with("migrated_from_"):
|
||||||
|
continue # (the old folder's own markers)
|
||||||
var dst := to.path_join(f)
|
var dst := to.path_join(f)
|
||||||
if not FileAccess.file_exists(dst) and DirAccess.copy_absolute(from.path_join(f), dst) == OK:
|
if not FileAccess.file_exists(dst) and DirAccess.copy_absolute(from.path_join(f), dst) == OK:
|
||||||
copied += 1
|
copied += 1
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
extends RefCounted
|
extends RefCounted
|
||||||
## Report a bug (0.6.0, the playtest; pause menu): opens a new issue on the
|
## Report a bug (0.6.0, the playtest; pause menu): opens a new issue on the
|
||||||
## Emberwild repo in your browser, already filled in with a title to edit,
|
## Cinderwild repo in your browser, already filled in with a title to edit,
|
||||||
## a few questions to answer, and the details we'd ask for anyway (version,
|
## a few questions to answer, and the details we'd ask for anyway (version,
|
||||||
## computer, single-player or online, which land, the day and time, where you
|
## computer, single-player or online, which land, the day and time, where you
|
||||||
## are). You read it, add what happened, and press Create Issue: nothing is
|
## are). You read it, add what happened, and press Create Issue: nothing is
|
||||||
@@ -10,7 +10,7 @@ extends RefCounted
|
|||||||
## crash or an error: the pause menu shows where it is and opens its folder,
|
## crash or an error: the pause menu shows where it is and opens its folder,
|
||||||
## to attach the file to the issue.
|
## to attach the file to the issue.
|
||||||
|
|
||||||
const REPO := "https://git.psbailey.uk/Admin/Emberwild"
|
const REPO := "https://git.psbailey.uk/Admin/Cinderwild"
|
||||||
|
|
||||||
## An autoload (Net, Updates), looked up at run time: this script is also
|
## An autoload (Net, Updates), looked up at run time: this script is also
|
||||||
## loaded by tests that start before the autoloads are compiled in.
|
## loaded by tests that start before the autoloads are compiled in.
|
||||||
@@ -46,7 +46,7 @@ static func mode(world: Node) -> String:
|
|||||||
## The details, one per line (what's in the issue, and what "Copy" copies).
|
## The details, one per line (what's in the issue, and what "Copy" copies).
|
||||||
static func details(world: Node) -> Dictionary:
|
static func details(world: Node) -> Dictionary:
|
||||||
var d := {
|
var d := {
|
||||||
"Version": "Emberwild %s (protocol %d)" % [String(ProjectSettings.get_setting("application/config/version", "?")), int(_al("Net").PROTOCOL)],
|
"Version": "Cinderwild %s (protocol %d)" % [String(ProjectSettings.get_setting("application/config/version", "?")), int(_al("Net").PROTOCOL)],
|
||||||
"Computer": "%s %s" % [OS.get_name(), OS.get_version()],
|
"Computer": "%s %s" % [OS.get_name(), OS.get_version()],
|
||||||
"Graphics": "%s (%s)" % [RenderingServer.get_video_adapter_name(), RenderingServer.get_current_rendering_method()],
|
"Graphics": "%s (%s)" % [RenderingServer.get_video_adapter_name(), RenderingServer.get_current_rendering_method()],
|
||||||
"Mode": mode(world),
|
"Mode": mode(world),
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
extends Node
|
extends Node
|
||||||
## The update check (docs/ROADMAP.md, M4 "Updates", first step): is there a
|
## The update check (docs/ROADMAP.md, M4 "Updates", first step): is there a
|
||||||
## newer Emberwild than this one? Asked once at start-up, of the feed in
|
## newer Cinderwild than this one? Asked once at start-up, of the feed in
|
||||||
## Project Settings (`emberwild/updates/feed`). The feed is either
|
## Project Settings (`cinderwild/updates/feed`). The feed is either
|
||||||
## - a release API answer (Gitea, Forgejo or GitHub `releases/latest`): its
|
## - a release API answer (Gitea, Forgejo or GitHub `releases/latest`): its
|
||||||
## `version.json` asset is fetched next, and its other assets are the
|
## `version.json` asset is fetched next, and its other assets are the
|
||||||
## downloads, by file name; or
|
## downloads, by file name; or
|
||||||
## - a `version.json` itself:
|
## - a `version.json` itself:
|
||||||
## {"version": "0.2.0", "protocol": 13, "notes": "...", "page": "https://...",
|
## {"version": "0.2.0", "protocol": 13, "notes": "...", "page": "https://...",
|
||||||
## "downloads": {"macos": {"file": "Emberwild-0.2.0-macos.zip", "sha256": "...",
|
## "downloads": {"macos": {"file": "Cinderwild-0.2.0-macos.zip", "sha256": "...",
|
||||||
## "size": 61164958, "url": "(optional; else the asset)"},
|
## "size": 61164958, "url": "(optional; else the asset)"},
|
||||||
## "windows": {...}}}
|
## "windows": {...}}}
|
||||||
## tools/export.sh writes that file next to the zips, for the release.
|
## tools/export.sh writes that file next to the zips, for the release.
|
||||||
@@ -22,7 +22,7 @@ signal checked(state: String)
|
|||||||
const TIMEOUT := 10.0
|
const TIMEOUT := 10.0
|
||||||
|
|
||||||
var version: String = String(ProjectSettings.get_setting("application/config/version", "0.0.0"))
|
var version: String = String(ProjectSettings.get_setting("application/config/version", "0.0.0"))
|
||||||
var feed: String = String(ProjectSettings.get_setting("emberwild/updates/feed", ""))
|
var feed: String = String(ProjectSettings.get_setting("cinderwild/updates/feed", ""))
|
||||||
var prefs_path := "user://updates.json"
|
var prefs_path := "user://updates.json"
|
||||||
var enabled := true
|
var enabled := true
|
||||||
## idle | off | checking | current | available | failed
|
## idle | off | checking | current | available | failed
|
||||||
@@ -101,7 +101,7 @@ func _finish(s: String) -> void:
|
|||||||
if s == "failed":
|
if s == "failed":
|
||||||
print("[updates] couldn't check for updates: ", error)
|
print("[updates] couldn't check for updates: ", error)
|
||||||
elif s == "available":
|
elif s == "available":
|
||||||
print("[updates] Emberwild %s is out (this is %s)" % [latest_version(), version])
|
print("[updates] Cinderwild %s is out (this is %s)" % [latest_version(), version])
|
||||||
checked.emit(s)
|
checked.emit(s)
|
||||||
|
|
||||||
func _get_json(url: String) -> Variant:
|
func _get_json(url: String) -> Variant:
|
||||||
@@ -112,7 +112,7 @@ func _get_json(url: String) -> Variant:
|
|||||||
var http := HTTPRequest.new()
|
var http := HTTPRequest.new()
|
||||||
http.timeout = TIMEOUT
|
http.timeout = TIMEOUT
|
||||||
add_child(http)
|
add_child(http)
|
||||||
var err := http.request(url, ["Accept: application/json", "User-Agent: Emberwild/" + version])
|
var err := http.request(url, ["Accept: application/json", "User-Agent: Cinderwild/" + version])
|
||||||
if err != OK:
|
if err != OK:
|
||||||
http.queue_free()
|
http.queue_free()
|
||||||
error = "couldn't ask %s (%d)" % [url, err]
|
error = "couldn't ask %s (%d)" % [url, err]
|
||||||
|
|||||||
@@ -176,7 +176,7 @@ func _start_dedicated() -> void:
|
|||||||
|
|
||||||
func _on_update_checked(s: String) -> void:
|
func _on_update_checked(s: String) -> void:
|
||||||
if s == "available":
|
if s == "available":
|
||||||
print("[server] Emberwild %s is out (protocol %d); this server runs %s (protocol %d). %s" % [
|
print("[server] Cinderwild %s is out (protocol %d); this server runs %s (protocol %d). %s" % [
|
||||||
Updates.latest_version(), Updates.latest_protocol(), Updates.version, PROTOCOL,
|
Updates.latest_version(), Updates.latest_protocol(), Updates.version, PROTOCOL,
|
||||||
"Players with the new version can't join until it's updated." if Updates.latest_protocol() != PROTOCOL else ""])
|
"Players with the new version can't join until it's updated." if Updates.latest_protocol() != PROTOCOL else ""])
|
||||||
|
|
||||||
@@ -694,7 +694,7 @@ func _c2s_hello(d: Dictionary) -> void:
|
|||||||
return
|
return
|
||||||
if int(d.get("protocol", 0)) != PROTOCOL:
|
if int(d.get("protocol", 0)) != PROTOCOL:
|
||||||
var theirs := int(d.get("protocol", 0))
|
var theirs := int(d.get("protocol", 0))
|
||||||
_reject(peer, "Different game version: this server runs Emberwild %s (protocol %d), you have protocol %d. %s" % [
|
_reject(peer, "Different game version: this server runs Cinderwild %s (protocol %d), you have protocol %d. %s" % [
|
||||||
Updates.version, PROTOCOL, theirs, "Update the game to join." if theirs < PROTOCOL else "The server needs updating."])
|
Updates.version, PROTOCOL, theirs, "Update the game to join." if theirs < PROTOCOL else "The server needs updating."])
|
||||||
return
|
return
|
||||||
# (A crossing to another land comes with a one-time pass instead, checked
|
# (A crossing to another land comes with a one-time pass instead, checked
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ extends RefCounted
|
|||||||
|
|
||||||
const DEFAULT_PATH := "user://server/server.json"
|
const DEFAULT_PATH := "user://server/server.json"
|
||||||
const DEFAULTS := {
|
const DEFAULTS := {
|
||||||
"server_name": "Emberwild server",
|
"server_name": "Cinderwild server",
|
||||||
"motd": "Welcome!",
|
"motd": "Welcome!",
|
||||||
"port": 24242,
|
"port": 24242,
|
||||||
"max_players": 8,
|
"max_players": 8,
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ func bind(p: CharacterBody3D) -> void:
|
|||||||
_main.add_child(tips_row)
|
_main.add_child(tips_row)
|
||||||
|
|
||||||
var upd := CheckButton.new()
|
var upd := CheckButton.new()
|
||||||
upd.text = "Check for updates at start (Emberwild %s)" % Updates.version
|
upd.text = "Check for updates at start (Cinderwild %s)" % Updates.version
|
||||||
upd.focus_mode = Control.FOCUS_NONE
|
upd.focus_mode = Control.FOCUS_NONE
|
||||||
upd.button_pressed = Updates.enabled
|
upd.button_pressed = Updates.enabled
|
||||||
upd.add_theme_font_size_override("font_size", 16)
|
upd.add_theme_font_size_override("font_size", 16)
|
||||||
@@ -187,7 +187,7 @@ func bind(p: CharacterBody3D) -> void:
|
|||||||
var rt := UITheme.label("Report a bug", 26)
|
var rt := UITheme.label("Report a bug", 26)
|
||||||
rt.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
rt.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
_report.add_child(rt)
|
_report.add_child(rt)
|
||||||
var how := UITheme.label("This opens a new issue on Emberwild's Gitea page in your browser, filled in with your version, computer and where you are in the game. Say what happened, then press Create Issue. (You'll need to be signed in there.) Nothing is sent from the game itself.", 15)
|
var how := UITheme.label("This opens a new issue on Cinderwild's Gitea page in your browser, filled in with your version, computer and where you are in the game. Say what happened, then press Create Issue. (You'll need to be signed in there.) Nothing is sent from the game itself.", 15)
|
||||||
how.autowrap_mode = TextServer.AUTOWRAP_WORD
|
how.autowrap_mode = TextServer.AUTOWRAP_WORD
|
||||||
how.custom_minimum_size = Vector2(380, 0)
|
how.custom_minimum_size = Vector2(380, 0)
|
||||||
_report.add_child(how)
|
_report.add_child(how)
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ func _ready() -> void:
|
|||||||
col.offset_right = 520
|
col.offset_right = 520
|
||||||
col.add_theme_constant_override("separation", 14)
|
col.add_theme_constant_override("separation", 14)
|
||||||
root.add_child(col)
|
root.add_child(col)
|
||||||
col.add_child(UITheme.label("EMBERWILD", 54))
|
col.add_child(UITheme.label("CINDERWILD", 54))
|
||||||
col.add_child(UITheme.label("Survive the island. Build a home.", 18, UITheme.TEXT_DIM))
|
col.add_child(UITheme.label("Survive the island. Build a home.", 18, UITheme.TEXT_DIM))
|
||||||
var spacer := Control.new()
|
var spacer := Control.new()
|
||||||
spacer.custom_minimum_size = Vector2(0, 24)
|
spacer.custom_minimum_size = Vector2(0, 24)
|
||||||
@@ -413,7 +413,7 @@ func _host_page() -> Control:
|
|||||||
func _fill_host() -> void:
|
func _fill_host() -> void:
|
||||||
var c: RefCounted = load("res://scripts/net/server_config.gd").open(Net.host_config)
|
var c: RefCounted = load("res://scripts/net/server_config.gd").open(Net.host_config)
|
||||||
var n: String = c.data.server_name
|
var n: String = c.data.server_name
|
||||||
_host_name.text = "%s's island" % Net.get_identity().name if n == "Emberwild server" else n
|
_host_name.text = "%s's island" % Net.get_identity().name if n in ["Cinderwild server", "Emberwild server"] else n
|
||||||
_host_port.text = str(int(c.data.port))
|
_host_port.text = str(int(c.data.port))
|
||||||
_host_pw.text = String(c.data.password)
|
_host_pw.text = String(c.data.password)
|
||||||
var preset := "co-op"
|
var preset := "co-op"
|
||||||
@@ -801,7 +801,7 @@ var version_label: Label
|
|||||||
## The version, bottom left; and a card bottom right when a newer one is out.
|
## The version, bottom left; and a card bottom right when a newer one is out.
|
||||||
func _build_update_banner(root: Control) -> void:
|
func _build_update_banner(root: Control) -> void:
|
||||||
# (bottom right: the menu column runs down the left and would cover it)
|
# (bottom right: the menu column runs down the left and would cover it)
|
||||||
version_label = UITheme.label("Emberwild %s" % Updates.version, 13, UITheme.TEXT_DIM)
|
version_label = UITheme.label("Cinderwild %s" % Updates.version, 13, UITheme.TEXT_DIM)
|
||||||
version_label.set_anchors_and_offsets_preset(Control.PRESET_BOTTOM_RIGHT)
|
version_label.set_anchors_and_offsets_preset(Control.PRESET_BOTTOM_RIGHT)
|
||||||
version_label.grow_horizontal = Control.GROW_DIRECTION_BEGIN
|
version_label.grow_horizontal = Control.GROW_DIRECTION_BEGIN
|
||||||
version_label.grow_vertical = Control.GROW_DIRECTION_BEGIN
|
version_label.grow_vertical = Control.GROW_DIRECTION_BEGIN
|
||||||
@@ -842,7 +842,7 @@ func _on_update_checked(s: String) -> void:
|
|||||||
return
|
return
|
||||||
var notes := String(Updates.latest.get("notes", ""))
|
var notes := String(Updates.latest.get("notes", ""))
|
||||||
var how := "" if Updates.packaged() else "\nRunning from the source: git pull in the game folder, then restart."
|
var how := "" if Updates.packaged() else "\nRunning from the source: git pull in the game folder, then restart."
|
||||||
_update_text.text = "Emberwild %s is out (you have %s).%s%s" % [Updates.latest_version(), Updates.version,
|
_update_text.text = "Cinderwild %s is out (you have %s).%s%s" % [Updates.latest_version(), Updates.version,
|
||||||
("\n" + notes) if not notes.is_empty() else "", how]
|
("\n" + notes) if not notes.is_empty() else "", how]
|
||||||
_update_btn.text = "Download" if Updates.packaged() and not String(Updates.download().get("url", "")).is_empty() else "Release page"
|
_update_btn.text = "Download" if Updates.packaged() and not String(Updates.download().get("url", "")).is_empty() else "Release page"
|
||||||
_update_btn.disabled = Updates.download_url().is_empty()
|
_update_btn.disabled = Updates.download_url().is_empty()
|
||||||
@@ -858,7 +858,7 @@ static func version_advice(reason: String) -> String:
|
|||||||
if not reason.begins_with("Different game version"):
|
if not reason.begins_with("Different game version"):
|
||||||
return ""
|
return ""
|
||||||
if Updates.state == "available":
|
if Updates.state == "available":
|
||||||
return "\nEmberwild %s is out: Download is on the title page." % Updates.latest_version()
|
return "\nCinderwild %s is out: Download is on the title page." % Updates.latest_version()
|
||||||
if reason.ends_with("Update the game to join."):
|
if reason.ends_with("Update the game to join."):
|
||||||
return "\nThere's no newer release yet: the server may be running a newer copy from the source."
|
return "\nThere's no newer release yet: the server may be running a newer copy from the source."
|
||||||
return ""
|
return ""
|
||||||
|
|||||||
36
server/cinderwild-server.service
Normal file
36
server/cinderwild-server.service
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# Cinderwild dedicated server as a systemd service (Linux).
|
||||||
|
#
|
||||||
|
# 1. Put the game where it'll live, e.g. /opt/cinderwild (a git clone), and
|
||||||
|
# install Godot 4.7+ as /usr/local/bin/godot.
|
||||||
|
# 2. Make a user for it: sudo useradd -r -m -d /var/lib/cinderwild cinderwild
|
||||||
|
# 3. Copy this file: sudo cp server/cinderwild-server.service /etc/systemd/system/
|
||||||
|
# and edit User, WorkingDirectory and GODOT below if yours differ.
|
||||||
|
# 4. Start it, and on boot: sudo systemctl enable --now cinderwild-server
|
||||||
|
# Logs: journalctl -u cinderwild-server -f
|
||||||
|
# Stop / restart: sudo systemctl stop|restart cinderwild-server
|
||||||
|
#
|
||||||
|
# server.json and the world live in the user's Godot data folder:
|
||||||
|
# /var/lib/cinderwild/.local/share/godot/app_userdata/Cinderwild/server/
|
||||||
|
# Admin commands: join the game and use chat (/help), or edit server.json and
|
||||||
|
# restart. (The console needs a terminal, so it isn't used here.)
|
||||||
|
|
||||||
|
[Unit]
|
||||||
|
Description=Cinderwild dedicated server
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=cinderwild
|
||||||
|
WorkingDirectory=/opt/cinderwild
|
||||||
|
Environment=GODOT=/usr/local/bin/godot
|
||||||
|
# run_server.sh checks the Godot version and imports a fresh copy first.
|
||||||
|
ExecStart=/opt/cinderwild/server/run_server.sh
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=10
|
||||||
|
# The world saves within 10 s of any change, so a stop loses at most that.
|
||||||
|
KillSignal=SIGINT
|
||||||
|
TimeoutStopSec=20
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
@@ -1,17 +1,17 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<!--
|
<!--
|
||||||
Emberwild dedicated server as a launchd agent (macOS): starts when you log
|
Cinderwild dedicated server as a launchd agent (macOS): starts when you log
|
||||||
in, and restarts if it stops.
|
in, and restarts if it stops.
|
||||||
|
|
||||||
1. Edit the two paths below (the game folder, and your user name in the log
|
1. Edit the two paths below (the game folder, and your user name in the log
|
||||||
path) if yours differ.
|
path) if yours differ.
|
||||||
2. Copy it: cp server/com.emberwild.server.plist ~/Library/LaunchAgents/
|
2. Copy it: cp server/com.cinderwild.server.plist ~/Library/LaunchAgents/
|
||||||
3. Start it: launchctl load -w ~/Library/LaunchAgents/com.emberwild.server.plist
|
3. Start it: launchctl load -w ~/Library/LaunchAgents/com.cinderwild.server.plist
|
||||||
Stop it: launchctl unload ~/Library/LaunchAgents/com.emberwild.server.plist
|
Stop it: launchctl unload ~/Library/LaunchAgents/com.cinderwild.server.plist
|
||||||
Log: tail -f ~/Library/Logs/emberwild-server.log
|
Log: tail -f ~/Library/Logs/cinderwild-server.log
|
||||||
|
|
||||||
server.json and the world are in
|
server.json and the world are in
|
||||||
~/Library/Application Support/Godot/app_userdata/Emberwild/server/
|
~/Library/Application Support/Godot/app_userdata/Cinderwild/server/
|
||||||
For admin commands, join the game and use chat (/help), or edit server.json
|
For admin commands, join the game and use chat (/help), or edit server.json
|
||||||
and stop/start it. (For a console, run server/run_server.sh in Terminal
|
and stop/start it. (For a console, run server/run_server.sh in Terminal
|
||||||
instead.)
|
instead.)
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
<key>Label</key>
|
<key>Label</key>
|
||||||
<string>com.emberwild.server</string>
|
<string>com.cinderwild.server</string>
|
||||||
<key>ProgramArguments</key>
|
<key>ProgramArguments</key>
|
||||||
<array>
|
<array>
|
||||||
<string>/Users/home/valheimlike/server/run_server.sh</string>
|
<string>/Users/home/valheimlike/server/run_server.sh</string>
|
||||||
@@ -43,8 +43,8 @@
|
|||||||
<key>ThrottleInterval</key>
|
<key>ThrottleInterval</key>
|
||||||
<integer>15</integer>
|
<integer>15</integer>
|
||||||
<key>StandardOutPath</key>
|
<key>StandardOutPath</key>
|
||||||
<string>/Users/home/Library/Logs/emberwild-server.log</string>
|
<string>/Users/home/Library/Logs/cinderwild-server.log</string>
|
||||||
<key>StandardErrorPath</key>
|
<key>StandardErrorPath</key>
|
||||||
<string>/Users/home/Library/Logs/emberwild-server.log</string>
|
<string>/Users/home/Library/Logs/cinderwild-server.log</string>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
# Emberwild dedicated server as a systemd service (Linux).
|
|
||||||
#
|
|
||||||
# 1. Put the game where it'll live, e.g. /opt/emberwild (a git clone), and
|
|
||||||
# install Godot 4.7+ as /usr/local/bin/godot.
|
|
||||||
# 2. Make a user for it: sudo useradd -r -m -d /var/lib/emberwild emberwild
|
|
||||||
# 3. Copy this file: sudo cp server/emberwild-server.service /etc/systemd/system/
|
|
||||||
# and edit User, WorkingDirectory and GODOT below if yours differ.
|
|
||||||
# 4. Start it, and on boot: sudo systemctl enable --now emberwild-server
|
|
||||||
# Logs: journalctl -u emberwild-server -f
|
|
||||||
# Stop / restart: sudo systemctl stop|restart emberwild-server
|
|
||||||
#
|
|
||||||
# server.json and the world live in the user's Godot data folder:
|
|
||||||
# /var/lib/emberwild/.local/share/godot/app_userdata/Emberwild/server/
|
|
||||||
# Admin commands: join the game and use chat (/help), or edit server.json and
|
|
||||||
# restart. (The console needs a terminal, so it isn't used here.)
|
|
||||||
|
|
||||||
[Unit]
|
|
||||||
Description=Emberwild dedicated server
|
|
||||||
After=network-online.target
|
|
||||||
Wants=network-online.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=simple
|
|
||||||
User=emberwild
|
|
||||||
WorkingDirectory=/opt/emberwild
|
|
||||||
Environment=GODOT=/usr/local/bin/godot
|
|
||||||
# run_server.sh checks the Godot version and imports a fresh copy first.
|
|
||||||
ExecStart=/opt/emberwild/server/run_server.sh
|
|
||||||
Restart=on-failure
|
|
||||||
RestartSec=10
|
|
||||||
# The world saves within 10 s of any change, so a stop loses at most that.
|
|
||||||
KillSignal=SIGINT
|
|
||||||
TimeoutStopSec=20
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
# Starts a dedicated Emberwild server (no window, no local player).
|
# Starts a dedicated Cinderwild server (no window, no local player).
|
||||||
#
|
#
|
||||||
# server/run_server.sh defaults from server.json
|
# server/run_server.sh defaults from server.json
|
||||||
# server/run_server.sh --port 24242 --server-name "Paul's island" --password secret
|
# server/run_server.sh --port 24242 --server-name "Paul's island" --password secret
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ if [ ! -f .godot/global_script_class_cache.cfg ]; then
|
|||||||
echo "Importing the project first (fresh copy)..."
|
echo "Importing the project first (fresh copy)..."
|
||||||
godot --headless --path . --import >/dev/null 2>&1
|
godot --headless --path . --import >/dev/null 2>&1
|
||||||
fi
|
fi
|
||||||
for t in test_survival_core test_survival_stats test_save_load test_weather_building test_creatures_combat test_wolves test_alpha test_bear test_thieves test_barrow test_boss test_den_sites test_fishing_diet test_food test_hazards test_iron test_stone test_net_lobby test_net_world test_net_shared test_net_life test_net_boss test_net_rules test_net_lands test_host test_tutorial test_tutorial_full test_hints test_updates test_audio test_items_more test_worldlets test_worldlet_pick test_water_bodies test_cove test_wreck test_shore test_boat test_bug_report test_tunnel test_map test_terrain_edges test_boundary; do
|
for t in test_boot test_survival_core test_survival_stats test_save_load test_weather_building test_creatures_combat test_wolves test_alpha test_bear test_thieves test_barrow test_boss test_den_sites test_fishing_diet test_food test_hazards test_iron test_stone test_net_lobby test_net_world test_net_shared test_net_life test_net_boss test_net_rules test_net_lands test_host test_tutorial test_tutorial_full test_hints test_updates test_audio test_items_more test_worldlets test_worldlet_pick test_water_bodies test_cove test_wreck test_shore test_boat test_bug_report test_tunnel test_map test_terrain_edges test_boundary; do
|
||||||
extra=""
|
extra=""
|
||||||
[ "$t" = "test_boundary" ] && extra="--fixed-fps 30"
|
[ "$t" = "test_boundary" ] && extra="--fixed-fps 30"
|
||||||
# A suite that waits on a world that failed to load would hang forever,
|
# A suite that waits on a world that failed to load would hang forever,
|
||||||
|
|||||||
71
tests/test_boot.gd
Normal file
71
tests/test_boot.gd
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
extends SceneTree
|
||||||
|
## The rename (Emberwild -> Cinderwild, 0.6.1): the first start copies your
|
||||||
|
## saves, identity and settings across from the old user-data folder, once,
|
||||||
|
## without overwriting anything already there, and only from the newest old
|
||||||
|
## name (so a world you deleted under Emberwild doesn't come back from the
|
||||||
|
## Valheimlike folder).
|
||||||
|
## Run: godot --headless --path . --script res://tests/test_boot.gd
|
||||||
|
|
||||||
|
var _fails := 0
|
||||||
|
|
||||||
|
func check(cond: bool, what: String) -> void:
|
||||||
|
print((" ok " if cond else " FAIL ") + what)
|
||||||
|
if not cond:
|
||||||
|
_fails += 1
|
||||||
|
|
||||||
|
func _write(path: String, text: String) -> void:
|
||||||
|
DirAccess.make_dir_recursive_absolute(path.get_base_dir())
|
||||||
|
var f := FileAccess.open(path, FileAccess.WRITE)
|
||||||
|
f.store_string(text)
|
||||||
|
f.close()
|
||||||
|
|
||||||
|
func _read(path: String) -> String:
|
||||||
|
return FileAccess.get_file_as_string(path) if FileAccess.file_exists(path) else ""
|
||||||
|
|
||||||
|
func _rm(dir: String) -> void:
|
||||||
|
if not DirAccess.dir_exists_absolute(dir):
|
||||||
|
return
|
||||||
|
var da := DirAccess.open(dir)
|
||||||
|
for f in da.get_files():
|
||||||
|
DirAccess.remove_absolute(dir.path_join(f))
|
||||||
|
for d in da.get_directories():
|
||||||
|
_rm(dir.path_join(d))
|
||||||
|
DirAccess.remove_absolute(dir)
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
var B: Node = root.get_node("Boot")
|
||||||
|
var base := OS.get_user_data_dir().path_join("test_boot")
|
||||||
|
_rm(base)
|
||||||
|
var ember := base.path_join("Emberwild")
|
||||||
|
var valheim := base.path_join("Valheimlike")
|
||||||
|
var cinder := base.path_join("Cinderwild")
|
||||||
|
_write(ember.path_join("worlds/home/state.json"), "ember home")
|
||||||
|
_write(ember.path_join("identity.json"), "me")
|
||||||
|
_write(ember.path_join("logs/godot.log"), "old log")
|
||||||
|
_write(ember.path_join("migrated_from_valheimlike.txt"), "old marker")
|
||||||
|
_write(valheim.path_join("worlds/deleted/state.json"), "gone")
|
||||||
|
_write(cinder.path_join("audio.json"), "new settings")
|
||||||
|
_write(ember.path_join("audio.json"), "old settings")
|
||||||
|
|
||||||
|
print("the first start")
|
||||||
|
var n: int = 0
|
||||||
|
for old_name in B.OLD_NAMES:
|
||||||
|
var old_dir := base.path_join(old_name)
|
||||||
|
if DirAccess.dir_exists_absolute(old_dir):
|
||||||
|
n = B.migrate_from(old_dir, cinder, old_name)
|
||||||
|
break
|
||||||
|
check(n == 2, "copies the old folder's files (%d)" % n)
|
||||||
|
check(_read(cinder.path_join("worlds/home/state.json")) == "ember home" and _read(cinder.path_join("identity.json")) == "me",
|
||||||
|
"your worlds and identity come across")
|
||||||
|
check(_read(cinder.path_join("audio.json")) == "new settings", "nothing already there is overwritten")
|
||||||
|
check(not FileAccess.file_exists(cinder.path_join("logs/godot.log")), "logs stay behind")
|
||||||
|
check(not FileAccess.file_exists(cinder.path_join("worlds/deleted/state.json")), "the older Valheimlike folder isn't raked over")
|
||||||
|
check(FileAccess.file_exists(ember.path_join("identity.json")), "the old folder's left as it was")
|
||||||
|
check(FileAccess.file_exists(cinder.path_join("migrated_from_emberwild.txt")) and not FileAccess.file_exists(cinder.path_join("migrated_from_valheimlike.txt")),
|
||||||
|
"a marker says it's done")
|
||||||
|
print("the next start")
|
||||||
|
_write(ember.path_join("worlds/later/state.json"), "later")
|
||||||
|
check(B.migrate_from(ember, cinder, "Emberwild") == 0 and not FileAccess.file_exists(cinder.path_join("worlds/later/state.json")), "it only happens once")
|
||||||
|
_rm(base)
|
||||||
|
print("RESULT: %s (%d failures)" % ["PASS" if _fails == 0 else "FAIL", _fails])
|
||||||
|
quit(1 if _fails > 0 else 0)
|
||||||
1
tests/test_boot.gd.uid
Normal file
1
tests/test_boot.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://bnsn0icdtxs16
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
extends SceneTree
|
extends SceneTree
|
||||||
## 0.6.0, the playtest: Report a bug. The pause menu has a "Report a bug"
|
## 0.6.0, the playtest: Report a bug. The pause menu has a "Report a bug"
|
||||||
## page; its link is a new issue on the Emberwild repo, filled in with
|
## page; its link is a new issue on the Cinderwild repo, filled in with
|
||||||
## questions to answer and the details (version, computer, mode, land, time,
|
## questions to answer and the details (version, computer, mode, land, time,
|
||||||
## where you are), short enough for a browser; the log's path is shown.
|
## where you are), short enough for a browser; the log's path is shown.
|
||||||
## Run: godot --headless --path . --script res://tests/test_bug_report.gd
|
## Run: godot --headless --path . --script res://tests/test_bug_report.gd
|
||||||
@@ -43,11 +43,11 @@ func _body() -> void:
|
|||||||
|
|
||||||
print("the link")
|
print("the link")
|
||||||
var url: String = BugReport.issue_url(w)
|
var url: String = BugReport.issue_url(w)
|
||||||
check(url.begins_with("https://git.psbailey.uk/Admin/Emberwild/issues/new?title=Bug%3A%20&body="), "a new issue on the Emberwild repo")
|
check(url.begins_with("https://git.psbailey.uk/Admin/Cinderwild/issues/new?title=Bug%3A%20&body="), "a new issue on the Cinderwild repo")
|
||||||
check(url.length() < 4000, "short enough for any browser (%d characters)" % url.length())
|
check(url.length() < 4000, "short enough for any browser (%d characters)" % url.length())
|
||||||
var body := url.get_slice("&body=", 1).uri_decode()
|
var body := url.get_slice("&body=", 1).uri_decode()
|
||||||
var ver: String = String(ProjectSettings.get_setting("application/config/version"))
|
var ver: String = String(ProjectSettings.get_setting("application/config/version"))
|
||||||
for want in ["**What happened?**", "**How can we make it happen again?**", "- Version: Emberwild %s" % ver,
|
for want in ["**What happened?**", "**How can we make it happen again?**", "- Version: Cinderwild %s" % ver,
|
||||||
"- Computer: ", "- Mode: single-player", "- Land: Cove", "- Time: day ", "- Where: ", "- Seed: 1337", "godot.log"]:
|
"- Computer: ", "- Mode: single-player", "- Land: Cove", "- Time: day ", "- Where: ", "- Seed: 1337", "godot.log"]:
|
||||||
check(body.contains(want), "it says %s" % want)
|
check(body.contains(want), "it says %s" % want)
|
||||||
check(BugReport.log_path().ends_with("logs/godot.log") and BugReport.log_path().begins_with("/"), "the log's full path (%s)" % BugReport.log_path())
|
check(BugReport.log_path().ends_with("logs/godot.log") and BugReport.log_path().begins_with("/"), "the log's full path (%s)" % BugReport.log_path())
|
||||||
|
|||||||
@@ -53,11 +53,11 @@ func _run() -> void:
|
|||||||
Net.arrived.connect(func(l): _arrived = l)
|
Net.arrived.connect(func(l): _arrived = l)
|
||||||
for d in [SRV, BOBS]:
|
for d in [SRV, BOBS]:
|
||||||
_rm_tree(d)
|
_rm_tree(d)
|
||||||
# (EMBERWILD_SERVER_BIN=<an exported game>: the hub, and the land servers
|
# (CINDERWILD_SERVER_BIN=<an exported game>: the hub, and the land servers
|
||||||
# it starts, are that build: checks a release does it too.)
|
# it starts, are that build: checks a release does it too.)
|
||||||
_spawn(["--server", "--port", str(PORT), "--config", ProjectSettings.globalize_path(SRV + "/server.json"),
|
_spawn(["--server", "--port", str(PORT), "--config", ProjectSettings.globalize_path(SRV + "/server.json"),
|
||||||
"--server-name", "Lands Test", "--seed", str(SEED), "--debug-cheats", "--no-creatures"], "",
|
"--server-name", "Lands Test", "--seed", str(SEED), "--debug-cheats", "--no-creatures"], "",
|
||||||
OS.get_environment("EMBERWILD_SERVER_BIN"))
|
OS.get_environment("CINDERWILD_SERVER_BIN"))
|
||||||
await _sleep(4.0)
|
await _sleep(4.0)
|
||||||
await _body()
|
await _body()
|
||||||
if world != null and is_instance_valid(world):
|
if world != null and is_instance_valid(world):
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ func _body() -> void:
|
|||||||
var plat: String = C.platform_key()
|
var plat: String = C.platform_key()
|
||||||
print("a version.json feed")
|
print("a version.json feed")
|
||||||
pages[U.feed] = JSON.stringify({"version": "9.1.0", "protocol": 99, "notes": "The Cove.", "page": "https://example.test/r/9.1.0",
|
pages[U.feed] = JSON.stringify({"version": "9.1.0", "protocol": 99, "notes": "The Cove.", "page": "https://example.test/r/9.1.0",
|
||||||
"downloads": {plat: {"file": "Emberwild-9.1.0-%s.zip" % plat, "url": "https://example.test/dl/%s.zip" % plat, "sha256": "ab"}}})
|
"downloads": {plat: {"file": "Cinderwild-9.1.0-%s.zip" % plat, "url": "https://example.test/dl/%s.zip" % plat, "sha256": "ab"}}})
|
||||||
check(await _check_now() == "available", "9.1.0 is out: available")
|
check(await _check_now() == "available", "9.1.0 is out: available")
|
||||||
check(U.latest_version() == "9.1.0" and U.latest_protocol() == 99, "its version and protocol")
|
check(U.latest_version() == "9.1.0" and U.latest_protocol() == 99, "its version and protocol")
|
||||||
check(U.download_url() == "https://example.test/dl/%s.zip" % plat and U.download().sha256 == "ab", "this platform's download (%s), with its checksum" % plat)
|
check(U.download_url() == "https://example.test/dl/%s.zip" % plat and U.download().sha256 == "ab", "this platform's download (%s), with its checksum" % plat)
|
||||||
@@ -67,9 +67,9 @@ func _body() -> void:
|
|||||||
U.feed = "https://example.test/api/releases/latest"
|
U.feed = "https://example.test/api/releases/latest"
|
||||||
pages[U.feed] = JSON.stringify({"tag_name": "v9.2.0", "html_url": "https://example.test/releases/v9.2.0", "assets": [
|
pages[U.feed] = JSON.stringify({"tag_name": "v9.2.0", "html_url": "https://example.test/releases/v9.2.0", "assets": [
|
||||||
{"name": "version.json", "browser_download_url": "https://example.test/a/version.json"},
|
{"name": "version.json", "browser_download_url": "https://example.test/a/version.json"},
|
||||||
{"name": "Emberwild-9.2.0-%s.zip" % plat, "browser_download_url": "https://example.test/a/game-%s.zip" % plat}]})
|
{"name": "Cinderwild-9.2.0-%s.zip" % plat, "browser_download_url": "https://example.test/a/game-%s.zip" % plat}]})
|
||||||
pages["https://example.test/a/version.json"] = JSON.stringify({"version": "9.2.0", "protocol": 99,
|
pages["https://example.test/a/version.json"] = JSON.stringify({"version": "9.2.0", "protocol": 99,
|
||||||
"downloads": {plat: {"file": "Emberwild-9.2.0-%s.zip" % plat, "sha256": "cd"}}, "windows_only_key": true})
|
"downloads": {plat: {"file": "Cinderwild-9.2.0-%s.zip" % plat, "sha256": "cd"}}, "windows_only_key": true})
|
||||||
check(await _check_now() == "available" and U.latest_version() == "9.2.0", "the release's version.json is read: 9.2.0")
|
check(await _check_now() == "available" and U.latest_version() == "9.2.0", "the release's version.json is read: 9.2.0")
|
||||||
check(U.download_url() == "https://example.test/a/game-%s.zip" % plat, "the download is the release asset with that file name")
|
check(U.download_url() == "https://example.test/a/game-%s.zip" % plat, "the download is the release asset with that file name")
|
||||||
check(U.page == "https://example.test/releases/v9.2.0", "and the release page is kept")
|
check(U.page == "https://example.test/releases/v9.2.0", "and the release page is kept")
|
||||||
@@ -97,7 +97,7 @@ func _body() -> void:
|
|||||||
|
|
||||||
print("the title screen")
|
print("the title screen")
|
||||||
var TS: Script = load("res://scripts/ui/title_screen.gd")
|
var TS: Script = load("res://scripts/ui/title_screen.gd")
|
||||||
var msg := "Different game version: this server runs Emberwild 9.3.0 (protocol 99), you have protocol 12. Update the game to join."
|
var msg := "Different game version: this server runs Cinderwild 9.3.0 (protocol 99), you have protocol 12. Update the game to join."
|
||||||
check(TS.version_advice(msg).contains("9.3.0 is out"), "refused for an old version: it says the new one is out")
|
check(TS.version_advice(msg).contains("9.3.0 is out"), "refused for an old version: it says the new one is out")
|
||||||
check(TS.version_advice("Server is full").is_empty(), "(other refusals: nothing added)")
|
check(TS.version_advice("Server is full").is_empty(), "(other refusals: nothing added)")
|
||||||
pages[U.feed] = JSON.stringify({"version": "9.4.0", "notes": "Sea fishing."})
|
pages[U.feed] = JSON.stringify({"version": "9.4.0", "notes": "Sea fishing."})
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Emberwild's own sounds: the music, the ambience beds and the sound effects
|
"""Cinderwild's own sounds: the music, the ambience beds and the sound effects
|
||||||
that the Kenney CC0 packs don't cover, all synthesised here (no samples, no
|
that the Kenney CC0 packs don't cover, all synthesised here (no samples, no
|
||||||
third-party audio), so they're ours to release under CC0 too.
|
third-party audio), so they're ours to release under CC0 too.
|
||||||
|
|
||||||
|
|||||||
@@ -4,9 +4,9 @@
|
|||||||
# tools/export.sh # macOS and Windows
|
# tools/export.sh # macOS and Windows
|
||||||
# tools/export.sh macOS # just one ("macOS" or "Windows")
|
# tools/export.sh macOS # just one ("macOS" or "Windows")
|
||||||
#
|
#
|
||||||
# build/Emberwild-<version>-macos.zip the .app (universal: Apple silicon + Intel),
|
# build/Cinderwild-<version>-macos.zip the .app (universal: Apple silicon + Intel),
|
||||||
# ad-hoc signed, not notarised
|
# ad-hoc signed, not notarised
|
||||||
# build/Emberwild-<version>-windows.zip Emberwild.exe + Emberwild.pck (+ a console
|
# build/Cinderwild-<version>-windows.zip Cinderwild.exe + Cinderwild.pck (+ a console
|
||||||
# wrapper), unsigned
|
# wrapper), unsigned
|
||||||
#
|
#
|
||||||
# Needs the Godot export templates for this Godot's version (Editor > Manage
|
# Needs the Godot export templates for this Godot's version (Editor > Manage
|
||||||
@@ -29,21 +29,21 @@ mkdir -p build
|
|||||||
|
|
||||||
if [ "$which" = all ] || [ "$which" = macOS ]; then
|
if [ "$which" = all ] || [ "$which" = macOS ]; then
|
||||||
rm -rf build/macos && mkdir -p build/macos
|
rm -rf build/macos && mkdir -p build/macos
|
||||||
"$GODOT" --headless --path . --export-release "macOS" build/macos/Emberwild.zip > build/export-macos.log 2>&1
|
"$GODOT" --headless --path . --export-release "macOS" build/macos/Cinderwild.zip > build/export-macos.log 2>&1
|
||||||
if [ -f build/macos/Emberwild.zip ]; then
|
if [ -f build/macos/Cinderwild.zip ]; then
|
||||||
mv build/macos/Emberwild.zip "build/Emberwild-$VERSION-macos.zip"
|
mv build/macos/Cinderwild.zip "build/Cinderwild-$VERSION-macos.zip"
|
||||||
echo "built build/Emberwild-$VERSION-macos.zip"
|
echo "built build/Cinderwild-$VERSION-macos.zip"
|
||||||
else
|
else
|
||||||
echo "macOS export failed: see build/export-macos.log"; exit 1
|
echo "macOS export failed: see build/export-macos.log"; exit 1
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "$which" = all ] || [ "$which" = Windows ]; then
|
if [ "$which" = all ] || [ "$which" = Windows ]; then
|
||||||
rm -rf build/windows && mkdir -p build/windows/Emberwild
|
rm -rf build/windows && mkdir -p build/windows/Cinderwild
|
||||||
"$GODOT" --headless --path . --export-release "Windows" build/windows/Emberwild/Emberwild.exe > build/export-windows.log 2>&1
|
"$GODOT" --headless --path . --export-release "Windows" build/windows/Cinderwild/Cinderwild.exe > build/export-windows.log 2>&1
|
||||||
if [ -f build/windows/Emberwild/Emberwild.exe ]; then
|
if [ -f build/windows/Cinderwild/Cinderwild.exe ]; then
|
||||||
(cd build/windows && rm -f "../Emberwild-$VERSION-windows.zip" && zip -qr "../Emberwild-$VERSION-windows.zip" Emberwild)
|
(cd build/windows && rm -f "../Cinderwild-$VERSION-windows.zip" && zip -qr "../Cinderwild-$VERSION-windows.zip" Cinderwild)
|
||||||
echo "built build/Emberwild-$VERSION-windows.zip"
|
echo "built build/Cinderwild-$VERSION-windows.zip"
|
||||||
else
|
else
|
||||||
echo "Windows export failed: see build/export-windows.log"; exit 1
|
echo "Windows export failed: see build/export-windows.log"; exit 1
|
||||||
fi
|
fi
|
||||||
@@ -58,7 +58,7 @@ out = {"version": version, "protocol": protocol, "date": datetime.date.today().i
|
|||||||
if notes:
|
if notes:
|
||||||
out["notes"] = notes
|
out["notes"] = notes
|
||||||
for plat in ("macos", "windows"):
|
for plat in ("macos", "windows"):
|
||||||
f = "build/Emberwild-%s-%s.zip" % (version, plat)
|
f = "build/Cinderwild-%s-%s.zip" % (version, plat)
|
||||||
if os.path.exists(f):
|
if os.path.exists(f):
|
||||||
h = hashlib.sha256(open(f, "rb").read()).hexdigest()
|
h = hashlib.sha256(open(f, "rb").read()).hexdigest()
|
||||||
out["downloads"][plat] = {"file": os.path.basename(f), "sha256": h, "size": os.path.getsize(f)}
|
out["downloads"][plat] = {"file": os.path.basename(f), "sha256": h, "size": os.path.getsize(f)}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
# Applications). It's read from the environment, never from the command line.
|
# Applications). It's read from the environment, never from the command line.
|
||||||
|
|
||||||
cd "$(dirname "$0")/.." || exit 1
|
cd "$(dirname "$0")/.." || exit 1
|
||||||
API=${API:-https://git.psbailey.uk/api/v1/repos/Admin/Emberwild}
|
API=${API:-https://git.psbailey.uk/api/v1/repos/Admin/Cinderwild}
|
||||||
[ -n "$GITEA_TOKEN" ] || { echo "set GITEA_TOKEN first: read -s GITEA_TOKEN && export GITEA_TOKEN"; exit 1; }
|
[ -n "$GITEA_TOKEN" ] || { echo "set GITEA_TOKEN first: read -s GITEA_TOKEN && export GITEA_TOKEN"; exit 1; }
|
||||||
[ -f build/version.json ] || { echo "no build/version.json: run tools/export.sh first"; exit 1; }
|
[ -f build/version.json ] || { echo "no build/version.json: run tools/export.sh first"; exit 1; }
|
||||||
VERSION=$(python3 -c "import json; print(json.load(open('build/version.json'))['version'])")
|
VERSION=$(python3 -c "import json; print(json.load(open('build/version.json'))['version'])")
|
||||||
@@ -20,12 +20,12 @@ AUTH="Authorization: token $GITEA_TOKEN"
|
|||||||
|
|
||||||
git ls-remote --exit-code origin "refs/tags/$TAG" >/dev/null || { echo "push the tag first: git push origin $TAG"; exit 1; }
|
git ls-remote --exit-code origin "refs/tags/$TAG" >/dev/null || { echo "push the tag first: git push origin $TAG"; exit 1; }
|
||||||
|
|
||||||
BODY=$(python3 -c "import json,sys; print(json.dumps({'tag_name': sys.argv[1], 'name': 'Emberwild ' + sys.argv[2], 'body': sys.argv[3], 'draft': False, 'prerelease': False}))" "$TAG" "$VERSION" "$NOTES")
|
BODY=$(python3 -c "import json,sys; print(json.dumps({'tag_name': sys.argv[1], 'name': 'Cinderwild ' + sys.argv[2], 'body': sys.argv[3], 'draft': False, 'prerelease': False}))" "$TAG" "$VERSION" "$NOTES")
|
||||||
ID=$(curl -s -H "$AUTH" -H "Content-Type: application/json" -d "$BODY" "$API/releases" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('id') or ''); sys.stderr.write(d.get('message','') + '\n') if not d.get('id') else None")
|
ID=$(curl -s -H "$AUTH" -H "Content-Type: application/json" -d "$BODY" "$API/releases" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('id') or ''); sys.stderr.write(d.get('message','') + '\n') if not d.get('id') else None")
|
||||||
[ -n "$ID" ] || { echo "couldn't create the release (above)"; exit 1; }
|
[ -n "$ID" ] || { echo "couldn't create the release (above)"; exit 1; }
|
||||||
echo "release $TAG created (id $ID)"
|
echo "release $TAG created (id $ID)"
|
||||||
|
|
||||||
for f in version.json "Emberwild-$VERSION-macos.zip" "Emberwild-$VERSION-windows.zip"; do
|
for f in version.json "Cinderwild-$VERSION-macos.zip" "Cinderwild-$VERSION-windows.zip"; do
|
||||||
[ -f "build/$f" ] || { echo "missing build/$f"; exit 1; }
|
[ -f "build/$f" ] || { echo "missing build/$f"; exit 1; }
|
||||||
R=$(curl -s -H "$AUTH" -F "attachment=@build/$f" "$API/releases/$ID/assets?name=$f")
|
R=$(curl -s -H "$AUTH" -F "attachment=@build/$f" "$API/releases/$ID/assets?name=$f")
|
||||||
echo "$R" | grep -q '"browser_download_url"' && echo " uploaded $f" || { echo " upload of $f failed: $R"; exit 1; }
|
echo "$R" | grep -q '"browser_download_url"' && echo " uploaded $f" || { echo " upload of $f failed: $R"; exit 1; }
|
||||||
@@ -34,7 +34,7 @@ done
|
|||||||
# Check what anyone downloads is what was built.
|
# Check what anyone downloads is what was built.
|
||||||
REPO=${API%/api/v1/repos/*}/$(echo "$API" | sed 's#.*/repos/##')
|
REPO=${API%/api/v1/repos/*}/$(echo "$API" | sed 's#.*/repos/##')
|
||||||
for p in macos windows; do
|
for p in macos windows; do
|
||||||
f="Emberwild-$VERSION-$p.zip"
|
f="Cinderwild-$VERSION-$p.zip"
|
||||||
want=$(python3 -c "import json,sys; print(json.load(open('build/version.json'))['downloads'][sys.argv[1]]['sha256'])" $p)
|
want=$(python3 -c "import json,sys; print(json.load(open('build/version.json'))['downloads'][sys.argv[1]]['sha256'])" $p)
|
||||||
got=$(curl -s -L "$REPO/releases/download/$TAG/$f" | shasum -a 256 | cut -d' ' -f1)
|
got=$(curl -s -L "$REPO/releases/download/$TAG/$f" | shasum -a 256 | cut -d' ' -f1)
|
||||||
[ "$want" = "$got" ] && echo " $f: checksum ok" || { echo " $f: CHECKSUM MISMATCH"; exit 1; }
|
[ "$want" = "$got" ] && echo " $f: checksum ok" || { echo " $f: CHECKSUM MISMATCH"; exit 1; }
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
## Getting started {#getting-started}
|
## Getting started {#getting-started}
|
||||||
|
|
||||||
Emberwild drops you on a wooded island ringed by mountains, with a lake in the middle and sea cliffs on one side. You start with nothing: everything comes from the island.
|
Cinderwild drops you on a wooded island ringed by mountains, with a lake in the middle and sea cliffs on one side. You start with nothing: everything comes from the island.
|
||||||
|
|
||||||
### Installing
|
### Installing
|
||||||
|
|
||||||
- **Mac:** unzip and move Emberwild to Applications. It isn't notarised by Apple, so the first time, right-click it and choose **Open** (or System Settings > Privacy & Security > Open Anyway).
|
- **Mac:** unzip and move Cinderwild to Applications. It isn't notarised by Apple, so the first time, right-click it and choose **Open** (or System Settings > Privacy & Security > Open Anyway).
|
||||||
- **Windows:** unzip and run `Emberwild.exe`. Keep `Emberwild.pck` in the same folder or the game won't start. It's unsigned, so if SmartScreen says "Windows protected your PC", choose **More info**, then **Run anyway**.
|
- **Windows:** unzip and run `Cinderwild.exe`. Keep `Cinderwild.pck` in the same folder or the game won't start. It's unsigned, so if SmartScreen says "Windows protected your PC", choose **More info**, then **Run anyway**.
|
||||||
|
|
||||||
### The title screen
|
### The title screen
|
||||||
|
|
||||||
@@ -430,11 +430,11 @@ The music follows what's happening: calm by day, a night theme after dark, a sto
|
|||||||
|
|
||||||
### Reporting a bug
|
### Reporting a bug
|
||||||
|
|
||||||
**Report a bug** in the pause menu opens a new issue on Emberwild's [Gitea page](https://git.psbailey.uk/Admin/Emberwild/issues) in your browser, already filled in with your version, computer, mode, land, the in-game time and where you are. Say what happened and how to make it happen again, then press **Create Issue** (you'll need to be signed in there). Nothing is sent from the game itself. **Copy the details** puts the same details on your clipboard instead, and **Show the log file** opens the folder with `godot.log`: attach it for a crash or an error.
|
**Report a bug** in the pause menu opens a new issue on Cinderwild's [Gitea page](https://git.psbailey.uk/Admin/Cinderwild/issues) in your browser, already filled in with your version, computer, mode, land, the in-game time and where you are. Say what happened and how to make it happen again, then press **Create Issue** (you'll need to be signed in there). Nothing is sent from the game itself. **Copy the details** puts the same details on your clipboard instead, and **Show the log file** opens the folder with `godot.log`: attach it for a crash or an error.
|
||||||
|
|
||||||
### Updates
|
### Updates
|
||||||
|
|
||||||
With the update check on, the title screen tells you when a newer Emberwild is out and offers a **Download** button or the release page.
|
With the update check on, the title screen tells you when a newer Cinderwild is out and offers a **Download** button or the release page.
|
||||||
|
|
||||||
## Tips for your first days {#tips}
|
## Tips for your first days {#tips}
|
||||||
|
|
||||||
|
|||||||
@@ -3,124 +3,340 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>Emberwild: a self-hosted co-op survival game</title>
|
<title>Cinderwild: a self-hosted co-op survival game</title>
|
||||||
<meta name="description" content="Emberwild is a low-poly survival game: gather, hunt, build a home and hold out through the night, alone or with friends on your own server.">
|
<meta name="description" content="Cinderwild is a low-poly survival game: gather, hunt, build a home and hold out through the night, then dig through the sea cliffs to the Cove. Alone, or with friends on your own server.">
|
||||||
<meta property="og:title" content="Emberwild">
|
<meta name="theme-color" content="#15110d">
|
||||||
<meta property="og:description" content="Survive the island. Build a home. Self-hosted co-op survival.">
|
<meta property="og:type" content="website">
|
||||||
<meta property="og:image" content="poster.jpg">
|
<meta property="og:url" content="{{SITE_URL}}/">
|
||||||
|
<meta property="og:title" content="Cinderwild">
|
||||||
|
<meta property="og:description" content="Survive the island. Build a home. Self-hosted co-op survival for Mac and Windows.">
|
||||||
|
<meta property="og:image" content="{{SITE_URL}}/poster.jpg">
|
||||||
|
<meta name="twitter:card" content="summary_large_image">
|
||||||
<style>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
--bg: #15110d; --panel: #221b14; --line: #3a2e22;
|
--bg: #15110d; --bg2: #1b1510; --panel: #221b14; --line: #3a2e22;
|
||||||
--text: #f1e8da; --dim: #b8a892; --ember: #e8833a; --ember-hi: #f6a35c;
|
--text: #f1e8da; --dim: #bcae98; --ember: #e8833a; --ember-hi: #f6a35c;
|
||||||
}
|
}
|
||||||
* { box-sizing: border-box; }
|
* { box-sizing: border-box; }
|
||||||
|
html { scroll-behavior: smooth; }
|
||||||
body { margin: 0; background: var(--bg); color: var(--text);
|
body { margin: 0; background: var(--bg); color: var(--text);
|
||||||
font: 17px/1.55 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; }
|
font: 17px/1.6 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; }
|
||||||
a { color: var(--ember-hi); }
|
a { color: var(--ember-hi); }
|
||||||
a:focus-visible, button:focus-visible { outline: 3px solid var(--ember-hi); outline-offset: 3px; }
|
a:focus-visible, button:focus-visible, video:focus-visible { outline: 3px solid var(--ember-hi); outline-offset: 3px; }
|
||||||
.wrap { max-width: 980px; margin: 0 auto; padding: 0 20px; }
|
img { max-width: 100%; height: auto; display: block; }
|
||||||
header { padding: 48px 0 20px; text-align: center; }
|
.skip { position: absolute; left: -9999px; top: 8px; background: var(--ember); color: #1a120a; padding: 8px 14px; border-radius: 6px; z-index: 10; }
|
||||||
h1 { font-size: clamp(44px, 9vw, 84px); letter-spacing: .06em; margin: 0; font-weight: 600; }
|
.skip:focus { left: 8px; }
|
||||||
.tag { color: var(--dim); font-size: 20px; margin: 6px 0 0; }
|
.wrap { max-width: 1080px; margin: 0 auto; padding: 0 22px; }
|
||||||
.guide-link { margin: 10px 0 0; font-size: 17px; }
|
|
||||||
.video { margin: 28px 0 0; border: 1px solid var(--line); border-radius: 10px; overflow: hidden;
|
/* Top bar */
|
||||||
background: #000; aspect-ratio: 16 / 9; }
|
nav.top { position: absolute; inset: 0 0 auto 0; z-index: 2; }
|
||||||
.video video { width: 100%; height: 100%; display: block; }
|
nav.top .wrap { display: flex; align-items: center; gap: 22px; padding-top: 16px; padding-bottom: 16px; }
|
||||||
section { margin: 44px 0; }
|
nav.top .brand { font-weight: 700; letter-spacing: .14em; color: var(--text); text-decoration: none; margin-right: auto; }
|
||||||
h2 { font-size: 26px; margin: 0 0 14px; font-weight: 600; }
|
nav.top a.l { color: var(--text); text-decoration: none; font-size: 15px; opacity: .9; }
|
||||||
.downloads { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 16px; }
|
nav.top a.l:hover { color: var(--ember-hi); }
|
||||||
.dl { background: var(--panel); border: 1px solid var(--line); border-radius: 10px; padding: 20px; }
|
@media (max-width: 620px) { nav.top a.l.opt { display: none; } }
|
||||||
.dl.mine { border-color: var(--ember); }
|
|
||||||
.dl h3 { margin: 0 0 4px; font-size: 20px; }
|
/* Hero */
|
||||||
.dl p { margin: 0 0 14px; color: var(--dim); font-size: 15px; }
|
.hero { position: relative; min-height: min(88vh, 760px); display: flex; align-items: flex-end;
|
||||||
|
background: #0d0a07 url("shots/cove.jpg") center 35% / cover no-repeat; }
|
||||||
|
.hero::before { content: ""; position: absolute; inset: 0;
|
||||||
|
background: linear-gradient(180deg, rgba(13,10,7,.55) 0%, rgba(13,10,7,.15) 35%, rgba(21,17,13,.85) 78%, var(--bg) 100%); }
|
||||||
|
.hero .wrap { position: relative; padding-bottom: 56px; width: 100%; }
|
||||||
|
.pill { display: inline-block; font-size: 14px; padding: 3px 12px; border-radius: 999px;
|
||||||
|
background: rgba(232,131,58,.18); border: 1px solid rgba(246,163,92,.55); color: var(--ember-hi); }
|
||||||
|
.hero h1 { font-size: clamp(52px, 11vw, 112px); line-height: 1; letter-spacing: .07em; margin: 14px 0 8px; font-weight: 700;
|
||||||
|
text-shadow: 0 2px 24px rgba(0,0,0,.55); }
|
||||||
|
.hero .tag { font-size: clamp(19px, 2.6vw, 25px); margin: 0 0 6px; text-shadow: 0 1px 10px rgba(0,0,0,.7); }
|
||||||
|
.hero .sub { color: var(--dim); margin: 0 0 26px; max-width: 640px; text-shadow: 0 1px 8px rgba(0,0,0,.8); }
|
||||||
|
.ctas { display: flex; flex-wrap: wrap; gap: 12px; align-items: center; }
|
||||||
.btn { display: inline-block; background: var(--ember); color: #1a120a; text-decoration: none;
|
.btn { display: inline-block; background: var(--ember); color: #1a120a; text-decoration: none;
|
||||||
font-weight: 600; padding: 11px 20px; border-radius: 8px; }
|
font-weight: 650; padding: 12px 22px; border-radius: 9px; border: 1px solid transparent; }
|
||||||
.btn:hover { background: var(--ember-hi); }
|
.btn:hover { background: var(--ember-hi); }
|
||||||
.note { color: var(--dim); font-size: 14px; margin-top: 12px; }
|
.btn.big { font-size: 19px; padding: 15px 28px; }
|
||||||
.note code { color: var(--text); }
|
.btn.ghost { background: rgba(21,17,13,.55); color: var(--text); border-color: rgba(241,232,218,.35); }
|
||||||
ul.features { columns: 2 260px; padding-left: 20px; margin: 0; }
|
.btn.ghost:hover { border-color: var(--ember-hi); color: var(--ember-hi); background: rgba(21,17,13,.75); }
|
||||||
ul.features li { margin: 0 0 6px; break-inside: avoid; }
|
.hero .small { color: var(--dim); font-size: 14px; margin: 12px 0 0; }
|
||||||
footer { border-top: 1px solid var(--line); padding: 24px 0 40px; color: var(--dim); font-size: 14px; }
|
.hero .small a { color: var(--text); }
|
||||||
@media (prefers-reduced-motion: reduce) { * { scroll-behavior: auto; } }
|
|
||||||
|
section { padding: 56px 0; }
|
||||||
|
section.alt { background: var(--bg2); border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); }
|
||||||
|
h2 { font-size: clamp(26px, 3.4vw, 34px); line-height: 1.2; margin: 0 0 8px; font-weight: 650; }
|
||||||
|
.lead { color: var(--dim); margin: 0 0 26px; max-width: 720px; }
|
||||||
|
|
||||||
|
/* Trailer */
|
||||||
|
.video { border: 1px solid var(--line); border-radius: 12px; overflow: hidden; background: #000; aspect-ratio: 16 / 9;
|
||||||
|
box-shadow: 0 20px 60px rgba(0,0,0,.45); }
|
||||||
|
.video video { width: 100%; height: 100%; display: block; }
|
||||||
|
|
||||||
|
/* Feature cards */
|
||||||
|
.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 18px; }
|
||||||
|
.card { background: var(--panel); border: 1px solid var(--line); border-radius: 12px; overflow: hidden; display: flex; flex-direction: column; }
|
||||||
|
.card img { aspect-ratio: 16 / 9; object-fit: cover; width: 100%; }
|
||||||
|
.card .body { padding: 16px 18px 18px; }
|
||||||
|
.card h3 { margin: 0 0 6px; font-size: 20px; }
|
||||||
|
.card p { margin: 0; color: var(--dim); font-size: 15.5px; }
|
||||||
|
|
||||||
|
/* What's new */
|
||||||
|
.new { display: grid; grid-template-columns: 1.3fr 1fr; gap: 28px; align-items: start; }
|
||||||
|
@media (max-width: 820px) { .new { grid-template-columns: 1fr; } }
|
||||||
|
ul.ticks { list-style: none; padding: 0; margin: 0; }
|
||||||
|
ul.ticks li { padding: 8px 0 8px 30px; position: relative; border-bottom: 1px solid var(--line); }
|
||||||
|
ul.ticks li:last-child { border-bottom: 0; }
|
||||||
|
ul.ticks li::before { content: ""; position: absolute; left: 4px; top: 17px; width: 10px; height: 10px; border-radius: 50%;
|
||||||
|
background: var(--ember); box-shadow: 0 0 10px rgba(232,131,58,.8); }
|
||||||
|
.callout { background: var(--panel); border: 1px solid var(--ember); border-radius: 12px; padding: 20px 22px; }
|
||||||
|
.callout h3 { margin: 0 0 8px; font-size: 20px; }
|
||||||
|
.callout p { margin: 0 0 10px; color: var(--dim); font-size: 15.5px; }
|
||||||
|
.callout p:last-child { margin: 0; }
|
||||||
|
kbd { font: 600 13px/1 ui-monospace, SFMono-Regular, Menlo, monospace; background: #2f251b; border: 1px solid #4a3b2c;
|
||||||
|
border-bottom-width: 2px; border-radius: 5px; padding: 3px 6px; color: var(--text); }
|
||||||
|
|
||||||
|
/* Gallery */
|
||||||
|
.gallery { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 14px; margin: 0; }
|
||||||
|
.gallery figure { margin: 0; position: relative; border-radius: 10px; overflow: hidden; border: 1px solid var(--line); background: #000; }
|
||||||
|
.gallery a { display: block; }
|
||||||
|
.gallery img { aspect-ratio: 16 / 9; object-fit: cover; width: 100%; transition: transform .35s ease; }
|
||||||
|
.gallery a:hover img { transform: scale(1.03); }
|
||||||
|
.gallery figcaption { position: absolute; left: 0; right: 0; bottom: 0; padding: 22px 14px 10px; font-size: 14.5px;
|
||||||
|
background: linear-gradient(transparent, rgba(0,0,0,.78)); pointer-events: none; }
|
||||||
|
|
||||||
|
/* Downloads */
|
||||||
|
.downloads { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 18px; }
|
||||||
|
.dl { background: var(--panel); border: 1px solid var(--line); border-radius: 12px; padding: 22px; }
|
||||||
|
.dl.mine { border-color: var(--ember); box-shadow: 0 0 0 1px var(--ember) inset; }
|
||||||
|
.dl h3 { margin: 0 0 2px; font-size: 21px; }
|
||||||
|
.dl .meta { margin: 0 0 16px; color: var(--dim); font-size: 15px; }
|
||||||
|
.dl .yours { display: none; font-size: 13px; color: var(--ember-hi); margin-left: 8px; font-weight: 600; }
|
||||||
|
.dl.mine .yours { display: inline; }
|
||||||
|
.note { color: var(--dim); font-size: 14.5px; margin: 14px 0 0; }
|
||||||
|
.note code, td code, p code { color: var(--text); background: #2a2119; padding: 1px 6px; border-radius: 5px; font-size: .92em; }
|
||||||
|
|
||||||
|
/* Friends */
|
||||||
|
.steps { counter-reset: s; display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 18px; margin: 0 0 22px; padding: 0; list-style: none; }
|
||||||
|
.steps li { background: var(--panel); border: 1px solid var(--line); border-radius: 12px; padding: 18px 18px 18px 60px; position: relative; color: var(--dim); font-size: 15.5px; }
|
||||||
|
.steps li b { color: var(--text); }
|
||||||
|
.steps li::before { counter-increment: s; content: counter(s); position: absolute; left: 18px; top: 16px; width: 28px; height: 28px;
|
||||||
|
border-radius: 50%; background: var(--ember); color: #1a120a; font-weight: 700; display: grid; place-items: center; font-size: 15px; }
|
||||||
|
table { border-collapse: collapse; width: 100%; max-width: 640px; font-size: 15.5px; }
|
||||||
|
th, td { text-align: left; padding: 9px 12px; border-bottom: 1px solid var(--line); }
|
||||||
|
th { color: var(--dim); font-weight: 600; }
|
||||||
|
|
||||||
|
footer { border-top: 1px solid var(--line); padding: 28px 0 44px; color: var(--dim); font-size: 14.5px; }
|
||||||
|
footer .wrap { display: flex; flex-wrap: wrap; gap: 10px 26px; justify-content: space-between; }
|
||||||
|
footer nav a { margin-right: 18px; }
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
html { scroll-behavior: auto; }
|
||||||
|
.gallery img { transition: none; }
|
||||||
|
.gallery a:hover img { transform: none; }
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header class="wrap">
|
<a class="skip" href="#main">Skip to the page</a>
|
||||||
<h1>EMBERWILD</h1>
|
|
||||||
<p class="tag">Survive the island. Build a home.</p>
|
<nav class="top" aria-label="Site">
|
||||||
<p class="guide-link"><a href="guide.html">Read the guide</a>: how to play, and everything in the game.</p>
|
<div class="wrap">
|
||||||
<div class="video">
|
<a class="brand" href="#">CINDERWILD</a>
|
||||||
<video controls preload="metadata" poster="poster.jpg" aria-label="Emberwild trailer">
|
<a class="l opt" href="#trailer">Trailer</a>
|
||||||
<source src="trailer.mp4" type="video/mp4">
|
<a class="l opt" href="#world">The game</a>
|
||||||
Your browser can't play this video. <a href="trailer.mp4">Download the trailer</a>.
|
<a class="l" href="guide.html">Guide</a>
|
||||||
</video>
|
<a class="l" href="#download">Download</a>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<header class="hero">
|
||||||
|
<div class="wrap">
|
||||||
|
<span class="pill">Version {{VERSION}} · playtest</span>
|
||||||
|
<h1>CINDERWILD</h1>
|
||||||
|
<p class="tag">Survive the island. Build a home. Find what lies beyond.</p>
|
||||||
|
<p class="sub">A low-poly survival game for Mac and Windows. Gather, hunt and build, hold out
|
||||||
|
through wolf moons and hard winters, then dig down through the sea cliffs to the Cove.
|
||||||
|
Alone, or with friends on a server you run yourself.</p>
|
||||||
|
<div class="ctas">
|
||||||
|
<a class="btn big" id="hero-dl" href="#download">Download free</a>
|
||||||
|
<a class="btn ghost" href="#trailer">Watch the trailer</a>
|
||||||
|
<a class="btn ghost" href="guide.html">Read the guide</a>
|
||||||
|
</div>
|
||||||
|
<p class="small" id="hero-other">For macOS (Apple silicon and Intel) and Windows (64-bit).</p>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main class="wrap">
|
<main id="main">
|
||||||
<section aria-labelledby="dl-title">
|
<section id="trailer" aria-labelledby="trailer-title">
|
||||||
<h2 id="dl-title">Download version {{VERSION}}</h2>
|
<div class="wrap">
|
||||||
<div class="downloads">
|
<h2 id="trailer-title">The trailer</h2>
|
||||||
<div class="dl" data-os="mac">
|
<p class="lead">Under a minute, recorded in the game.</p>
|
||||||
<h3>macOS</h3>
|
<div class="video">
|
||||||
<p>Apple silicon and Intel · {{MAC_SIZE}}</p>
|
<video controls preload="metadata" poster="poster.jpg" aria-label="Cinderwild trailer">
|
||||||
<a class="btn" href="{{MAC_URL}}">Download for Mac</a>
|
<source src="trailer.mp4" type="video/mp4">
|
||||||
<p class="note">First launch: right-click the app and choose <b>Open</b>
|
Your browser can't play this video. <a href="trailer.mp4">Download the trailer</a>.
|
||||||
(it isn't notarised by Apple yet).</p>
|
</video>
|
||||||
</div>
|
|
||||||
<div class="dl" data-os="win">
|
|
||||||
<h3>Windows</h3>
|
|
||||||
<p>64-bit · {{WIN_SIZE}}</p>
|
|
||||||
<a class="btn" href="{{WIN_URL}}">Download for Windows</a>
|
|
||||||
<p class="note">Unzip, then run <code>Emberwild.exe</code>. If SmartScreen
|
|
||||||
warns, choose <b>More info</b> > <b>Run anyway</b> (it isn't signed yet).</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="note">Released {{DATE}}. {{NOTES}} The game tells you on its title
|
|
||||||
screen when a newer version is out.
|
|
||||||
<a href="{{RELEASES_URL}}">All releases and checksums</a>.</p>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section aria-labelledby="what-title">
|
<section id="world" class="alt" aria-labelledby="world-title">
|
||||||
<h2 id="what-title">What it is</h2>
|
<div class="wrap">
|
||||||
<ul class="features">
|
<h2 id="world-title">What you'll do</h2>
|
||||||
<li>Gather, craft and cook; keep warm, fed and dry.</li>
|
<p class="lead">Every tree, animal and hut is built from low-poly shapes in the game's
|
||||||
<li>Hunt deer, boar and rabbits; fish the lake.</li>
|
own code, under a lot of weather.</p>
|
||||||
<li>Build a home from floors, walls and roofs that snap together.</li>
|
<div class="cards">
|
||||||
<li>Wolves move in, and every seventh night the moon turns red.</li>
|
<article class="card">
|
||||||
<li>Weather that matters: storms, blizzards and hard winters.</li>
|
<img src="shots/meadow.jpg" alt="A sunny meadow with deer among the trees" width="1280" height="720" loading="lazy">
|
||||||
<li>A bear, a barrow, and the Old Stag at the standing stones.</li>
|
<div class="body"><h3>Survive</h3>
|
||||||
<li>Dig down through the sea cliffs to the Cove, and fish off its pier.</li>
|
<p>Gather, craft and cook. Keep warm, fed and dry, and eat a mix of meat, fish and
|
||||||
<li>A wreck on the Cove's beach, and a pirate treasure for each player to dig up.</li>
|
greens. Food goes off; salt and smoke keep it.</p></div>
|
||||||
<li>On a server, friends can be in different lands at once.</li>
|
</article>
|
||||||
<li>A map that fills in as you explore, and a compass.</li>
|
<article class="card">
|
||||||
<li>Play alone, or host a server for friends on your own machine.</li>
|
<img src="shots/hut.jpg" alt="A wooden hut with a campfire at dusk" width="1280" height="720" loading="lazy">
|
||||||
<li>A guided tutorial with Old Tam the hermit.</li>
|
<div class="body"><h3>Build</h3>
|
||||||
</ul>
|
<p>Floors, walls and roofs that snap together, in wood and then stone. A roof, a
|
||||||
|
fire and a bed make a home you'll be glad of at night.</p></div>
|
||||||
|
</article>
|
||||||
|
<article class="card">
|
||||||
|
<img src="shots/wolves.jpg" alt="Wolves circling a campfire under a red moon" width="1280" height="720" loading="lazy">
|
||||||
|
<div class="body"><h3>Hold out</h3>
|
||||||
|
<p>Wolves move in, and every seventh night the moon turns red. There's a bear in
|
||||||
|
the deep woods, a barrow, and the Old Stag at the standing stones.</p></div>
|
||||||
|
</article>
|
||||||
|
<article class="card">
|
||||||
|
<img src="shots/treasure.jpg" alt="A marker stone in the Cove's hills at dusk, with a drowned sailor among the trees" width="1280" height="720" loading="lazy">
|
||||||
|
<div class="body"><h3>Explore</h3>
|
||||||
|
<p>Dig through the sea cliffs to the Cove: a sandy bay, a pier for sea fishing, a
|
||||||
|
wreck, and a pirate treasure for each player, guarded by the drowned crew.</p></div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section aria-labelledby="host-title">
|
<section id="new" aria-labelledby="new-title">
|
||||||
<h2 id="host-title">Playing with friends</h2>
|
<div class="wrap">
|
||||||
<p>Choose <b>Host Game</b> on the title screen, and friends choose <b>Join Game</b>
|
<h2 id="new-title">New in {{VERSION}}</h2>
|
||||||
with your address (UDP port 24242). Everyone needs the same version. On the same
|
<p class="lead">The Cove gets its own. This is a playtest build: thanks for trying it.</p>
|
||||||
network, or over Tailscale, that's all. For friends over the internet, forward
|
<div class="new">
|
||||||
<b>UDP 24242 and 24243</b> on your router: the Cove runs as its own server on the
|
<ul class="ticks">
|
||||||
next port, started when someone first goes there. A spare computer can run a
|
<li>A new name: Emberwild is now Cinderwild (another game already had the old one). Your worlds come with you.</li>
|
||||||
dedicated server from the same download:
|
<li>A wreck on the Cove's beach, with a treasure map in the captain's sea chest for each player.</li>
|
||||||
<code>Emberwild --headless -- --server</code>.
|
<li>Your own X on your map: dig up the pirate chest for a ship's compass, a spyglass, a cutlass and gold.</li>
|
||||||
<a href="guide.html#multiplayer">More in the guide</a>.</p>
|
<li>The drowned crew rise to guard it. Dig by day if you're wise.</li>
|
||||||
|
<li>Crabs on the sand from dusk till dawn, and gulls after any food you leave out.</li>
|
||||||
|
<li>Build a rowing boat from driftwood, seagrass rope and pitch, and tie it up at the pier.</li>
|
||||||
|
<li>Report a bug, straight from the pause menu.</li>
|
||||||
|
</ul>
|
||||||
|
<aside class="callout" aria-labelledby="bug-title">
|
||||||
|
<h3 id="bug-title">Found a bug?</h3>
|
||||||
|
<p>Press <kbd>Esc</kbd> in the game, then <b>Report a bug</b>. It opens a new issue
|
||||||
|
on <a href="{{REPO_URL}}/issues">our Gitea</a>, already filled in with your
|
||||||
|
version, computer and where you are. Say what happened and press Create Issue.</p>
|
||||||
|
<p>You'll need a free account there. <a href="{{REPO_URL}}/src/branch/master/docs/PLAYTEST.md">What to try, and what isn't a bug yet</a>.</p>
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="alt" aria-labelledby="gallery-title">
|
||||||
|
<div class="wrap">
|
||||||
|
<h2 id="gallery-title">Screenshots</h2>
|
||||||
|
<p class="lead">Straight from the game. Click one to see it full size.</p>
|
||||||
|
<div class="gallery">
|
||||||
|
<figure><a href="shots/cove.jpg"><img src="shots/cove.jpg" alt="The Cove's bay and pier on a clear morning, a rowing boat tied up" width="1280" height="720" loading="lazy"></a>
|
||||||
|
<figcaption>The Cove, and a boat at the pier</figcaption></figure>
|
||||||
|
<figure><a href="shots/crabs.jpg"><img src="shots/crabs.jpg" alt="Red crabs on the sand at dusk" width="1280" height="720" loading="lazy"></a>
|
||||||
|
<figcaption>Crabs come up the beach at dusk</figcaption></figure>
|
||||||
|
<figure><a href="shots/winter.jpg"><img src="shots/winter.jpg" alt="A hut in a snowy clearing in a blizzard" width="1280" height="720" loading="lazy"></a>
|
||||||
|
<figcaption>Weather the winter</figcaption></figure>
|
||||||
|
<figure><a href="shots/hut.jpg"><img src="shots/hut.jpg" alt="A wooden hut with a campfire at dusk" width="1280" height="720" loading="lazy"></a>
|
||||||
|
<figcaption>A home of your own</figcaption></figure>
|
||||||
|
<figure><a href="shots/wolves.jpg"><img src="shots/wolves.jpg" alt="Wolves circling a campfire under a red moon" width="1280" height="720" loading="lazy"></a>
|
||||||
|
<figcaption>The wolf moon</figcaption></figure>
|
||||||
|
<figure><a href="shots/treasure.jpg"><img src="shots/treasure.jpg" alt="A marker stone and a mound of loose earth in the hills at dusk" width="1280" height="720" loading="lazy"></a>
|
||||||
|
<figcaption>X marks the spot</figcaption></figure>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="download" aria-labelledby="dl-title">
|
||||||
|
<div class="wrap">
|
||||||
|
<h2 id="dl-title">Download version {{VERSION}}</h2>
|
||||||
|
<p class="lead">Free. Released {{DATE}}. The game tells you on its title screen when a newer version is out.</p>
|
||||||
|
<div class="downloads">
|
||||||
|
<div class="dl" data-os="mac">
|
||||||
|
<h3>macOS <span class="yours">· your computer</span></h3>
|
||||||
|
<p class="meta">Apple silicon and Intel · {{MAC_SIZE}}</p>
|
||||||
|
<a class="btn" href="{{MAC_URL}}">Download for Mac</a>
|
||||||
|
<p class="note">Unzip it. The first time, right-click the app and choose <b>Open</b>
|
||||||
|
(it isn't notarised by Apple yet).</p>
|
||||||
|
</div>
|
||||||
|
<div class="dl" data-os="win">
|
||||||
|
<h3>Windows <span class="yours">· your computer</span></h3>
|
||||||
|
<p class="meta">64-bit · {{WIN_SIZE}}</p>
|
||||||
|
<a class="btn" href="{{WIN_URL}}">Download for Windows</a>
|
||||||
|
<p class="note">Unzip, then run <code>Cinderwild.exe</code>. If SmartScreen warns,
|
||||||
|
choose <b>More info</b> > <b>Run anyway</b> (it isn't signed yet).</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="note"><a href="{{RELEASES_URL}}">All releases, release notes and checksums</a>.</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="friends" class="alt" aria-labelledby="host-title">
|
||||||
|
<div class="wrap">
|
||||||
|
<h2 id="host-title">Playing with friends</h2>
|
||||||
|
<p class="lead">No accounts and no company servers: one of you hosts, on your own computer.</p>
|
||||||
|
<ol class="steps">
|
||||||
|
<li><b>Host Game</b> on the title screen. Pick a name, a rules preset (co-op, rivals
|
||||||
|
or adversarial) and a password if you like.</li>
|
||||||
|
<li>Friends choose <b>Join Game</b> and type your address. Everyone needs the same
|
||||||
|
version.</li>
|
||||||
|
<li>On the same network, or over Tailscale, that's it. Over the internet, forward the
|
||||||
|
ports below on your router.</li>
|
||||||
|
</ol>
|
||||||
|
<table>
|
||||||
|
<caption class="note" style="caption-side: bottom; text-align: left;">The Cove runs as its own server on the next port, started when someone first goes there.</caption>
|
||||||
|
<thead><tr><th scope="col">UDP port</th><th scope="col">For</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td><code>24242</code></td><td>The Valley (the server you join)</td></tr>
|
||||||
|
<tr><td><code>24243</code></td><td>The Cove</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<p class="note">A spare computer can run a dedicated server from the same download:
|
||||||
|
<code>Cinderwild --headless -- --server</code>.
|
||||||
|
<a href="guide.html#multiplayer">More in the guide</a>.</p>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer class="wrap">Emberwild {{VERSION}}. Made with Godot. <a href="guide.html">The guide</a>.</footer>
|
<footer>
|
||||||
|
<div class="wrap">
|
||||||
|
<nav aria-label="More">
|
||||||
|
<a href="guide.html">The guide</a>
|
||||||
|
<a href="{{RELEASES_URL}}">Releases</a>
|
||||||
|
<a href="{{REPO_URL}}/issues">Report a bug</a>
|
||||||
|
<a href="{{REPO_URL}}">Source</a>
|
||||||
|
</nav>
|
||||||
|
<span>Cinderwild {{VERSION}}. Made with Godot.</span>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Point out the download for the visitor's own computer (it still works without this).
|
// The visitor's own computer: its download first and marked, and the
|
||||||
|
// hero's button goes straight to it. (Without this, the page still works.)
|
||||||
(function () {
|
(function () {
|
||||||
var ua = navigator.userAgent;
|
var ua = navigator.userAgent;
|
||||||
var os = /Mac/i.test(ua) && !/iPhone|iPad/i.test(ua) ? "mac" : /Win/i.test(ua) ? "win" : "";
|
var os = /iPhone|iPad|Android/i.test(ua) ? "" : /Mac/i.test(ua) ? "mac" : /Win/i.test(ua) ? "win" : "";
|
||||||
var card = os && document.querySelector('.dl[data-os="' + os + '"]');
|
if (!os) return;
|
||||||
if (card) { card.classList.add("mine"); card.parentNode.insertBefore(card, card.parentNode.firstChild); }
|
var card = document.querySelector('.dl[data-os="' + os + '"]');
|
||||||
|
if (!card) return;
|
||||||
|
card.classList.add("mine");
|
||||||
|
card.parentNode.insertBefore(card, card.parentNode.firstChild);
|
||||||
|
var link = card.querySelector("a.btn");
|
||||||
|
var hero = document.getElementById("hero-dl");
|
||||||
|
hero.href = link.href;
|
||||||
|
hero.textContent = os === "mac" ? "Download for Mac" : "Download for Windows";
|
||||||
|
var other = document.getElementById("hero-other");
|
||||||
|
other.innerHTML = "Free · {{VERSION}} · " + card.querySelector(".meta").textContent.split("·").pop().trim() +
|
||||||
|
' · <a href="#download">' + (os === "mac" ? "Windows" : "Mac") + " and install notes</a>";
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -221,8 +221,8 @@ PAGE = """<!doctype html>
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>Emberwild guide: everything in the game</title>
|
<title>Cinderwild guide: everything in the game</title>
|
||||||
<meta name="description" content="How to play Emberwild: controls, survival, food, building, weather, wildlife, hunting, fishing, iron, the Old Stag, the journey to the Cove and multiplayer, with every item, recipe and creature.">
|
<meta name="description" content="How to play Cinderwild: controls, survival, food, building, weather, wildlife, hunting, fishing, iron, the Old Stag, the journey to the Cove and multiplayer, with every item, recipe and creature.">
|
||||||
<style>
|
<style>
|
||||||
:root { --bg: #15110d; --panel: #221b14; --line: #3a2e22; --text: #f1e8da; --dim: #b8a892;
|
:root { --bg: #15110d; --panel: #221b14; --line: #3a2e22; --text: #f1e8da; --dim: #b8a892;
|
||||||
--ember: #e8833a; --ember-hi: #f6a35c; --mark: #5a3a14; }
|
--ember: #e8833a; --ember-hi: #f6a35c; --mark: #5a3a14; }
|
||||||
@@ -262,7 +262,7 @@ PAGE = """<!doctype html>
|
|||||||
<a class="skip" href="#content">Skip to the guide</a>
|
<a class="skip" href="#content">Skip to the guide</a>
|
||||||
<div class="layout">
|
<div class="layout">
|
||||||
<nav class="toc" aria-label="Guide contents">
|
<nav class="toc" aria-label="Guide contents">
|
||||||
<a class="home" href="index.html">EMBERWILD</a>
|
<a class="home" href="index.html">CINDERWILD</a>
|
||||||
<label for="q" class="group" style="display:block;margin:0 0 6px">Search the guide</label>
|
<label for="q" class="group" style="display:block;margin:0 0 6px">Search the guide</label>
|
||||||
<input id="q" type="search" placeholder="wolves, bed, iron..." autocomplete="off">
|
<input id="q" type="search" placeholder="wolves, bed, iron..." autocomplete="off">
|
||||||
<ol>
|
<ol>
|
||||||
@@ -273,12 +273,12 @@ PAGE = """<!doctype html>
|
|||||||
</ol>
|
</ol>
|
||||||
</nav>
|
</nav>
|
||||||
<main id="content">
|
<main id="content">
|
||||||
<h1>The Emberwild guide</h1>
|
<h1>The Cinderwild guide</h1>
|
||||||
<p class="lead">Everything in Emberwild {VERSION}: how to play, what can happen, and every item, recipe and
|
<p class="lead">Everything in Cinderwild {VERSION}: how to play, what can happen, and every item, recipe and
|
||||||
creature. <a href="index.html">Download the game</a>.</p>
|
creature. <a href="index.html">Download the game</a>.</p>
|
||||||
<p id="none" class="none hidden" role="status">Nothing matches that.</p>
|
<p id="none" class="none hidden" role="status">Nothing matches that.</p>
|
||||||
{SECTIONS}
|
{SECTIONS}
|
||||||
<footer>Emberwild {VERSION}. The reference tables are made from the game's own data each time this page is built.</footer>
|
<footer>Cinderwild {VERSION}. The reference tables are made from the game's own data each time this page is built.</footer>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
16
tools/site/make_shots.sh
Executable file
16
tools/site/make_shots.sh
Executable file
@@ -0,0 +1,16 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# The download page's screenshots (tools/site/shots.gd), rendered in a window:
|
||||||
|
#
|
||||||
|
# tools/site/make_shots.sh # -> build/shots/*.jpg (make_site.sh copies them)
|
||||||
|
#
|
||||||
|
# Takes under a minute. Run it again when the game's look changes.
|
||||||
|
|
||||||
|
cd "$(dirname "$0")/../.." || exit 1
|
||||||
|
. tools/godot_env.sh
|
||||||
|
mkdir -p build/shots
|
||||||
|
rm -f build/shots/*.jpg
|
||||||
|
"$GODOT" --path . --resolution 1280x720 --script tools/site/shots.gd -- "$PWD/build/shots" > build/shots/render.log 2>&1
|
||||||
|
if grep -q "SCRIPT ERROR" build/shots/render.log || [ -z "$(ls build/shots/*.jpg 2>/dev/null)" ]; then
|
||||||
|
echo "rendering the screenshots failed: see build/shots/render.log"; exit 1
|
||||||
|
fi
|
||||||
|
grep "^shots:" build/shots/render.log
|
||||||
@@ -7,22 +7,31 @@
|
|||||||
# build/site/index.html the page, with this release's download links
|
# build/site/index.html the page, with this release's download links
|
||||||
# build/site/poster.jpg the trailer's title frame
|
# build/site/poster.jpg the trailer's title frame
|
||||||
# build/site/trailer.mp4 the trailer (tools/trailer/make_trailer.sh)
|
# build/site/trailer.mp4 the trailer (tools/trailer/make_trailer.sh)
|
||||||
|
# build/site/shots/ the screenshots (tools/site/make_shots.sh)
|
||||||
#
|
#
|
||||||
# Copy the folder to any static web host. Gitea itself won't serve HTML, so
|
# Copy the folder to any static web host. Gitea itself won't serve HTML, so
|
||||||
# the page lives elsewhere and links to the files on the Gitea release.
|
# the page lives elsewhere and links to the files on the Gitea release.
|
||||||
# Rebuild it after each release (the links name the version).
|
# Rebuild it after each release (the links name the version).
|
||||||
|
|
||||||
cd "$(dirname "$0")/../.." || exit 1
|
cd "$(dirname "$0")/../.." || exit 1
|
||||||
REPO=${REPO:-https://git.psbailey.uk/Admin/Emberwild}
|
REPO=${REPO:-https://git.psbailey.uk/Admin/Cinderwild}
|
||||||
|
SITE=${SITE:-https://cinderwild.psbailey.uk} # (where the page is served: for link previews)
|
||||||
|
SHOTS="meadow hut wolves winter cove treasure crabs"
|
||||||
[ -f build/version.json ] || { echo "no build/version.json: run tools/export.sh first"; exit 1; }
|
[ -f build/version.json ] || { echo "no build/version.json: run tools/export.sh first"; exit 1; }
|
||||||
[ -f build/Emberwild-trailer.mp4 ] || { echo "no trailer: run tools/trailer/make_trailer.sh first"; exit 1; }
|
[ -f build/Cinderwild-trailer.mp4 ] || { echo "no trailer: run tools/trailer/make_trailer.sh first"; exit 1; }
|
||||||
|
for s in $SHOTS; do
|
||||||
|
[ -f "build/shots/$s.jpg" ] || { echo "no build/shots/$s.jpg: run tools/site/make_shots.sh first"; exit 1; }
|
||||||
|
done
|
||||||
|
|
||||||
rm -rf build/site && mkdir -p build/site
|
rm -rf build/site && mkdir -p build/site
|
||||||
cp build/Emberwild-trailer.mp4 build/site/trailer.mp4
|
cp build/Cinderwild-trailer.mp4 build/site/trailer.mp4
|
||||||
cp build/trailer/poster.jpg build/site/poster.jpg
|
cp build/trailer/poster.jpg build/site/poster.jpg
|
||||||
python3 - "$REPO" <<'EOF'
|
mkdir -p build/site/shots
|
||||||
|
for s in $SHOTS; do cp "build/shots/$s.jpg" build/site/shots/; done
|
||||||
|
python3 - "$REPO" "$SITE" <<'EOF'
|
||||||
import html, json, sys
|
import html, json, sys
|
||||||
repo = sys.argv[1].rstrip("/")
|
repo = sys.argv[1].rstrip("/")
|
||||||
|
site = sys.argv[2].rstrip("/")
|
||||||
v = json.load(open("build/version.json"))
|
v = json.load(open("build/version.json"))
|
||||||
tag = "v" + v["version"]
|
tag = "v" + v["version"]
|
||||||
def dl(plat):
|
def dl(plat):
|
||||||
@@ -36,7 +45,7 @@ page = open("tools/site/index.html").read()
|
|||||||
for k, val in {
|
for k, val in {
|
||||||
"VERSION": v["version"], "DATE": v.get("date", ""), "NOTES": v.get("notes", ""),
|
"VERSION": v["version"], "DATE": v.get("date", ""), "NOTES": v.get("notes", ""),
|
||||||
"MAC_URL": mac_url, "MAC_SIZE": mac_size, "WIN_URL": win_url, "WIN_SIZE": win_size,
|
"MAC_URL": mac_url, "MAC_SIZE": mac_size, "WIN_URL": win_url, "WIN_SIZE": win_size,
|
||||||
"RELEASES_URL": repo + "/releases",
|
"RELEASES_URL": repo + "/releases", "REPO_URL": repo, "SITE_URL": site,
|
||||||
}.items():
|
}.items():
|
||||||
page = page.replace("{{%s}}" % k, html.escape(str(val), quote=True))
|
page = page.replace("{{%s}}" % k, html.escape(str(val), quote=True))
|
||||||
assert "{{" not in page, "a placeholder was left unfilled"
|
assert "{{" not in page, "a placeholder was left unfilled"
|
||||||
@@ -55,7 +64,8 @@ rm -f "$DATA"
|
|||||||
|
|
||||||
# Cloudflare Pages refuses any file over 25 MiB: catch it here, not at deploy.
|
# Cloudflare Pages refuses any file over 25 MiB: catch it here, not at deploy.
|
||||||
LIMIT=26214400
|
LIMIT=26214400
|
||||||
for f in build/site/*; do
|
for f in build/site/* build/site/shots/*; do
|
||||||
|
[ -f "$f" ] || continue
|
||||||
s=$(stat -f %z "$f" 2>/dev/null || stat -c %s "$f")
|
s=$(stat -f %z "$f" 2>/dev/null || stat -c %s "$f")
|
||||||
if [ "$s" -ge "$LIMIT" ]; then
|
if [ "$s" -ge "$LIMIT" ]; then
|
||||||
echo "$f is $s bytes: over Cloudflare Pages' 25 MiB limit (re-encode the trailer smaller)"; exit 1
|
echo "$f is $s bytes: over Cloudflare Pages' 25 MiB limit (re-encode the trailer smaller)"; exit 1
|
||||||
|
|||||||
126
tools/site/shots.gd
Normal file
126
tools/site/shots.gd
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
extends "res://tools/trailer/trailer.gd"
|
||||||
|
## The download page's screenshots (tools/site/make_shots.sh): the trailer's
|
||||||
|
## shots, each caught as one still instead of filmed, plus a couple of the
|
||||||
|
## Cove's own (the treasure dig and its drowned crew, crabs on the sand).
|
||||||
|
## godot --path . --resolution 1280x720 --script tools/site/shots.gd -- <out dir>
|
||||||
|
## Writes <name>.jpg for each, 1280x720.
|
||||||
|
|
||||||
|
## How far into each trailer shot the still is taken (0..1).
|
||||||
|
const AT := {"meadow": 0.3, "hut": 0.8, "wolves": 0.15, "winter": 0.5, "cove": 0.85}
|
||||||
|
|
||||||
|
var _out := ""
|
||||||
|
var _name := ""
|
||||||
|
var made: Array[String] = []
|
||||||
|
|
||||||
|
func _run() -> void:
|
||||||
|
var args := OS.get_cmdline_user_args()
|
||||||
|
_out = args[0] if not args.is_empty() else ProjectSettings.globalize_path("res://build/shots")
|
||||||
|
DirAccess.make_dir_recursive_absolute(_out)
|
||||||
|
var H: Node = root.get_node("Hints")
|
||||||
|
H.path = "user://shots_hints.json"
|
||||||
|
H.load_prefs()
|
||||||
|
H.enabled = false
|
||||||
|
var S: Node = root.get_node("Saves")
|
||||||
|
S.root_dir = "user://shots_worlds"
|
||||||
|
_overlay()
|
||||||
|
await _enter_world("")
|
||||||
|
_calm()
|
||||||
|
await _stage()
|
||||||
|
for s in [["meadow", _shot_meadow], ["hut", _shot_hut], ["wolves", _shot_wolves], ["winter", _shot_blizzard]]:
|
||||||
|
_name = s[0]
|
||||||
|
await (s[1] as Callable).call()
|
||||||
|
await _enter_world("cove")
|
||||||
|
_calm()
|
||||||
|
_name = "cove"
|
||||||
|
await _shot_cove()
|
||||||
|
_name = "treasure"
|
||||||
|
await _shot_treasure()
|
||||||
|
_name = "crabs"
|
||||||
|
await _shot_crabs()
|
||||||
|
for w in S.list_worlds():
|
||||||
|
S.delete_world(w.id)
|
||||||
|
DirAccess.remove_absolute(S.root_dir)
|
||||||
|
DirAccess.remove_absolute(H.path)
|
||||||
|
print("shots: ", ", ".join(made))
|
||||||
|
quit(0)
|
||||||
|
|
||||||
|
## No wildlife wandering into frame, nothing coming for food left out.
|
||||||
|
func _calm() -> void:
|
||||||
|
world.creatures.enabled = false
|
||||||
|
for c in world.creatures.get_children():
|
||||||
|
c.queue_free()
|
||||||
|
if world.thieves != null:
|
||||||
|
world.thieves.enabled = false
|
||||||
|
if world.shore != null:
|
||||||
|
world.shore.enabled = false
|
||||||
|
|
||||||
|
## One still, part-way through the shot `f` would film; no captions, no fades.
|
||||||
|
func _shot(secs: float, f: Callable, _caption: String = "", _fade: float = 0.5) -> void:
|
||||||
|
var t := secs * float(AT.get(_name, 0.6))
|
||||||
|
for _i in range(4):
|
||||||
|
f.call(t, t / secs)
|
||||||
|
_bare()
|
||||||
|
await process_frame
|
||||||
|
world.props._update_grass(100000)
|
||||||
|
var wait := 1.2 if _name == "winter" else 0.6 # (let the snow fill the air)
|
||||||
|
var left := wait
|
||||||
|
while left > 0.0:
|
||||||
|
f.call(t, t / secs)
|
||||||
|
_bare()
|
||||||
|
await process_frame
|
||||||
|
left -= 1.0 / 60.0
|
||||||
|
await RenderingServer.frame_post_draw
|
||||||
|
var img := root.get_viewport().get_texture().get_image()
|
||||||
|
var path := _out.path_join(_name + ".jpg")
|
||||||
|
img.save_jpg(path, 0.84)
|
||||||
|
made.append(_name)
|
||||||
|
|
||||||
|
func _bare() -> void:
|
||||||
|
for l in [big, sub, cap]:
|
||||||
|
l.modulate.a = 0.0
|
||||||
|
black.modulate.a = 0.0
|
||||||
|
|
||||||
|
## The Cove's treasure at dusk: your mound by its marker stone, and the
|
||||||
|
## drowned crew risen round it.
|
||||||
|
func _shot_treasure() -> void:
|
||||||
|
var p: Node3D = world.player
|
||||||
|
p.inventory.add("treasure_map", 1)
|
||||||
|
await create_timer(1.0).timeout # (the mound shows once the map's in your bag)
|
||||||
|
var tr: Node3D = world.treasure
|
||||||
|
var m: Node3D = tr.mounds.get(world.my_id()) if tr != null else null
|
||||||
|
if m == null:
|
||||||
|
return
|
||||||
|
tr.force_crew = true
|
||||||
|
tr._raise_crew(world.my_id(), m.global_position)
|
||||||
|
var eye: Vector3 = _g(m.global_position + Vector3(3.6, 0, 2.8), 1.6)
|
||||||
|
for g in tr.crews[world.my_id()].crew:
|
||||||
|
g.set_physics_process(false)
|
||||||
|
var to: Vector3 = eye - g.global_position
|
||||||
|
g.rotation.y = atan2(-to.x, -to.z)
|
||||||
|
await _shot(1.0, func(_t: float, _k: float):
|
||||||
|
_clock(18.1)
|
||||||
|
cam.global_position = eye
|
||||||
|
cam.look_at(m.global_position + Vector3(0, 0.6, 0))
|
||||||
|
_park_player(m.global_position + Vector3(1.2, 0, 0)))
|
||||||
|
|
||||||
|
## Crabs on the sand at dusk, the pier behind.
|
||||||
|
func _shot_crabs() -> void:
|
||||||
|
var b := _g(Vector3(4.0, 0, 77.0))
|
||||||
|
var offs := [Vector3(0, 0, 0), Vector3(1.2, 0, 0.6), Vector3(-0.9, 0, 1.1), Vector3(2.2, 0, -0.5)]
|
||||||
|
for i in range(offs.size()):
|
||||||
|
var c: Node3D = world.shore.spawn(b + offs[i]) if world.shore != null else null
|
||||||
|
if c == null:
|
||||||
|
continue
|
||||||
|
c.set_physics_process(false)
|
||||||
|
c.yaw = 0.6 * i
|
||||||
|
c.rotation.y = c.yaw
|
||||||
|
if i == 0:
|
||||||
|
c.state = "charge"
|
||||||
|
c.speed = 1.2
|
||||||
|
c._animate(0.1)
|
||||||
|
var eye := _g(b + Vector3(-1.0, 0, 2.4), 1.7) # (from down the sand, looking back up the beach)
|
||||||
|
await _shot(1.0, func(_t: float, _k: float):
|
||||||
|
_clock(18.4)
|
||||||
|
cam.global_position = eye
|
||||||
|
cam.look_at(b + Vector3(0.5, 0.0, 0.2))
|
||||||
|
_park_player(eye))
|
||||||
1
tools/site/shots.gd.uid
Normal file
1
tools/site/shots.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://bll0xov1lash4
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
# Records the test trailer (tools/trailer/trailer.gd) with Godot's Movie
|
# Records the test trailer (tools/trailer/trailer.gd) with Godot's Movie
|
||||||
# Maker and encodes it, with a generated music pad under the game's sound:
|
# Maker and encodes it, with a generated music pad under the game's sound:
|
||||||
#
|
#
|
||||||
# tools/trailer/make_trailer.sh # -> build/Emberwild-trailer.mp4 (+ poster.jpg)
|
# tools/trailer/make_trailer.sh # -> build/Cinderwild-trailer.mp4 (+ poster.jpg)
|
||||||
#
|
#
|
||||||
# Needs ffmpeg. Takes about a minute: the shots are rendered frame by frame
|
# Needs ffmpeg. Takes about a minute: the shots are rendered frame by frame
|
||||||
# at 30 fps whatever the machine's speed, so the result never stutters.
|
# at 30 fps whatever the machine's speed, so the result never stutters.
|
||||||
@@ -13,7 +13,7 @@ command -v ffmpeg >/dev/null || { echo "needs ffmpeg (brew install ffmpeg)"; exi
|
|||||||
|
|
||||||
mkdir -p build/trailer
|
mkdir -p build/trailer
|
||||||
RAW=build/trailer/raw.avi
|
RAW=build/trailer/raw.avi
|
||||||
OUT=build/Emberwild-trailer.mp4
|
OUT=build/Cinderwild-trailer.mp4
|
||||||
rm -f "$RAW"
|
rm -f "$RAW"
|
||||||
"$GODOT" --path . --resolution 1280x720 --fixed-fps 30 --write-movie "$RAW" \
|
"$GODOT" --path . --resolution 1280x720 --fixed-fps 30 --write-movie "$RAW" \
|
||||||
--script tools/trailer/trailer.gd -- --no-creatures > build/trailer/render.log 2>&1
|
--script tools/trailer/trailer.gd -- --no-creatures > build/trailer/render.log 2>&1
|
||||||
|
|||||||
@@ -182,7 +182,7 @@ func _clock(hours: float) -> void:
|
|||||||
# --- the shots -------------------------------------------------------------------------
|
# --- the shots -------------------------------------------------------------------------
|
||||||
|
|
||||||
func _shot_dawn() -> void:
|
func _shot_dawn() -> void:
|
||||||
big.text = "EMBERWILD"
|
big.text = "CINDERWILD"
|
||||||
sub.text = "Survive the island. Build a home."
|
sub.text = "Survive the island. Build a home."
|
||||||
var c := Vector3(0, 0, -10)
|
var c := Vector3(0, 0, -10)
|
||||||
await _shot(7.5, func(t: float, k: float):
|
await _shot(7.5, func(t: float, k: float):
|
||||||
@@ -335,7 +335,7 @@ func _shot_cove() -> void:
|
|||||||
_park_player(Vector3(pier.start.x, pier.start.y + 1.0, pier.start.z)), "Find what lies beyond.")
|
_park_player(Vector3(pier.start.x, pier.start.y + 1.0, pier.start.z)), "Find what lies beyond.")
|
||||||
|
|
||||||
func _shot_end() -> void:
|
func _shot_end() -> void:
|
||||||
big.text = "EMBERWILD"
|
big.text = "CINDERWILD"
|
||||||
sub.text = "Self-hosted co-op survival · %s" % String(ProjectSettings.get_setting("application/config/version", ""))
|
sub.text = "Self-hosted co-op survival · %s" % String(ProjectSettings.get_setting("application/config/version", ""))
|
||||||
# Rising over the Cove's bay, looking back at the land.
|
# Rising over the Cove's bay, looking back at the land.
|
||||||
var pier: Node3D = world.pier
|
var pier: Node3D = world.pier
|
||||||
|
|||||||
Reference in New Issue
Block a user