Compare commits
12
Commits
773a2bbc9c
...
ea98d1b556
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ea98d1b556 | ||
|
|
62c4015410 | ||
|
|
32748d1786 | ||
|
|
a4bb78585b | ||
|
|
ad8a5392d6 | ||
|
|
91d9f6b419 | ||
|
|
a0421226a2 | ||
|
|
4d86a57d75 | ||
|
|
a90f4b7ed0 | ||
|
|
81451247b6 | ||
|
|
d34c6cd36d | ||
|
|
eea55def6c |
@@ -9,7 +9,11 @@
|
||||
# SSL certificates are now handled automatically by Caddy
|
||||
# - role: ssl # REMOVED - Caddy handles all certificate management
|
||||
- role: github-actions
|
||||
# Provisions streams/pipelines/lookup tables over the Graylog REST API, so
|
||||
# it can only run when the stack is up -- see graylog_enabled in
|
||||
# inventories/home/hosts.yml.
|
||||
- role: graylog-config
|
||||
when: graylog_enabled | bool
|
||||
tags: graylog-config
|
||||
- role: ups
|
||||
tags: ups
|
||||
|
||||
@@ -1,5 +1,26 @@
|
||||
---
|
||||
all:
|
||||
vars:
|
||||
# Master switch for the Graylog logging stack (graylog + graylog-opensearch
|
||||
# + graylog-mongo), the fluent-bit journal shipper that feeds it, the GeoIP
|
||||
# database download it enriches with, and the graylog-config role that
|
||||
# provisions its streams/pipelines over the REST API.
|
||||
#
|
||||
# Lives in inventory rather than a role default because it is read by three
|
||||
# separate roles (common, podman, graylog-config) and role defaults are
|
||||
# scoped to their own role.
|
||||
#
|
||||
# Off because the stack is the worst cost/benefit tenant on this 4-core box:
|
||||
# two JVMs plus MongoDB hold ~1.6 GB resident and burn ~3% of the CPU around
|
||||
# the clock to store ~3k messages a day -- about 28 MB of actual log data
|
||||
# across its four live indices. journald already retains ~25 days of the
|
||||
# same logs at its 500M cap, so turning this off costs searchability, not
|
||||
# the logs themselves.
|
||||
#
|
||||
# Set true (here, or -e graylog_enabled=true) to bring it back. All data
|
||||
# under {{ graylog_path }} is left in place, so re-enabling resumes with the
|
||||
# existing indices, streams and pipelines intact.
|
||||
graylog_enabled: false
|
||||
hosts:
|
||||
home.debyl.io:
|
||||
ansible_user: fedora
|
||||
|
||||
@@ -11,11 +11,15 @@
|
||||
name: fail2ban
|
||||
state: restarted
|
||||
|
||||
# Guarded: a config change still notifies this handler when graylog_enabled is
|
||||
# false, and an unconditional restart would start the service the fluent-bit
|
||||
# task just stopped.
|
||||
- name: restart fluent-bit
|
||||
become: true
|
||||
ansible.builtin.systemd:
|
||||
name: fluent-bit
|
||||
state: restarted
|
||||
when: graylog_enabled | bool
|
||||
|
||||
- name: restart_journald
|
||||
become: true
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
---
|
||||
# Fluent Bit - Log forwarder from journald to Graylog GELF
|
||||
# Deployed as systemd service (not container) for direct journal access
|
||||
#
|
||||
# Graylog's GELF input is fluent-bit's only output, so the two share a switch:
|
||||
# with the stack down fluent-bit would just spin retrying a dead 127.0.0.1:12202
|
||||
# and filling the journal it is meant to be draining. The package and config
|
||||
# stay installed either way -- only the service follows graylog_enabled (see
|
||||
# inventories/home/hosts.yml) -- so re-enabling is a restart, not a reinstall.
|
||||
|
||||
- name: install fluent-bit package
|
||||
become: true
|
||||
@@ -37,9 +43,9 @@
|
||||
mode: '0644'
|
||||
notify: restart fluent-bit
|
||||
|
||||
- name: enable and start fluent-bit service
|
||||
- name: set fluent-bit service state to match graylog_enabled
|
||||
become: true
|
||||
ansible.builtin.systemd:
|
||||
name: fluent-bit
|
||||
enabled: true
|
||||
state: started
|
||||
enabled: "{{ graylog_enabled | bool }}"
|
||||
state: "{{ 'started' if (graylog_enabled | bool) else 'stopped' }}"
|
||||
|
||||
@@ -3,10 +3,20 @@
|
||||
# 1.9 TB root means it will happily grow into the tens of gigabytes; it had
|
||||
# reached 4 GB before this was set.
|
||||
#
|
||||
# Kept small on purpose: every container runs with log_driver=journald and
|
||||
# Container stdout lands here: every container runs with log_driver=journald
|
||||
# except zomboid, which is chatty enough to evict everything else and so gets
|
||||
# its own rotating k8s-file log instead.
|
||||
{% if graylog_enabled | bool %}
|
||||
# fluent-bit drains the journal into Graylog continuously (systemd input,
|
||||
# _COMM=conmon -- see templates/fluent-bit/fluent-bit.conf.j2), so Graylog is
|
||||
# the system of record. What stays here is only the buffer that covers
|
||||
# fluent-bit being down, and 500M is a long outage at this log rate.
|
||||
# the system of record and what stays here is only the buffer that covers
|
||||
# fluent-bit being down.
|
||||
{% else %}
|
||||
# Graylog is disabled (see graylog_enabled in inventories/home/hosts.yml), so
|
||||
# the journal is now the only log store. No increase was needed for that: the
|
||||
# cap is a size limit, not a time limit, and fluent-bit only ever read the
|
||||
# journal rather than rotating it, so retention is unchanged at roughly 25 days
|
||||
# at the current rate -- longer than Graylog's own four live indices covered.
|
||||
{% endif %}
|
||||
[Journal]
|
||||
SystemMaxUse={{ journald_max_use | default('500M') }}
|
||||
|
||||
@@ -19,10 +19,15 @@ Stages, in order — the ordering is deliberate, see the comments in the templat
|
||||
2. **SQLite snapshots** (`.backup`, then `pragma integrity_check`) where used.
|
||||
3. **rsync** of the data tree, config, and db dumps to TrueNAS.
|
||||
|
||||
Failures raise `status=failed` on the `nextcloud-backup` syslog tag, which an
|
||||
external Graylog rule matches to send mail. Do not rename that tag: it is shared
|
||||
by every instance including Gitea, and renaming it here silently stops alerting
|
||||
for all of them.
|
||||
Failures raise `status=failed` on the `nextcloud-backup` syslog tag. The mail
|
||||
itself is sent by the unit's own `OnFailure=` handler
|
||||
(`templates/nextcloud/nextcloud-backup-alert.sh.j2`) straight through
|
||||
`sendmail`, so alerting does **not** depend on Graylog and is unaffected by
|
||||
`graylog_enabled` being off. A Graylog rule matching the same tag is a
|
||||
secondary, dashboard-side copy of that signal.
|
||||
|
||||
Do not rename that tag: it is shared by every instance including Gitea, and
|
||||
renaming it here silently stops the Graylog-side alerting for all of them.
|
||||
|
||||
|
||||
## Restore
|
||||
@@ -141,3 +146,55 @@ LibreSign setup failures and buys nothing at this scale.
|
||||
- `LC_ALL` / `LANG` must be set or the JVM comes up as `ANSI_X3.4-1968` and
|
||||
LibreSign warns that accented characters in signer names will be mangled
|
||||
(LibreSign issue #4872).
|
||||
|
||||
|
||||
## Logging
|
||||
|
||||
Every container runs with `log_driver=journald`, so container stdout lands in
|
||||
the host journal, capped at 500M by `roles/common/templates/journald-size.conf.j2`
|
||||
(about 25 days at the current rate). `journalctl CONTAINER_NAME=<name>` is the
|
||||
day-to-day way to read it.
|
||||
|
||||
On top of that sits an optional Graylog stack — `graylog`, `graylog-opensearch`,
|
||||
`graylog-mongo`, fed by a host `fluent-bit` service that tails the journal and
|
||||
ships GELF to `127.0.0.1:12202`, enriched with the MaxMind GeoIP database, and
|
||||
configured over the REST API by the separate `graylog-config` role.
|
||||
|
||||
**It is off.** The switch is `graylog_enabled` in
|
||||
`inventories/home/hosts.yml`, and it gates all five of those pieces at once:
|
||||
|
||||
| `graylog_enabled` | effect |
|
||||
| --- | --- |
|
||||
| `true` | stack deployed, fluent-bit shipping, GeoIP downloaded, `graylog-config` runs, `logs.debyl.io` proxies to the UI and `/gelf` |
|
||||
| `false` | containers removed and their systemd user units disabled and deleted, fluent-bit stopped and disabled, GeoIP and `graylog-config` skipped, `logs.debyl.io` answers 503 |
|
||||
|
||||
It was turned off because the cost/benefit is bad on a 4-core box: two JVMs plus
|
||||
MongoDB held ~1.6 GB resident and ~3% of the CPU continuously to store roughly
|
||||
3k messages a day — about 28 MB of real log data across four live indices — all
|
||||
of which journald already keeps for longer.
|
||||
|
||||
Turning it back on is `graylog_enabled: true` plus `make deploy TAGS=graylog`.
|
||||
Nothing is destroyed by the off path: the volumes under `{{ graylog_path }}`
|
||||
keep the indices, the Mongo database holding streams/pipelines/dashboards, and
|
||||
the node-id file, so the stack comes back with its configuration intact.
|
||||
|
||||
Two things genuinely stop while it is off, both by design:
|
||||
|
||||
- **Search and dashboards.** The logs still exist in journald; the query
|
||||
interface over them does not.
|
||||
- **External GELF ingest.** The AWS Lambda that POSTs to `logs.debyl.io/gelf`
|
||||
for the `debyltech-api` stream gets a 503. Those events are dropped, not
|
||||
queued — nothing else records them.
|
||||
|
||||
### Caddy config reloads
|
||||
|
||||
The `reload caddy` handler deliberately reads `/config/Caddyfile`, not the
|
||||
`/etc/caddy/Caddyfile` the container starts from, even though both are the same
|
||||
host file. `/etc/caddy/Caddyfile` is a **single-file** bind mount, which podman
|
||||
binds by inode, and Ansible's `template` module writes a temp file and renames
|
||||
it into place — so every deploy gives the host file a new inode while the
|
||||
container keeps seeing the one it was created with. Reloading from that path
|
||||
silently re-applied the previous config; changes only landed when something
|
||||
recreated the container. `{{ caddy_path }}/config` is also bind-mounted as a
|
||||
*directory* at `/config`, and directory mounts resolve names at `open()` time,
|
||||
so `/config/Caddyfile` is always the file Ansible just wrote.
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,7 @@
|
||||
function SpawnPoints()
|
||||
return {
|
||||
unemployed = {
|
||||
{ worldX = 40, worldY = 22, posX = 67, posY = 201 }
|
||||
}
|
||||
}
|
||||
end
|
||||
@@ -0,0 +1,15 @@
|
||||
function SpawnRegions()
|
||||
return {
|
||||
{ name = "Brandenburg, KY", file = "media/maps/Brandenburg, KY/spawnpoints.lua" },
|
||||
{ name = "Echo Creek, KY", file = "media/maps/Echo Creek, KY/spawnpoints.lua" },
|
||||
{ name = "Ekron, KY", file = "media/maps/Ekron, KY/spawnpoints.lua" },
|
||||
{ name = "Fallas Lake, KY", file = "media/maps/Fallas Lake, KY/spawnpoints.lua" },
|
||||
{ name = "Irvington, KY", file = "media/maps/Irvington, KY/spawnpoints.lua" },
|
||||
{ name = "March Ridge, KY", file = "media/maps/March Ridge, KY/spawnpoints.lua" },
|
||||
{ name = "Muldraugh, KY", file = "media/maps/Muldraugh, KY/spawnpoints.lua" },
|
||||
{ name = "Riverside, KY", file = "media/maps/Riverside, KY/spawnpoints.lua" },
|
||||
{ name = "Rosewood, KY", file = "media/maps/Rosewood, KY/spawnpoints.lua" },
|
||||
{ name = "Valley Station, KY", file = "media/maps/Valley Station, KY/spawnpoints.lua" },
|
||||
{ name = "West Point, KY", file = "media/maps/West Point, KY/spawnpoints.lua" },
|
||||
}
|
||||
end
|
||||
@@ -25,11 +25,21 @@
|
||||
tags:
|
||||
- caddy
|
||||
|
||||
# Reads /config/Caddyfile, NOT the /etc/caddy/Caddyfile the container starts
|
||||
# from, even though both are the same host file. /etc/caddy/Caddyfile is a
|
||||
# single-file bind mount, which podman binds by inode; the template module
|
||||
# writes a temp file and renames it into place, so every deploy gives the host
|
||||
# file a new inode and the container keeps seeing the one it was created with.
|
||||
# Reloading from that path silently re-applied the old config -- the change only
|
||||
# ever landed when something recreated the container. {{ caddy_path }}/config is
|
||||
# also bind-mounted as a *directory* at /config, and a directory mount resolves
|
||||
# names at open() time, so /config/Caddyfile is always the file Ansible just
|
||||
# wrote.
|
||||
- name: reload caddy
|
||||
become: true
|
||||
become_user: "{{ podman_user }}"
|
||||
ansible.builtin.command: |
|
||||
podman exec caddy caddy reload --config /etc/caddy/Caddyfile
|
||||
podman exec caddy caddy reload --config /config/Caddyfile --adapter caddyfile
|
||||
tags:
|
||||
- caddy
|
||||
- caddy-config
|
||||
@@ -42,3 +52,12 @@
|
||||
scope: user
|
||||
tags:
|
||||
- zomboid
|
||||
|
||||
- name: reload podman systemd
|
||||
become: true
|
||||
become_user: "{{ podman_user }}"
|
||||
ansible.builtin.systemd:
|
||||
daemon_reload: true
|
||||
scope: user
|
||||
tags:
|
||||
- podman
|
||||
|
||||
@@ -1,4 +1,14 @@
|
||||
---
|
||||
# The image's PID 1 is busybox crond (ENTRYPOINT crond -f -d 8), which runs the
|
||||
# /etc/periodic/15min/awsddns script every quarter hour. busybox crond only
|
||||
# waitpid()s the job PIDs it is tracking -- it does no generic orphan reaping --
|
||||
# so whenever the run-parts/sh layer between crond and the script exits first,
|
||||
# the script is reparented to PID 1 and stays <defunct> forever. That had left
|
||||
# 12 zombies across these five containers after 30 days of uptime.
|
||||
#
|
||||
# init: true makes podman inject catatonit as PID 1 with crond as its child, and
|
||||
# catatonit reaps every orphan it inherits. Changing this recreates the
|
||||
# containers, which also clears the zombies already accumulated.
|
||||
- import_tasks: podman/podman-check.yml
|
||||
vars:
|
||||
container_name: awsddns
|
||||
@@ -13,6 +23,7 @@
|
||||
image: "{{ image }}"
|
||||
restart_policy: on-failure:3
|
||||
log_driver: journald
|
||||
init: true
|
||||
env:
|
||||
AWS_ZONE_TTL: 60
|
||||
AWS_ZONE_ID: "{{ aws_zone_id }}"
|
||||
@@ -40,6 +51,7 @@
|
||||
image: "{{ image }}"
|
||||
restart_policy: on-failure:3
|
||||
log_driver: journald
|
||||
init: true
|
||||
env:
|
||||
AWS_ZONE_TTL: 60
|
||||
AWS_ZONE_ID: "{{ aws_skudak_zone_id }}"
|
||||
@@ -67,6 +79,7 @@
|
||||
image: "{{ image }}"
|
||||
restart_policy: on-failure:3
|
||||
log_driver: journald
|
||||
init: true
|
||||
env:
|
||||
AWS_ZONE_TTL: 60
|
||||
AWS_ZONE_ID: "{{ fulfillr_zone_id }}"
|
||||
@@ -96,6 +109,7 @@
|
||||
image: "{{ image }}"
|
||||
restart_policy: on-failure:3
|
||||
log_driver: journald
|
||||
init: true
|
||||
env:
|
||||
AWS_ZONE_TTL: 60
|
||||
AWS_ZONE_ID: "{{ fulfillr_zone_id }}"
|
||||
@@ -123,6 +137,7 @@
|
||||
image: "{{ image }}"
|
||||
restart_policy: on-failure:3
|
||||
log_driver: journald
|
||||
init: true
|
||||
env:
|
||||
AWS_ZONE_TTL: 60
|
||||
AWS_ZONE_ID: "Z07501202A6AYMHCVP50A"
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
---
|
||||
# Runs in place of graylog.yml when graylog_enabled is false.
|
||||
#
|
||||
# A bare `when:` on the import would only stop Ansible from *creating* the
|
||||
# stack; containers already on the host would keep running and their systemd
|
||||
# user units would keep starting them at boot. So the disabled path has to be an
|
||||
# active teardown: disable the units, then remove the containers.
|
||||
#
|
||||
# Volumes under {{ graylog_path }} are deliberately untouched -- indices, the
|
||||
# Mongo database holding streams/pipelines/dashboards, and the node-id file all
|
||||
# survive, so flipping graylog_enabled back to true resumes where this left off.
|
||||
|
||||
# graylog is stopped first: its unit `requires` the other two, and tearing down
|
||||
# a dependency out from under it makes the shutdown noisy for no reason.
|
||||
- name: stop and disable graylog stack systemd units
|
||||
become: true
|
||||
become_user: "{{ podman_user }}"
|
||||
ansible.builtin.systemd:
|
||||
name: "{{ item }}.service"
|
||||
enabled: false
|
||||
state: stopped
|
||||
daemon_reload: true
|
||||
scope: user
|
||||
loop:
|
||||
- graylog
|
||||
- graylog-opensearch
|
||||
- graylog-mongo
|
||||
# Left over from the Elasticsearch-to-OpenSearch migration; the container is
|
||||
# long gone but the enabled unit still tries to start it every boot.
|
||||
- graylog-elastic
|
||||
register: graylog_units
|
||||
failed_when: false
|
||||
tags: graylog
|
||||
|
||||
- name: remove graylog stack containers
|
||||
become: true
|
||||
become_user: "{{ podman_user }}"
|
||||
containers.podman.podman_container:
|
||||
name: "{{ item }}"
|
||||
state: absent
|
||||
loop:
|
||||
- graylog
|
||||
- graylog-opensearch
|
||||
- graylog-mongo
|
||||
tags: graylog
|
||||
|
||||
- name: remove graylog stack systemd unit files
|
||||
become: true
|
||||
become_user: "{{ podman_user }}"
|
||||
ansible.builtin.file:
|
||||
path: "{{ podman_home }}/.config/systemd/user/{{ item }}.service"
|
||||
state: absent
|
||||
loop:
|
||||
- graylog
|
||||
- graylog-opensearch
|
||||
- graylog-mongo
|
||||
- graylog-elastic
|
||||
notify: reload podman systemd
|
||||
tags: graylog
|
||||
@@ -1,4 +1,9 @@
|
||||
---
|
||||
- name: load vendored sophie modlist
|
||||
ansible.builtin.include_vars:
|
||||
file: zomboid_sophie_mods.yml
|
||||
tags: zomboid-conf
|
||||
|
||||
- name: create zomboid host directory volumes
|
||||
become: true
|
||||
ansible.builtin.file:
|
||||
@@ -122,6 +127,97 @@
|
||||
mode: 0644
|
||||
notify: restorecon podman
|
||||
|
||||
# Server config is seeded from the vendored Sophie 42 preset *before* first
|
||||
# boot, so the server never generates a vanilla INI we then have to patch.
|
||||
#
|
||||
# force is off by design: these files are a starting point, not a managed
|
||||
# state. Stop the server, hand-edit them, regenerate the world -- Ansible will
|
||||
# not clobber the edits on the next deploy. Re-push deliberately with
|
||||
# -e zomboid_config_force=true.
|
||||
- name: create zomboid server config directory
|
||||
become: true
|
||||
ansible.builtin.file:
|
||||
path: "{{ zomboid_path }}/data/Server"
|
||||
state: directory
|
||||
owner: "{{ podman_subuid.stdout }}"
|
||||
group: "{{ podman_user }}"
|
||||
mode: 0755
|
||||
notify: restorecon podman
|
||||
tags: zomboid-conf
|
||||
|
||||
- name: seed zomboid server ini from sophie preset
|
||||
become: true
|
||||
ansible.builtin.template:
|
||||
src: zomboid/server.ini.j2
|
||||
dest: "{{ zomboid_path }}/data/Server/{{ zomboid_server_name }}.ini"
|
||||
force: "{{ zomboid_config_force | bool }}"
|
||||
owner: "{{ podman_subuid.stdout }}"
|
||||
group: "{{ podman_user }}"
|
||||
mode: 0644
|
||||
notify: restorecon podman
|
||||
tags: zomboid-conf
|
||||
|
||||
# Copied, not templated: these are Lua and must not go through Jinja.
|
||||
- name: seed zomboid sandbox and spawn config from sophie preset
|
||||
become: true
|
||||
ansible.builtin.copy:
|
||||
src: "zomboid/sophie/{{ item }}.lua"
|
||||
dest: "{{ zomboid_path }}/data/Server/{{ zomboid_server_name }}_{{ item }}.lua"
|
||||
force: "{{ zomboid_config_force | bool }}"
|
||||
owner: "{{ podman_subuid.stdout }}"
|
||||
group: "{{ podman_user }}"
|
||||
mode: 0644
|
||||
loop:
|
||||
- SandboxVars
|
||||
- spawnregions
|
||||
- spawnpoints
|
||||
notify: restorecon podman
|
||||
tags: zomboid-conf
|
||||
|
||||
- name: deploy zomboid log prune script
|
||||
become: true
|
||||
ansible.builtin.template:
|
||||
src: zomboid/zomboid-log-prune.sh.j2
|
||||
dest: "{{ podman_home }}/bin/zomboid-log-prune.sh"
|
||||
owner: "{{ podman_user }}"
|
||||
group: "{{ podman_user }}"
|
||||
mode: '0755'
|
||||
|
||||
- name: deploy zomboid log prune units
|
||||
become: true
|
||||
ansible.builtin.template:
|
||||
src: "zomboid/zomboid-log-prune.{{ item }}.j2"
|
||||
dest: "{{ podman_home }}/.config/systemd/user/zomboid-log-prune.{{ item }}"
|
||||
owner: "{{ podman_user }}"
|
||||
group: "{{ podman_user }}"
|
||||
mode: '0644'
|
||||
loop:
|
||||
- service
|
||||
- timer
|
||||
notify: reload zomboid systemd
|
||||
|
||||
- name: enable zomboid log prune timer
|
||||
become: true
|
||||
become_user: "{{ podman_user }}"
|
||||
ansible.builtin.systemd:
|
||||
name: zomboid-log-prune.timer
|
||||
scope: user
|
||||
enabled: true
|
||||
state: started
|
||||
daemon_reload: true
|
||||
|
||||
# The server's own logs, not the container's. PZ holds these fds open for the
|
||||
# life of the process, so copytruncate is mandatory -- a rename would leave it
|
||||
# writing into an unlinked inode and the file would appear to stop growing.
|
||||
- name: deploy zomboid logrotate config
|
||||
become: true
|
||||
ansible.builtin.template:
|
||||
src: zomboid/logrotate.j2
|
||||
dest: /etc/logrotate.d/zomboid
|
||||
owner: root
|
||||
group: root
|
||||
mode: 0644
|
||||
|
||||
# Set volume permissions for steam user (UID 1000) inside container
|
||||
# This uses podman unshare to set ownership correctly for rootless podman
|
||||
- name: set zomboid volume permissions for steam user
|
||||
@@ -147,11 +243,13 @@
|
||||
name: zomboid
|
||||
image: "{{ image }}"
|
||||
restart_policy: on-failure:3
|
||||
log_driver: journald
|
||||
log_driver: k8s-file
|
||||
log_opt:
|
||||
max_size: 50m
|
||||
env:
|
||||
SERVER_NAME: "{{ zomboid_server_names[zomboid_server_mode] }}"
|
||||
MIN_RAM: 8g
|
||||
MAX_RAM: 24g
|
||||
SERVER_NAME: "{{ zomboid_server_name }}"
|
||||
MIN_RAM: "{{ zomboid_min_ram }}"
|
||||
MAX_RAM: "{{ zomboid_max_ram }}"
|
||||
AUTO_UPDATE: "true"
|
||||
ADMIN_PASSWORD: "{{ zomboid_admin_password }}"
|
||||
SERVER_PASSWORD: "{{ zomboid_password }}"
|
||||
@@ -173,112 +271,43 @@
|
||||
vars:
|
||||
container_name: zomboid
|
||||
|
||||
# Ensure zomboid restarts on any exit (including admin-triggered restarts)
|
||||
- name: configure zomboid systemd to always restart
|
||||
# Make systemd actually supervise the container, so it restarts on any exit --
|
||||
# including the clean one that `@bot restart` produces via RCON quit.
|
||||
#
|
||||
# `podman generate systemd` emits Type=forking with ExecStart=podman start and
|
||||
# a PIDFile pointing at conmon. podman start returns immediately, so the process
|
||||
# systemd is told to watch was never its child; it says so itself in the journal
|
||||
# ("Supervising process N which is not our child. We'll most likely not notice
|
||||
# when it exits") and it does not notice. The unit sat at active/running with
|
||||
# NRestarts=0 while the container was exited(0) and the server was down. The
|
||||
# PIDFile is worse than useless here: it embeds the container ID, so it goes
|
||||
# stale every time a deploy recreates the container.
|
||||
#
|
||||
# Type=simple with `podman start -a` keeps podman in the foreground as systemd's
|
||||
# own child, so the exit is seen and Restart=always fires.
|
||||
#
|
||||
# stdout/stderr are discarded on purpose. Attaching re-emits the container's
|
||||
# output on podman's stdout, which systemd would capture straight back into the
|
||||
# journal -- exactly the flood the k8s-file log driver exists to avoid. Nothing
|
||||
# is lost: `podman logs` still serves it from the container's own log.
|
||||
- name: configure zomboid systemd supervision
|
||||
become: true
|
||||
become_user: "{{ podman_user }}"
|
||||
ansible.builtin.lineinfile:
|
||||
path: "{{ podman_home }}/.config/systemd/user/zomboid.service"
|
||||
regexp: "^Restart="
|
||||
line: "Restart=always"
|
||||
regexp: "{{ item.regexp }}"
|
||||
line: "{{ item.line }}"
|
||||
state: "{{ item.state | default('present') }}"
|
||||
insertafter: '^\[Service\]'
|
||||
loop:
|
||||
- { regexp: '^Restart=', line: 'Restart=always' }
|
||||
- { regexp: '^Type=', line: 'Type=simple' }
|
||||
- { regexp: '^ExecStart=', line: 'ExecStart=/usr/bin/podman start -a zomboid' }
|
||||
- { regexp: '^StandardOutput=', line: 'StandardOutput=null' }
|
||||
- { regexp: '^StandardError=', line: 'StandardError=null' }
|
||||
- { regexp: '^PIDFile=', line: '', state: absent }
|
||||
notify: reload zomboid systemd
|
||||
|
||||
# Check if server INI exists (generated on first server run)
|
||||
- name: check if zomboid server ini exists
|
||||
become: true
|
||||
ansible.builtin.stat:
|
||||
path: "{{ zomboid_path }}/data/Server/{{ zomboid_server_names[zomboid_server_mode] }}.ini"
|
||||
register: zomboid_ini_stat
|
||||
tags: zomboid-conf
|
||||
|
||||
# Backup settings (requires server to have run once to generate ini)
|
||||
- name: configure zomboid backup settings
|
||||
become: true
|
||||
ansible.builtin.lineinfile:
|
||||
path: "{{ zomboid_path }}/data/Server/{{ zomboid_server_names[zomboid_server_mode] }}.ini"
|
||||
regexp: "^{{ item.key }}="
|
||||
line: "{{ item.key }}={{ item.value }}"
|
||||
loop:
|
||||
- { key: "SaveWorldEveryMinutes", value: "10" }
|
||||
- { key: "BackupsPeriod", value: "30" }
|
||||
- { key: "BackupsCount", value: "10" }
|
||||
# B42 Linux server fix: disable Lua checksum to allow mods to load
|
||||
- { key: "DoLuaChecksum", value: "false" }
|
||||
# Server password
|
||||
- { key: "Password", value: "{{ zomboid_password }}" }
|
||||
when: zomboid_ini_stat.stat.exists
|
||||
tags: zomboid-conf
|
||||
|
||||
# Discord integration (uses Gregbot token, posts /all chat to Discord)
|
||||
- name: configure zomboid discord integration
|
||||
become: true
|
||||
ansible.builtin.lineinfile:
|
||||
path: "{{ zomboid_path }}/data/Server/{{ zomboid_server_names[zomboid_server_mode] }}.ini"
|
||||
regexp: "^{{ item.key }}="
|
||||
line: "{{ item.key }}={{ item.value }}"
|
||||
loop:
|
||||
- { key: "DiscordEnable", value: "true" }
|
||||
- { key: "DiscordToken", value: "{{ zomboid_discord_token }}" }
|
||||
- { key: "DiscordChannel", value: "zomboidbot" }
|
||||
- { key: "DiscordChannelID", value: "1451961291194241095" }
|
||||
when: zomboid_ini_stat.stat.exists
|
||||
tags: zomboid-conf
|
||||
|
||||
# RCON configuration for remote administration
|
||||
- name: configure zomboid rcon
|
||||
become: true
|
||||
ansible.builtin.lineinfile:
|
||||
path: "{{ zomboid_path }}/data/Server/{{ zomboid_server_names[zomboid_server_mode] }}.ini"
|
||||
regexp: "^{{ item.key }}="
|
||||
line: "{{ item.key }}={{ item.value }}"
|
||||
loop:
|
||||
- { key: "RCONPort", value: "{{ zomboid_rcon_port }}" }
|
||||
- { key: "RCONPassword", value: "{{ zomboid_admin_password }}" }
|
||||
when: zomboid_ini_stat.stat.exists
|
||||
tags: zomboid-conf
|
||||
|
||||
# Mod configuration for 'modded' server profile
|
||||
- name: configure zomboid mods for modded server
|
||||
become: true
|
||||
ansible.builtin.lineinfile:
|
||||
path: "{{ zomboid_path }}/data/Server/{{ zomboid_server_names[zomboid_server_mode] }}.ini"
|
||||
regexp: "^{{ item.key }}="
|
||||
line: "{{ item.key }}={{ item.value }}"
|
||||
loop:
|
||||
- { key: "Mods", value: "{{ zomboid_mods.mod_ids }}" }
|
||||
- { key: "WorkshopItems", value: "{{ zomboid_mods.workshop_items }}" }
|
||||
when:
|
||||
- zomboid_server_mode == 'modded'
|
||||
- zomboid_ini_stat.stat.exists
|
||||
tags: zomboid-conf
|
||||
|
||||
# Mod configuration for 'b42revamp' server profile (168 mods from Steam collection)
|
||||
- name: configure zomboid mods for b42revamp server
|
||||
become: true
|
||||
ansible.builtin.lineinfile:
|
||||
path: "{{ zomboid_path }}/data/Server/{{ zomboid_server_names[zomboid_server_mode] }}.ini"
|
||||
regexp: "^{{ item.key }}="
|
||||
line: "{{ item.key }}={{ item.value }}"
|
||||
loop:
|
||||
- { key: "Mods", value: "{{ zomboid_mods_b42revamp.mod_ids }}" }
|
||||
- { key: "WorkshopItems", value: "{{ zomboid_mods_b42revamp.workshop_items }}" }
|
||||
when:
|
||||
- zomboid_server_mode == 'b42revamp'
|
||||
- zomboid_ini_stat.stat.exists
|
||||
tags: zomboid-conf
|
||||
|
||||
# Map configuration (for modded servers with custom maps)
|
||||
- name: configure zomboid map
|
||||
become: true
|
||||
ansible.builtin.lineinfile:
|
||||
path: "{{ zomboid_path }}/data/Server/{{ zomboid_server_names[zomboid_server_mode] }}.ini"
|
||||
regexp: "^Map="
|
||||
line: "Map={{ zomboid_maps[zomboid_server_mode] }}"
|
||||
when:
|
||||
- zomboid_server_mode != 'vanilla'
|
||||
- zomboid_ini_stat.stat.exists
|
||||
tags: zomboid-conf
|
||||
|
||||
# Firewall logging for player IP correlation
|
||||
# Logs new UDP connections to Zomboid port for IP address tracking
|
||||
- name: add firewall rule to log zomboid connections
|
||||
@@ -436,71 +465,198 @@
|
||||
notify: restart firewalld
|
||||
tags: firewall
|
||||
|
||||
# Priority 4: LOG rate-limited queries from unverified IPs
|
||||
# Very aggressive: 2 burst, then 1 per hour
|
||||
# Note: Uses same hashlimit name as DROP rule to share bucket
|
||||
- name: log rate-limited queries from unverified IPs on 16261
|
||||
# Priority 4/5: query flood limiting.
|
||||
#
|
||||
# The original thresholds were 1/hour burst 2 per source IP, on the theory that
|
||||
# only scanners send 53-byte query packets and that real players would first be
|
||||
# marked verified by the priority-2 rule when they sent something else. That
|
||||
# premise is inverted: a client's *first* contact is a 53-byte query, so nobody
|
||||
# can be verified before they query, and nobody can query more than twice an
|
||||
# hour without being dropped. The counters were unambiguous -- 5 packets ever
|
||||
# matched the verified-accept rule against 30,563 drops -- and fail2ban then
|
||||
# banned the dropped players for a week each, several an hour, all residential
|
||||
# IPs. The server was unreachable for everyone.
|
||||
#
|
||||
# Remove the old rules so hosts carrying them converge, then re-add the same
|
||||
# shape at a threshold no real client reaches. Opening the server browser sends
|
||||
# a handful of queries; a flood sends thousands.
|
||||
|
||||
- name: remove broken log query rate-limit rule on 16261 (permanent)
|
||||
become: true
|
||||
ansible.builtin.command: >
|
||||
firewall-cmd --permanent --direct --add-rule ipv4 filter INPUT 4
|
||||
firewall-cmd --permanent --direct --remove-rule ipv4 filter INPUT 4
|
||||
-p udp --dport 16261 -m conntrack --ctstate NEW
|
||||
-m length --length 53
|
||||
-m hashlimit --hashlimit-above 1/hour --hashlimit-burst 2
|
||||
--hashlimit-mode srcip --hashlimit-name zomboid_query_16261
|
||||
--hashlimit-htable-expire 3600000
|
||||
-j LOG --log-prefix "ZOMBOID_RATELIMIT: " --log-level 4
|
||||
register: unverified_log_result
|
||||
changed_when: "'already' not in unverified_log_result.stderr"
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
notify: restart firewalld
|
||||
tags: firewall
|
||||
|
||||
- name: log rate-limited queries from unverified IPs on 16262
|
||||
- name: remove broken log query rate-limit rule on 16261 (runtime)
|
||||
become: true
|
||||
ansible.builtin.command: >
|
||||
firewall-cmd --permanent --direct --add-rule ipv4 filter INPUT 4
|
||||
-p udp --dport 16262 -m conntrack --ctstate NEW
|
||||
firewall-cmd --direct --remove-rule ipv4 filter INPUT 4
|
||||
-p udp --dport 16261 -m conntrack --ctstate NEW
|
||||
-m length --length 53
|
||||
-m hashlimit --hashlimit-above 1/hour --hashlimit-burst 2
|
||||
--hashlimit-mode srcip --hashlimit-name zomboid_query_16262
|
||||
--hashlimit-mode srcip --hashlimit-name zomboid_query_16261
|
||||
--hashlimit-htable-expire 3600000
|
||||
-j LOG --log-prefix "ZOMBOID_RATELIMIT: " --log-level 4
|
||||
register: unverified_log_result_16262
|
||||
changed_when: "'already' not in unverified_log_result_16262.stderr"
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
notify: restart firewalld
|
||||
tags: firewall
|
||||
|
||||
# Priority 5: DROP rate-limited queries from unverified IPs
|
||||
# Note: Uses same hashlimit name as LOG rule to share bucket
|
||||
- name: drop rate-limited queries from unverified IPs on 16261
|
||||
- name: remove broken drop query rate-limit rule on 16261 (permanent)
|
||||
become: true
|
||||
ansible.builtin.command: >
|
||||
firewall-cmd --permanent --direct --add-rule ipv4 filter INPUT 5
|
||||
firewall-cmd --permanent --direct --remove-rule ipv4 filter INPUT 5
|
||||
-p udp --dport 16261 -m conntrack --ctstate NEW
|
||||
-m length --length 53
|
||||
-m hashlimit --hashlimit-above 1/hour --hashlimit-burst 2
|
||||
--hashlimit-mode srcip --hashlimit-name zomboid_query_16261
|
||||
--hashlimit-htable-expire 3600000
|
||||
-j DROP
|
||||
register: unverified_drop_result
|
||||
changed_when: "'already' not in unverified_drop_result.stderr"
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
notify: restart firewalld
|
||||
tags: firewall
|
||||
|
||||
- name: drop rate-limited queries from unverified IPs on 16262
|
||||
- name: remove broken drop query rate-limit rule on 16261 (runtime)
|
||||
become: true
|
||||
ansible.builtin.command: >
|
||||
firewall-cmd --permanent --direct --add-rule ipv4 filter INPUT 5
|
||||
firewall-cmd --direct --remove-rule ipv4 filter INPUT 5
|
||||
-p udp --dport 16261 -m conntrack --ctstate NEW
|
||||
-m length --length 53
|
||||
-m hashlimit --hashlimit-above 1/hour --hashlimit-burst 2
|
||||
--hashlimit-mode srcip --hashlimit-name zomboid_query_16261
|
||||
--hashlimit-htable-expire 3600000
|
||||
-j DROP
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
tags: firewall
|
||||
|
||||
- name: remove broken log query rate-limit rule on 16262 (permanent)
|
||||
become: true
|
||||
ansible.builtin.command: >
|
||||
firewall-cmd --permanent --direct --remove-rule ipv4 filter INPUT 4
|
||||
-p udp --dport 16262 -m conntrack --ctstate NEW
|
||||
-m length --length 53
|
||||
-m hashlimit --hashlimit-above 1/hour --hashlimit-burst 2
|
||||
--hashlimit-mode srcip --hashlimit-name zomboid_query_16262
|
||||
--hashlimit-htable-expire 3600000
|
||||
-j LOG --log-prefix "ZOMBOID_RATELIMIT: " --log-level 4
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
tags: firewall
|
||||
|
||||
- name: remove broken log query rate-limit rule on 16262 (runtime)
|
||||
become: true
|
||||
ansible.builtin.command: >
|
||||
firewall-cmd --direct --remove-rule ipv4 filter INPUT 4
|
||||
-p udp --dport 16262 -m conntrack --ctstate NEW
|
||||
-m length --length 53
|
||||
-m hashlimit --hashlimit-above 1/hour --hashlimit-burst 2
|
||||
--hashlimit-mode srcip --hashlimit-name zomboid_query_16262
|
||||
--hashlimit-htable-expire 3600000
|
||||
-j LOG --log-prefix "ZOMBOID_RATELIMIT: " --log-level 4
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
tags: firewall
|
||||
|
||||
- name: remove broken drop query rate-limit rule on 16262 (permanent)
|
||||
become: true
|
||||
ansible.builtin.command: >
|
||||
firewall-cmd --permanent --direct --remove-rule ipv4 filter INPUT 5
|
||||
-p udp --dport 16262 -m conntrack --ctstate NEW
|
||||
-m length --length 53
|
||||
-m hashlimit --hashlimit-above 1/hour --hashlimit-burst 2
|
||||
--hashlimit-mode srcip --hashlimit-name zomboid_query_16262
|
||||
--hashlimit-htable-expire 3600000
|
||||
-j DROP
|
||||
register: unverified_drop_result_16262
|
||||
changed_when: "'already' not in unverified_drop_result_16262.stderr"
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
tags: firewall
|
||||
|
||||
- name: remove broken drop query rate-limit rule on 16262 (runtime)
|
||||
become: true
|
||||
ansible.builtin.command: >
|
||||
firewall-cmd --direct --remove-rule ipv4 filter INPUT 5
|
||||
-p udp --dport 16262 -m conntrack --ctstate NEW
|
||||
-m length --length 53
|
||||
-m hashlimit --hashlimit-above 1/hour --hashlimit-burst 2
|
||||
--hashlimit-mode srcip --hashlimit-name zomboid_query_16262
|
||||
--hashlimit-htable-expire 3600000
|
||||
-j DROP
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
tags: firewall
|
||||
|
||||
- name: log query floods on 16261
|
||||
become: true
|
||||
ansible.builtin.command: >
|
||||
firewall-cmd --permanent --direct --add-rule ipv4 filter INPUT 4
|
||||
-p udp --dport 16261 -m conntrack --ctstate NEW
|
||||
-m length --length 53
|
||||
-m hashlimit --hashlimit-above {{ zomboid_query_rate_limit }}
|
||||
--hashlimit-burst {{ zomboid_query_burst }}
|
||||
--hashlimit-mode srcip --hashlimit-name zomboid_query_16261
|
||||
--hashlimit-htable-expire 3600000
|
||||
-j LOG --log-prefix "ZOMBOID_RATELIMIT: " --log-level 4
|
||||
register: qflood_log_16261
|
||||
changed_when: "'already' not in qflood_log_16261.stderr"
|
||||
failed_when: false
|
||||
notify: restart firewalld
|
||||
tags: firewall
|
||||
|
||||
- name: drop query floods on 16261
|
||||
become: true
|
||||
ansible.builtin.command: >
|
||||
firewall-cmd --permanent --direct --add-rule ipv4 filter INPUT 5
|
||||
-p udp --dport 16261 -m conntrack --ctstate NEW
|
||||
-m length --length 53
|
||||
-m hashlimit --hashlimit-above {{ zomboid_query_rate_limit }}
|
||||
--hashlimit-burst {{ zomboid_query_burst }}
|
||||
--hashlimit-mode srcip --hashlimit-name zomboid_query_16261
|
||||
--hashlimit-htable-expire 3600000
|
||||
-j DROP
|
||||
register: qflood_drop_16261
|
||||
changed_when: "'already' not in qflood_drop_16261.stderr"
|
||||
failed_when: false
|
||||
notify: restart firewalld
|
||||
tags: firewall
|
||||
|
||||
- name: log query floods on 16262
|
||||
become: true
|
||||
ansible.builtin.command: >
|
||||
firewall-cmd --permanent --direct --add-rule ipv4 filter INPUT 4
|
||||
-p udp --dport 16262 -m conntrack --ctstate NEW
|
||||
-m length --length 53
|
||||
-m hashlimit --hashlimit-above {{ zomboid_query_rate_limit }}
|
||||
--hashlimit-burst {{ zomboid_query_burst }}
|
||||
--hashlimit-mode srcip --hashlimit-name zomboid_query_16262
|
||||
--hashlimit-htable-expire 3600000
|
||||
-j LOG --log-prefix "ZOMBOID_RATELIMIT: " --log-level 4
|
||||
register: qflood_log_16262
|
||||
changed_when: "'already' not in qflood_log_16262.stderr"
|
||||
failed_when: false
|
||||
notify: restart firewalld
|
||||
tags: firewall
|
||||
|
||||
- name: drop query floods on 16262
|
||||
become: true
|
||||
ansible.builtin.command: >
|
||||
firewall-cmd --permanent --direct --add-rule ipv4 filter INPUT 5
|
||||
-p udp --dport 16262 -m conntrack --ctstate NEW
|
||||
-m length --length 53
|
||||
-m hashlimit --hashlimit-above {{ zomboid_query_rate_limit }}
|
||||
--hashlimit-burst {{ zomboid_query_burst }}
|
||||
--hashlimit-mode srcip --hashlimit-name zomboid_query_16262
|
||||
--hashlimit-htable-expire 3600000
|
||||
-j DROP
|
||||
register: qflood_drop_16262
|
||||
changed_when: "'already' not in qflood_drop_16262.stderr"
|
||||
failed_when: false
|
||||
notify: restart firewalld
|
||||
tags: firewall
|
||||
|
||||
@@ -55,39 +55,39 @@
|
||||
- import_tasks: containers/skudak/wiki.yml
|
||||
vars:
|
||||
db_image: docker.io/library/mysql:5.7.21
|
||||
image: docker.io/solidnerd/bookstack:26.3.4
|
||||
image: docker.io/solidnerd/bookstack:26.5.4
|
||||
tags: skudak, skudak-wiki
|
||||
|
||||
- import_tasks: containers/home/photos.yml
|
||||
vars:
|
||||
db_image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0@sha256:bcf63357191b76a916ae5eb93464d65c07511da41e3bf7a8416db519b40b1c23
|
||||
ml_image: ghcr.io/immich-app/immich-machine-learning:v3.0.0
|
||||
ml_image: ghcr.io/immich-app/immich-machine-learning:v3.1.0
|
||||
redis_image: docker.io/redis:6.2-alpine@sha256:eaba718fecd1196d88533de7ba49bf903ad33664a92debb24660a922ecd9cac8
|
||||
image: ghcr.io/immich-app/immich-server:v3.0.0
|
||||
image: ghcr.io/immich-app/immich-server:v3.1.0
|
||||
tags: photos
|
||||
|
||||
- import_tasks: containers/home/cloud.yml
|
||||
vars:
|
||||
db_image: docker.io/library/mariadb:10.6
|
||||
image: docker.io/library/nextcloud:34.0.2-apache
|
||||
image: docker.io/library/nextcloud:34.0.3-apache
|
||||
tags: cloud
|
||||
|
||||
- import_tasks: containers/skudak/cloud.yml
|
||||
vars:
|
||||
db_image: docker.io/library/mariadb:10.6
|
||||
redis_image: docker.io/redis:8.2-alpine
|
||||
image: docker.io/library/nextcloud:34.0.2-apache
|
||||
image: docker.io/library/nextcloud:34.0.3-apache
|
||||
tags: skudak, skudak-cloud
|
||||
|
||||
- import_tasks: containers/debyltech/fulfillr.yml
|
||||
vars:
|
||||
image: git.debyl.io/debyltech/fulfillr:20260728.2155
|
||||
image: git.debyl.io/debyltech/fulfillr:20260825.1909
|
||||
tags: debyltech, fulfillr
|
||||
|
||||
# Staging back-office (fulfillr-dev.debyltech.com) — same image, staging Turso config.
|
||||
- import_tasks: containers/debyltech/fulfillr-dev.yml
|
||||
vars:
|
||||
image: git.debyl.io/debyltech/fulfillr:20260728.2155
|
||||
image: git.debyl.io/debyltech/fulfillr:20260825.1909
|
||||
tags: debyltech, fulfillr-dev
|
||||
|
||||
- import_tasks: containers/debyltech/uptime-kuma.yml
|
||||
@@ -100,7 +100,10 @@
|
||||
image: docker.io/louislam/uptime-kuma:2.3.2
|
||||
tags: home, uptime
|
||||
|
||||
# GeoIP is only consumed by Graylog's enrichment pipelines, so it follows the
|
||||
# same switch -- see graylog_enabled in inventories/home/hosts.yml.
|
||||
- import_tasks: data/geoip.yml
|
||||
when: graylog_enabled | bool
|
||||
tags: graylog, geoip
|
||||
|
||||
- import_tasks: containers/debyltech/graylog.yml
|
||||
@@ -108,16 +111,27 @@
|
||||
mongo_image: docker.io/mongo:7.0
|
||||
opensearch_image: docker.io/opensearchproject/opensearch:2
|
||||
image: docker.io/graylog/graylog:7.0.1
|
||||
when: graylog_enabled | bool
|
||||
tags: debyltech, graylog
|
||||
|
||||
# The disabled path is an active teardown, not just a skipped create: without it
|
||||
# the containers already on the host keep running and their systemd user units
|
||||
# keep restarting them at boot.
|
||||
- import_tasks: containers/debyltech/graylog-teardown.yml
|
||||
when: not (graylog_enabled | bool)
|
||||
tags: debyltech, graylog
|
||||
|
||||
- import_tasks: containers/home/gregtime.yml
|
||||
vars:
|
||||
image: localhost/greg-time-bot:3.10.0
|
||||
image: localhost/greg-time-bot:3.16.1
|
||||
tags: gregtime
|
||||
|
||||
# Gated on zomboid_enabled (roles/podman/defaults/main.yml) so it can be taken
|
||||
# down for a CI-heavy stretch without losing the world.
|
||||
- import_tasks: containers/home/zomboid.yml
|
||||
vars:
|
||||
image: docker.io/cm2network/steamcmd:root
|
||||
when: zomboid_enabled | bool
|
||||
tags: zomboid
|
||||
|
||||
# ---------------------------------------------------------- Gitea backups
|
||||
|
||||
@@ -238,6 +238,34 @@
|
||||
}
|
||||
|
||||
# Graylog Logs - {{ logs_server_name }}
|
||||
{% if not (graylog_enabled | bool) %}
|
||||
# Graylog is disabled (see graylog_enabled in inventories/home/hosts.yml). The
|
||||
# vhost is kept rather than dropped so the name keeps resolving and its
|
||||
# certificate keeps renewing; both handlers answer 503 instead of proxying to a
|
||||
# port nothing is listening on, which would hang and then 502.
|
||||
#
|
||||
# NOTE: this means the external Lambda that POSTs to /gelf for the
|
||||
# `debyltech-api` stream is being turned away. Its events are dropped, not
|
||||
# queued -- nothing else records them.
|
||||
{{ logs_server_name }} {
|
||||
handle /gelf {
|
||||
respond "Log ingest disabled" 503
|
||||
}
|
||||
|
||||
handle {
|
||||
respond "Graylog is disabled" 503
|
||||
}
|
||||
|
||||
log {
|
||||
output file /var/log/caddy/graylog.log {
|
||||
roll_size {{ caddy_log_roll_size }}
|
||||
roll_keep {{ caddy_log_roll_keep }}
|
||||
roll_keep_for {{ caddy_log_roll_keep_for }}
|
||||
}
|
||||
format json
|
||||
}
|
||||
}
|
||||
{% else %}
|
||||
{{ logs_server_name }} {
|
||||
# GELF HTTP endpoint - open for Lambda (auth via header)
|
||||
# Must come BEFORE ip_restricted_site to allow external access
|
||||
@@ -283,6 +311,7 @@
|
||||
format json
|
||||
}
|
||||
}
|
||||
{% endif %}
|
||||
|
||||
# ============================================================================
|
||||
# COMPLEX CONFIGURATIONS
|
||||
|
||||
@@ -10,10 +10,10 @@ STEAMCMD="/home/steam/steamcmd/steamcmd.sh"
|
||||
SERVER_NAME="${SERVER_NAME:-zomboid}"
|
||||
PUID="${PUID:-1000}"
|
||||
PGID="${PGID:-1000}"
|
||||
MIN_RAM="${MIN_RAM:-4g}"
|
||||
MAX_RAM="${MAX_RAM:-8g}"
|
||||
MIN_RAM="${MIN_RAM:-{{ zomboid_min_ram }}}"
|
||||
MAX_RAM="${MAX_RAM:-{{ zomboid_max_ram }}}"
|
||||
|
||||
echo "=== Project Zomboid Build 42 Server ==="
|
||||
echo "=== Project Zomboid Build 42 Server ({{ zomboid_preset_version }}) ==="
|
||||
echo "Server Name: ${SERVER_NAME}"
|
||||
echo "RAM: ${MIN_RAM} - ${MAX_RAM}"
|
||||
|
||||
@@ -21,7 +21,15 @@ echo "RAM: ${MIN_RAM} - ${MAX_RAM}"
|
||||
echo "=== Setting file permissions ==="
|
||||
usermod -o -u "${PUID}" steam
|
||||
groupmod -o -g "${PGID}" steam
|
||||
chown -R steam:steam "${INSTALL_DIR}" "${CONFIG_DIR}"
|
||||
for dir in "${INSTALL_DIR}" "${CONFIG_DIR}"; do
|
||||
# Recursing is a full inode walk over the install tree -- with 256 workshop
|
||||
# mods that is real SSD churn on every restart, and a no-op after the first.
|
||||
if [ "$(stat -c %u "${dir}")" = "${PUID}" ]; then
|
||||
echo "=== ${dir} already owned by ${PUID}, skipping recursive chown ==="
|
||||
else
|
||||
chown -R steam:steam "${dir}"
|
||||
fi
|
||||
done
|
||||
# Only chown writable parts of /home/steam (not read-only mounts)
|
||||
chown steam:steam /home/steam
|
||||
chown -R steam:steam /home/steam/steamcmd 2>/dev/null || true
|
||||
@@ -32,13 +40,40 @@ mkdir -p "${CONFIG_DIR}/Server"
|
||||
mkdir -p "${CONFIG_DIR}/Saves"
|
||||
mkdir -p "${CONFIG_DIR}/db"
|
||||
|
||||
# Update/Install PZ dedicated server with Build 42 unstable branch
|
||||
# Update/Install PZ dedicated server from the default (stable) branch
|
||||
if [ "${AUTO_UPDATE:-true}" = "true" ]; then
|
||||
echo "=== Updating Project Zomboid Server (Build 42 unstable) ==="
|
||||
echo "=== Updating Project Zomboid Server (Build 42 stable) ==="
|
||||
su -c "${STEAMCMD} +runscript /home/steam/install.scmd" steam
|
||||
echo "=== Update complete ==="
|
||||
fi
|
||||
|
||||
# Strip scripts the dedicated-server package ships but the client no longer has.
|
||||
#
|
||||
# The two Steam apps are packaged separately and have drifted. Base.Log_Stack_01
|
||||
# was removed from the client in 42.16, but app 380870 still ships
|
||||
# entity_logstack.txt. The server registers every script it can see into a new
|
||||
# world's WorldDictionary, so the entry lands in the save, and then every client
|
||||
# fails at world load with:
|
||||
#
|
||||
# WorldDictionaryException: [SpriteConfigs] Missing dictionary script on
|
||||
# client: Base.Log_Stack_01
|
||||
#
|
||||
# Nothing in multiplayer can legitimately use a script no client has, so
|
||||
# removing it server-side costs nothing. This runs after the SteamCMD update on
|
||||
# purpose: validate restores the file on every boot, so it has to be stripped
|
||||
# each time, not once.
|
||||
#
|
||||
# Removing the file does not fix a world whose dictionary already records the
|
||||
# entry -- that save needs regenerating.
|
||||
strip_client_missing_scripts() {
|
||||
local stale="${INSTALL_DIR}/media/scripts/generated/entities/misc/entity_logstack.txt"
|
||||
if [ -f "${stale}" ]; then
|
||||
echo "=== Removing ${stale##*/} (shipped by server, absent from client) ==="
|
||||
rm -f "${stale}"
|
||||
fi
|
||||
}
|
||||
strip_client_missing_scripts
|
||||
|
||||
# Configure JVM memory settings in ProjectZomboid64.json (Build 42 uses JSON config)
|
||||
configure_memory() {
|
||||
local json_file="${INSTALL_DIR}/ProjectZomboid64.json"
|
||||
@@ -65,17 +100,22 @@ configure_memory() {
|
||||
}
|
||||
configure_memory
|
||||
|
||||
# Check if first run (no admin DB)
|
||||
ADMIN_DB="${CONFIG_DIR}/db/${SERVER_NAME}.db"
|
||||
|
||||
# Build server arguments
|
||||
# Note: -modfolders is NOT used - mods are configured via INI only
|
||||
# Reference: IndifferentBroccoli/projectzomboid-server-docker
|
||||
SERVER_ARGS="-cachedir=${CONFIG_DIR} -servername ${SERVER_NAME}"
|
||||
|
||||
# Add admin password for first run
|
||||
if [ ! -f "${ADMIN_DB}" ] && [ -n "${ADMIN_PASSWORD}" ]; then
|
||||
echo "=== First run: setting admin password ==="
|
||||
# Always pass the admin password when we have one.
|
||||
#
|
||||
# This used to be gated on the admin DB not existing yet, on the assumption that
|
||||
# the file's presence meant the admin user was already set up. It does not: PZ
|
||||
# creates the DB file in ServerWorldDatabase.create() *before* it creates the
|
||||
# admin user, so any boot interrupted between those two steps leaves a database
|
||||
# that exists but has no admin. From then on the guard withheld the flag, the
|
||||
# server fell back to an interactive "Enter new administrator password:" prompt,
|
||||
# read EOF because a detached container has no stdin, and died with
|
||||
# NoSuchElementException -- on every restart, forever.
|
||||
if [ -n "${ADMIN_PASSWORD}" ]; then
|
||||
SERVER_ARGS="${SERVER_ARGS} -adminpassword ${ADMIN_PASSWORD}"
|
||||
fi
|
||||
|
||||
|
||||
@@ -12,7 +12,14 @@ force_install_dir /project-zomboid
|
||||
|
||||
login anonymous
|
||||
|
||||
// Install/Update the Project Zomboid Dedicated Server - Unstable Branch (Build 42)
|
||||
app_update 380870 -beta unstable validate
|
||||
// Install/Update the Project Zomboid Dedicated Server (Build 42, stable)
|
||||
//
|
||||
// -beta public is required, not optional. Dropping the flag entirely does NOT
|
||||
// leave a previously-selected beta: SteamCMD writes the new branch to the
|
||||
// manifest's UserConfig but leaves MountedConfig -- and the actual installed
|
||||
// content -- on the old one. That failed silently here, leaving the server on
|
||||
// the 42.14.1 unstable build while the config had already moved to the mod-ID
|
||||
// syntax that only 42.20+ accepts, so every mod reported "not found".
|
||||
app_update 380870 -beta public validate
|
||||
|
||||
quit
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
# {{ ansible_managed }}
|
||||
#
|
||||
# Project Zomboid's server console, which is a single ever-growing file. This is
|
||||
# separate from the container's stdout, which podman rotates itself via the
|
||||
# k8s-file driver's max_size.
|
||||
#
|
||||
# Scope is deliberately just this file. PZ's other logs live in dated
|
||||
# directories under Logs/ that grow in file *count*, not file size, so nothing
|
||||
# there ever crosses a rotation threshold -- those are pruned by age instead,
|
||||
# via zomboid-log-prune.timer.
|
||||
#
|
||||
# copytruncate is mandatory here: the server holds these fds open for the whole
|
||||
# life of the process, so a rename would leave it writing into an unlinked
|
||||
# inode and the file would appear to simply stop growing.
|
||||
#
|
||||
# The Discord bot tails server-console.txt from a byte offset and resets that
|
||||
# offset when the file shrinks, so a truncate under it is safe.
|
||||
{{ zomboid_path }}/data/server-console.txt
|
||||
{
|
||||
# size, not a time directive: logrotate's daily timer is when this is
|
||||
# *checked*, 100M is when it actually rotates. Specifying both makes
|
||||
# logrotate warn that size overrides daily.
|
||||
copytruncate
|
||||
size 100M
|
||||
rotate 3
|
||||
compress
|
||||
delaycompress
|
||||
missingok
|
||||
notifempty
|
||||
}
|
||||
@@ -0,0 +1,443 @@
|
||||
{#-
|
||||
Project Zomboid server config -- Sophie 42 preset.
|
||||
|
||||
Vendored verbatim from https://github.com/GerDeathstar/sophie-pz
|
||||
("Sophie 42 Files.zip", uploaded 2026-07-31 == the `b42` release tag),
|
||||
file Server/sophie-server-42-1.ini.
|
||||
|
||||
Only the keys this deployment actually owns are templated -- credentials,
|
||||
RCON, Discord, backup cadence, and the mod lists. Every gameplay setting
|
||||
(PVP, safety system, safehouses, sleep, MaxPlayers, PauseEmpty, ResetID,
|
||||
the welcome message) is left exactly as the preset author shipped it.
|
||||
|
||||
Seeded once, then left alone: the deploy task uses force=false so the
|
||||
server and admins can hand-edit this file between world regenerations.
|
||||
Re-push it deliberately with -e zomboid_config_force=true.
|
||||
-#}
|
||||
{% set rename_map = {} %}
|
||||
{% for r in zomboid_mods_renamed %}{% set _ = rename_map.update({r.old: r.new}) %}{% endfor %}
|
||||
{% set resolved_mod_ids = [] %}
|
||||
{% for m in zomboid_sophie_mod_ids %}{% set _ = resolved_mod_ids.append(rename_map.get(m, m)) %}{% endfor %}
|
||||
{% set excluded_mod_ids = zomboid_mods_excluded | map(attribute='mod_id') | list %}
|
||||
{% set excluded_workshop_ids = zomboid_mods_excluded | map(attribute='workshop_id') | list %}
|
||||
# Players can hurt and kill other players
|
||||
PVP=true
|
||||
|
||||
# PVP is logged to admin chat
|
||||
PVPLogToolChat=true
|
||||
|
||||
# PVP is logged to file
|
||||
PVPLogToolFile=true
|
||||
|
||||
# Game time stops when there are no players online
|
||||
PauseEmpty=true
|
||||
|
||||
# Toggles global chat on or off.
|
||||
GlobalChat=true
|
||||
|
||||
ChatStreams=s,r,a,w,y,sh,f,all
|
||||
|
||||
# Clients may join without already having an account in the whitelist. If set to false, administrators must manually create username/password combos.
|
||||
Open=true
|
||||
|
||||
# The first welcome message visible in the chat panel. This will be displayed immediately after player login. you can use RGB colours to chance the colour of the welcome message. You can also use < LINE>, without the space, to create a separate lines within your text. Use: \<RGB:1,0,0> This message will show up red!
|
||||
ServerWelcomeMessage=Welcome to Project Zomboid Multiplayer! <LINE> <LINE> To interact with the Chat panel: press Tab, T, or Enter. <LINE> <LINE> The Tab key will change the target stream of the message. <LINE> <LINE> Global Streams: /all <LINE> Local Streams: /say, /yell <LINE> Special Steams: /whisper, /safehouse, /faction. <LINE> <LINE> Press the Up arrow to cycle through your message history. Click the Gear icon to customize chat. <LINE> <LINE> Happy surviving!
|
||||
|
||||
# Display usernames above player's heads in-game.
|
||||
DisplayUserName=true
|
||||
|
||||
# Display first & last name above player's heads.
|
||||
ShowFirstAndLastName=false
|
||||
|
||||
UsernameDisguises=false
|
||||
|
||||
HideDisguisedUserName=false
|
||||
|
||||
SwitchZombiesOwnershipEachUpdate=false
|
||||
|
||||
# Force every new player to spawn at these set x,y,z world coordinates. Find desired coordinates at map.projectzomboid.com. (Ignored when 0,0,0)
|
||||
SpawnPoint=0,0,0
|
||||
|
||||
# Players can enter and leave PVP on an individual basis. A player can only hurt another player when at least one of them is in PVP mode - as shown by the unobscured skull and crossbones on the left of the screen. When SafetySystem=false, players are free to hurt each other at any time if PVP is enabled.
|
||||
SafetySystem=true
|
||||
|
||||
# Display a skull icon over the head of players who have entered PVP mode
|
||||
ShowSafety=true
|
||||
|
||||
# The time it takes for a player to enter and leave PVP mode Min: 0 Max: 1000 Default: 2
|
||||
SafetyToggleTimer=2
|
||||
|
||||
# The delay before a player can enter or leave PVP mode again, having recently done so Min: 0 Max: 1000 Default: 3
|
||||
SafetyCooldownTimer=3
|
||||
|
||||
# Min: 0 Max: 60 Default: 60
|
||||
SafetyDisconnectDelay=60
|
||||
|
||||
# Item types new players spawn with. Separate multiple item types with commas. Example: Base.Axe,Base.Bag_BigHikingBag
|
||||
SpawnItems=
|
||||
|
||||
# Default starting port for player data. If UDP, this is this one of two ports used. Min: 0 Max: 65535 Default: 16261
|
||||
DefaultPort=16261
|
||||
|
||||
# Min: 0 Max: 65535 Default: 16262
|
||||
UDPPort=16262
|
||||
|
||||
# Reset ID determines if the server has undergone a soft-reset. If this number does match the client, the client must create a new character. Used in conjunction with PlayerServerID. It is strongly advised that you backup these IDs somewhere Min: 0 Max: 2147483647 Default: 985847558
|
||||
ResetID=6953472
|
||||
|
||||
# Enter the mod loading ID here. It can be found in \Steam\steamapps\workshop\modID\mods\modName\info.txt
|
||||
Mods={{ resolved_mod_ids | reject('in', excluded_mod_ids) | join(';') }}
|
||||
|
||||
# Enter the foldername of the mod found in \Steam\steamapps\workshop\modID\mods\modName\media\maps\
|
||||
Map={{ zomboid_map }}
|
||||
|
||||
# Kick clients whose game files don't match the server's.
|
||||
DoLuaChecksum=false
|
||||
|
||||
DenyLoginOnOverloadedServer=true
|
||||
|
||||
# Shows the server on the in-game browser. (Note: Steam-enabled servers are always visible in the Steam server browser)
|
||||
Public=false
|
||||
|
||||
# Name of the server displayed in the in-game browser and, if applicable, the Steam browser
|
||||
PublicName={{ zomboid_public_name }}
|
||||
|
||||
# Description displayed in the in-game public server browser. Typing will create a new line in your description
|
||||
PublicDescription=
|
||||
|
||||
# Maximum number of players that can be on the server at one time. This excludes admins.
|
||||
# WARNING: Server player counts above 32 will potentially result in poor map streaming and desync. Please advance with caution. Min: 1 Max: 100 Default: 32
|
||||
MaxPlayers=32
|
||||
|
||||
# Ping limit, in milliseconds, before a player is kicked from the server. (Set to 0 to disable) Min: 0 Max: 2147483647 Default: 0
|
||||
PingLimit=0
|
||||
|
||||
# Items will not respawn in buildings that players have claimed as a safehouse
|
||||
SafehousePreventsLootRespawn=true
|
||||
|
||||
# Remove player accounts from the whitelist after death. This prevents players creating a new character after death on Open=false servers
|
||||
DropOffWhiteListAfterDeath=false
|
||||
|
||||
# All forms of fire are disabled - except for campfires
|
||||
NoFire=false
|
||||
|
||||
# If checked, every time a player dies a global message will be displayed in the chat
|
||||
AnnounceDeath=true
|
||||
|
||||
# If checked, every time an animal dies a global message will be displayed in the chat
|
||||
AnnounceAnimalDeath=false
|
||||
|
||||
# Loaded parts of the map are saved after this set number of real-world minutes have passed. (The map is usually saved only after clients leave a loaded area) Min: 0 Max: 2147483647 Default: 0
|
||||
SaveWorldEveryMinutes=10
|
||||
|
||||
# Both admins and players can claim safehouses
|
||||
PlayerSafehouse=false
|
||||
|
||||
# Only admins can claim safehouses
|
||||
AdminSafehouse=false
|
||||
|
||||
# Allow non-members to enter a safehouse without being invited
|
||||
SafehouseAllowTrepass=true
|
||||
|
||||
# Allow fire to damage safehouses
|
||||
SafehouseAllowFire=true
|
||||
|
||||
# Allow non-members to take items from safehouses
|
||||
SafehouseAllowLoot=true
|
||||
|
||||
# Players will respawn in a safehouse that they were a member of before they died
|
||||
SafehouseAllowRespawn=false
|
||||
|
||||
# Players must have survived this number of in-game days before they are allowed to claim a safehouse Min: 0 Max: 2147483647 Default: 0
|
||||
SafehouseDaySurvivedToClaim=0
|
||||
|
||||
# Players are automatically removed from a safehouse they have not visited for this many real-world hours Min: 0 Max: 2147483647 Default: 144
|
||||
SafeHouseRemovalTime=144
|
||||
|
||||
# Governs whether players can claim non-residential buildings.
|
||||
SafehouseAllowNonResidential=false
|
||||
|
||||
SafehouseDisableDisguises=true
|
||||
|
||||
# Min: 0 Max: 2147483647 Default: 20000
|
||||
MaxSafezoneSize=20000
|
||||
|
||||
# Allow players to destroy world objects with sledgehammers
|
||||
AllowDestructionBySledgehammer=true
|
||||
|
||||
# Allow players to destroy world objects only in their safehouse (require AllowDestructionBySledgehammer to true).
|
||||
SledgehammerOnlyInSafehouse=false
|
||||
|
||||
War=false
|
||||
|
||||
# Time in seconds before the war starts. Min: 60 Max: 2147483647 Default: 600
|
||||
WarStartDelay=600
|
||||
|
||||
# War duration in seconds. Min: 60 Max: 2147483647 Default: 3600
|
||||
WarDuration=3600
|
||||
|
||||
# Safehouse hit points limit. Min: 0 Max: 2147483647 Default: 3
|
||||
WarSafehouseHitPoints=3
|
||||
|
||||
# ServerPlayerID determines if a character is from another server, or single player. This value may be changed by soft resets. If this number does match the client, the client must create a new character. This is used in conjunction with ResetID. It is strongly advised that you backup these IDs somewhere
|
||||
ServerPlayerID=
|
||||
|
||||
# The port for the RCON (Remote Console) Min: 0 Max: 65535 Default: 27015
|
||||
RCONPort={{ zomboid_rcon_port }}
|
||||
|
||||
# RCON password (Pick a strong password)
|
||||
RCONPassword={{ zomboid_admin_password }}
|
||||
|
||||
# Enables global text chat integration with a Discord channel
|
||||
DiscordEnable=true
|
||||
|
||||
# Discord bot access token
|
||||
DiscordToken={{ zomboid_discord_token }}
|
||||
|
||||
# The Discord chat channel name
|
||||
DiscordChatChannel={{ zomboid_discord_chat_channel }}
|
||||
|
||||
# The Discord logs channel name
|
||||
DiscordLogChannel=
|
||||
|
||||
# The Discord commands channel name
|
||||
DiscordCommandChannel=
|
||||
|
||||
# The Slack incoming webhook URL
|
||||
WebhookAddress=
|
||||
|
||||
# Clients must know this password to join the server. (Ignored when hosting a server via the Host button)
|
||||
Password={{ zomboid_password }}
|
||||
|
||||
# Limits the number of different accounts a single Steam user may create on this server. Ignored when using the Hosts button. Min: 0 Max: 2147483647 Default: 0
|
||||
MaxAccountsPerUser=0
|
||||
|
||||
# Allow co-op/splitscreen players
|
||||
AllowCoop=true
|
||||
|
||||
# Players are allowed to sleep when their survivor becomes tired, but they do not NEED to sleep
|
||||
SleepAllowed=false
|
||||
|
||||
# Players get tired and need to sleep. (Ignored if SleepAllowed=false)
|
||||
SleepNeeded=false
|
||||
|
||||
# WIP: Activating this setting may cause visual desynchronization of player positions.
|
||||
KnockedDownAllowed=false
|
||||
|
||||
SneakModeHideFromOtherPlayers=true
|
||||
|
||||
UltraSpeedDoesnotAffectToAnimals=false
|
||||
|
||||
# List Workshop Mod IDs for the server to download. Each must be separated by a semicolon. Example: WorkshopItems=514427485;513111049
|
||||
WorkshopItems={{ zomboid_sophie_workshop_items | reject('in', excluded_workshop_ids) | join(';') }}
|
||||
|
||||
# Show Steam usernames and avatars in the Players list.
|
||||
SteamScoreboard=false
|
||||
|
||||
# Enable the Steam VAC system
|
||||
SteamVAC=true
|
||||
|
||||
# Attempt to configure a UPnP-enabled internet gateway to automatically setup port forwarding rules. The server will fall back to default ports if this fails
|
||||
UPnP=true
|
||||
|
||||
# VOIP is enabled when checked
|
||||
VoiceEnable=true
|
||||
|
||||
# The minimum tile distance over which VOIP sounds can be heard. Min: 0.00 Max: 100000.00 Default: 10.00
|
||||
VoiceMinDistance=10.0
|
||||
|
||||
# The maximum tile distance over which VOIP sounds can be heard. Min: 0.00 Max: 100000.00 Default: 100.00
|
||||
VoiceMaxDistance=100.0
|
||||
|
||||
# Toggle directional audio for VOIP
|
||||
Voice3D=true
|
||||
|
||||
# Min: 10.00 Max: 150.00 Default: 70.00
|
||||
SpeedLimit=70.0
|
||||
|
||||
LoginQueueEnabled=false
|
||||
|
||||
# Min: 20 Max: 1200 Default: 60
|
||||
LoginQueueConnectTimeout=60
|
||||
|
||||
# Set the IP from which the server is broadcast. This is for network configurations with multiple IP addresses, such as server farms
|
||||
server_browser_announced_ip=
|
||||
|
||||
# Players can respawn in-game at the coordinates where they died
|
||||
PlayerRespawnWithSelf=false
|
||||
|
||||
# Players can respawn in-game at a split screen / Remote Play player's location
|
||||
PlayerRespawnWithOther=false
|
||||
|
||||
# Governs how fast time passes while players sleep. Value multiplies the speed of the time that passes during sleeping. Min: 1.00 Max: 100.00 Default: 40.00
|
||||
FastForwardMultiplier=40.0
|
||||
|
||||
# Safehouse acts like a normal house if an owner of the safehouse is connected (so secure when the owner is offline)
|
||||
DisableSafehouseWhenOwnerConnected=false
|
||||
|
||||
# Players can create factions when true
|
||||
Faction=true
|
||||
|
||||
# Players must survive this number of in-game days before being allowed to create a faction Min: 0 Max: 2147483647 Default: 0
|
||||
FactionDaySurvivedToCreate=0
|
||||
|
||||
# Number of players required as faction members before the faction owner can create a group tag Min: 1 Max: 2147483647 Default: 1
|
||||
FactionPlayersRequiredForTag=1
|
||||
|
||||
# Disables radio transmissions from players with an access level
|
||||
DisableRadioStaff=false
|
||||
|
||||
# Disables radio transmissions from players with 'admin' access level
|
||||
DisableRadioAdmin=true
|
||||
|
||||
# Disables radio transmissions from players with 'gm' access level
|
||||
DisableRadioGM=true
|
||||
|
||||
# Disables radio transmissions from players with 'overseer' access level
|
||||
DisableRadioOverseer=false
|
||||
|
||||
# Disables radio transmissions from players with 'moderator' access level
|
||||
DisableRadioModerator=false
|
||||
|
||||
# Disables radio transmissions from invisible players
|
||||
DisableRadioInvisible=true
|
||||
|
||||
# Semicolon-separated list of commands that will not be written to the cmd.txt server log. For example: -vehicle. Inputting * means do NOT write any vehicle command. Inputting: +vehicle.installPart means DO write that command
|
||||
ClientCommandFilter=-vehicle.*;+vehicle.damageWindow;+vehicle.fixPart;+vehicle.installPart;+vehicle.uninstallPart
|
||||
|
||||
# Semicolon-separated list of actions that will be written to the ClientActionLogs.txt server log.
|
||||
ClientActionLogs=ISEnterVehicle;ISExitVehicle;ISTakeEngineParts;
|
||||
|
||||
# Track changes in player perk levels in PerkLog.txt server log
|
||||
PerkLogs=true
|
||||
|
||||
# Maximum number of items that can be placed in a container. Zero means there is no limit. (PLEASE NOTE: This includes individual small items such as nails. A limit of 50 will mean only 50 nails can be stored.) Min: 0 Max: 9000 Default: 0
|
||||
ItemNumbersLimitPerContainer=0
|
||||
|
||||
# Number of days before old blood splats are removed.
|
||||
# Removal happens when map chunks are loaded.
|
||||
# Zero means they will never disappear Min: 0 Max: 365 Default: 0
|
||||
BloodSplatLifespanDays=0
|
||||
|
||||
# Allow use of non-ASCII (cyrillic etc) characters in usernames
|
||||
AllowNonAsciiUsername=false
|
||||
|
||||
BanKickGlobalSound=true
|
||||
|
||||
# If enabled, when HoursForCorpseRemoval triggers, it will also remove player's corpses from the ground.
|
||||
RemovePlayerCorpsesOnCorpseRemoval=false
|
||||
|
||||
# If true, player can use the "delete all" button on bins.
|
||||
TrashDeleteAll=false
|
||||
|
||||
# If true, player can hit again when struck by another player.
|
||||
PVPMeleeWhileHitReaction=false
|
||||
|
||||
# If true, players will have to mouse over someone to see their display name.
|
||||
MouseOverToSeeDisplayName=true
|
||||
|
||||
# If true, automatically hide the player you can't see (like zombies).
|
||||
HidePlayersBehindYou=true
|
||||
|
||||
# Damage multiplier for PVP melee attacks. Min: 0.00 Max: 500.00 Default: 30.00
|
||||
PVPMeleeDamageModifier=30.0
|
||||
|
||||
# Damage multiplier for PVP ranged attacks. Min: 0.00 Max: 500.00 Default: 50.00
|
||||
PVPFirearmDamageModifier=50.0
|
||||
|
||||
# Modify the range of zombie attraction to cars. (Lower values can help with lag.) Min: 0.00 Max: 10.00 Default: 0.50
|
||||
CarEngineAttractionModifier=0.5
|
||||
|
||||
# Governs whether players bump (and knock over) other players when running through them.
|
||||
PlayerBumpPlayer=false
|
||||
|
||||
# Controls display of remote players on the in-game map. 1=Hidden 2=Friends 3=Friends and nearby players 4=Everyone Min: 1 Max: 4 Default: 1
|
||||
MapRemotePlayerVisibility=1
|
||||
|
||||
# Min: 1 Max: 300 Default: 5
|
||||
BackupsCount=10
|
||||
|
||||
BackupsOnStart=true
|
||||
|
||||
BackupsOnVersionChange=true
|
||||
|
||||
# Min: 0 Max: 1500 Default: 0
|
||||
BackupsPeriod=30
|
||||
|
||||
# Disables vehicle towing
|
||||
DisableVehicleTowing=false
|
||||
|
||||
# Disables trailer towing
|
||||
DisableTrailerTowing=false
|
||||
|
||||
# Disables burnt vehicle towing
|
||||
DisableBurntTowing=false
|
||||
|
||||
# Path to the file with the list of words to be prohibited. Each word must be on a separate line
|
||||
BadWordListFile=
|
||||
|
||||
# Path to the file with the list of words that should be allowed, even if they contain bad word. Each word should be on a separate line.
|
||||
GoodWordListFile=
|
||||
|
||||
# What to do with the sender of a bad word in the chat: 1 - ban, 2 - kick, 3 - record the violation in the database, 4 - mute
|
||||
BadWordPolicy=3
|
||||
|
||||
# The symbol or piece of text that will replace the bad word
|
||||
BadWordReplacement=[HIDDEN]
|
||||
|
||||
# Disables safety system anti-cheat protection.
|
||||
AntiCheatSafety=2
|
||||
|
||||
# Disables character speed anti-cheat protection.
|
||||
AntiCheatSpeed=2
|
||||
|
||||
# Disables character no-clipping anti-cheat protection.
|
||||
AntiCheatNoClip=4
|
||||
|
||||
# Disables character hit anti-cheat protection.
|
||||
AntiCheatHit=2
|
||||
|
||||
# Disables packet exception checks anti-cheat protection.
|
||||
AntiCheatPacketException=4
|
||||
|
||||
# Disables player permissions anti-cheat protection.
|
||||
AntiCheatPermission=2
|
||||
|
||||
# Disables player XP anti-cheat protection.
|
||||
AntiCheatXP=2
|
||||
|
||||
# Disables safehouse anti-cheat protection.
|
||||
AntiCheatSafeHouse=2
|
||||
|
||||
# Disables player anti-cheat protection.
|
||||
AntiCheatPlayer=2
|
||||
|
||||
# Disables checksum anti-cheat protection.
|
||||
AntiCheatChecksum=2
|
||||
|
||||
# Sets the multiplayer update period in seconds. Statistics is disabled if value is 0. Min: 0 Max: 10 Default: 1
|
||||
MultiplayerStatisticsPeriod=1
|
||||
|
||||
# Disables scoreboard.
|
||||
DisableScoreboard=false
|
||||
|
||||
# Hides admins in the player list.
|
||||
HideAdminsInPlayerList=false
|
||||
|
||||
# Sets the limit for processing network packets from each client on the server. Min: 100 Max: 1000 Default: 300
|
||||
MaxPacketsPerSecond=300
|
||||
|
||||
# Shows player character coordinates in the lower right corner.
|
||||
ShowCoordinates=false
|
||||
|
||||
# The worldgen seed used to generate the world.
|
||||
# If you want to change this, put a new value in and delete map_worldgen.bin in your save directory.
|
||||
Seed=DMCERZSTKtuzgukR
|
||||
|
||||
UsePhysicsHitReaction=false
|
||||
|
||||
# Min: 64 Max: 1024 Default: 200
|
||||
ChatMessageCharacterLimit=200
|
||||
|
||||
# Min: 1 Max: 30 Default: 3
|
||||
ChatMessageSlowModeTime=3
|
||||
@@ -6,7 +6,7 @@ set -e
|
||||
|
||||
LOGFILE="{{ podman_home }}/.local/share/volumes/zomboid/logs/world-reset.log"
|
||||
TRIGGER_FILE="{{ podman_home }}/.local/share/volumes/gregtime/data/zomboid-reset.trigger"
|
||||
SERVER_NAME="{{ zomboid_server_names[zomboid_server_mode] }}"
|
||||
SERVER_NAME="{{ zomboid_server_name }}"
|
||||
SAVES_PATH="{{ podman_home }}/.local/share/volumes/zomboid/data/Saves/Multiplayer/${SERVER_NAME}"
|
||||
DB_PATH="{{ podman_home }}/.local/share/volumes/zomboid/data/db/${SERVER_NAME}.db"
|
||||
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
[Unit]
|
||||
Description=Prune Zomboid log archive older than {{ zomboid_log_retention_days }} days
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart={{ podman_home }}/bin/zomboid-log-prune.sh
|
||||
@@ -0,0 +1,24 @@
|
||||
#!/bin/bash
|
||||
# Prune Project Zomboid's own log archive.
|
||||
#
|
||||
# PZ rolls its logs into dated directories under Logs/ (logs_YYYY-MM-DD/, plus
|
||||
# loose YYYY-MM-DD_HH-MM_*.txt at the top) and never removes them. That is not
|
||||
# something logrotate can bound: it rotates by size, and no individual file here
|
||||
# ever reaches a sensible threshold -- the growth is in the *number* of files.
|
||||
# 112 directories and 146k files had accumulated to 18 GB before this existed.
|
||||
#
|
||||
# Files are owned by the container's UID, so this runs under podman unshare.
|
||||
set -euo pipefail
|
||||
|
||||
LOGS="{{ zomboid_path }}/data/Logs"
|
||||
DAYS={{ zomboid_log_retention_days }}
|
||||
|
||||
[[ -d "$LOGS" ]] || exit 0
|
||||
|
||||
before=$(podman unshare du -sh "$LOGS" 2>/dev/null | cut -f1)
|
||||
podman unshare find "$LOGS" -type f -mtime +"$DAYS" -delete 2>/dev/null || true
|
||||
# Second pass: the dated directories are empty once their files are gone.
|
||||
podman unshare find "$LOGS" -mindepth 1 -type d -empty -delete 2>/dev/null || true
|
||||
after=$(podman unshare du -sh "$LOGS" 2>/dev/null | cut -f1)
|
||||
|
||||
echo "zomboid log prune: keeping ${DAYS}d, ${before} -> ${after}"
|
||||
@@ -0,0 +1,10 @@
|
||||
[Unit]
|
||||
Description=Prune the Zomboid log archive daily
|
||||
|
||||
[Timer]
|
||||
OnCalendar=daily
|
||||
Persistent=true
|
||||
RandomizedDelaySec=30m
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
@@ -0,0 +1,562 @@
|
||||
---
|
||||
# Sophie 42 modlist -- vendored verbatim from the upstream server preset.
|
||||
#
|
||||
# Source: https://github.com/GerDeathstar/sophie-pz ("Sophie 42 Files.zip",
|
||||
# uploaded 2026-07-31, identical to the `b42` release tag)
|
||||
# Origin: Server/sophie-server-42-1.ini, keys `Mods=` and `WorkshopItems=`
|
||||
#
|
||||
# Kept as lists rather than the semicolon blobs the INI uses so that a Sophie
|
||||
# update produces a reviewable diff. `templates/zomboid/server.ini.j2` joins
|
||||
# them back with `;` after subtracting `zomboid_mods_excluded`.
|
||||
#
|
||||
# ORDER IS SIGNIFICANT for zomboid_sophie_mod_ids -- it is the mod load order.
|
||||
# Preserve upstream ordering exactly when re-vendoring; do not sort.
|
||||
#
|
||||
# No `\` prefix: Build 42 required one only during its unstable period and
|
||||
# rejects it on 42.20+.
|
||||
|
||||
# Mod IDs (from each package's mod.info) -- the `Mods=` server key.
|
||||
zomboid_sophie_mod_ids:
|
||||
- "ModLoadOrderSorter_b42"
|
||||
- "damnlib"
|
||||
- "YAPZLib"
|
||||
- "ChuckleberryFinnAlertSystem"
|
||||
- "errorMagnifier"
|
||||
- "ZomboidPaperAPI"
|
||||
- "MoodleFramework"
|
||||
- "moodlesinlua"
|
||||
- "NeatUI_Framework"
|
||||
- "HBVCEFb42"
|
||||
- "KATTAJ1_ClothesCore"
|
||||
- "SpnOpenClothBase"
|
||||
- "TombBody"
|
||||
- "isoContainers"
|
||||
- "PROJECTRVInterior42"
|
||||
- "LargeProgressIndicator"
|
||||
- "CleanUI"
|
||||
- "MiniHealthPlus"
|
||||
- "BravoItemDetails"
|
||||
- "DetailedDescriptionsForOccupationsAndTraits"
|
||||
- "MutiesContextMenuIcons"
|
||||
- "ItemCondition_KingEJ"
|
||||
- "SPNCC"
|
||||
- "SPNCCDetails"
|
||||
- "SPNCCDetailsHD"
|
||||
- "SeedSeasonIndicator"
|
||||
- "NeatUI_Hairstyler"
|
||||
- "Neat_Building"
|
||||
- "Neat_Building_AddonXP"
|
||||
- "Neat_Crafting"
|
||||
- "Neat_Crafting_AddonXP"
|
||||
- "DynamicMoodles"
|
||||
- "devcattpixelmoodles"
|
||||
- "redrawnmoodles"
|
||||
- "PMResurrection"
|
||||
- "PlainMoodles_by_Slobodskoy"
|
||||
- "quarterbordermoodles"
|
||||
- "SimpleMoodleIndicators"
|
||||
- "v1centesmoodles"
|
||||
- "v1centesmoodlesbeta"
|
||||
- "SandboxSettings"
|
||||
- "SimpleOverhaulTraitsAndOccupations"
|
||||
- "BeyondTen"
|
||||
- "HasResearch"
|
||||
- "WorkingKnowledge"
|
||||
- "SkillRecoveryJournal"
|
||||
- "M13ReadingTweaks"
|
||||
- "P4HasBeenRead"
|
||||
- "P4PickingMeister"
|
||||
- "P4TidyUpMeister"
|
||||
- "KillCount"
|
||||
- "CausaMortis"
|
||||
- "KI5campers"
|
||||
- "KI5trailers"
|
||||
- "TradingTrailer"
|
||||
- "49powerWagon"
|
||||
- "59meteor"
|
||||
- "63beetle"
|
||||
- "63Type2Van"
|
||||
- "65banshee"
|
||||
- "66pontiacLeMans"
|
||||
- "67commando"
|
||||
- "67gt500"
|
||||
- "68firebird"
|
||||
- "69camaro"
|
||||
- "69charger"
|
||||
- "69fordMustang"
|
||||
- "69fordMustangExtra"
|
||||
- "69mini"
|
||||
- "69mini_ItalianJob"
|
||||
- "69mini_PitbullSpecial"
|
||||
- "70barracuda"
|
||||
- "70chevelle"
|
||||
- "70dodge"
|
||||
- "70fordEscort"
|
||||
- "70roadRunner"
|
||||
- "73fordFalcon"
|
||||
- "73fordFalconPS"
|
||||
- "73nissanGTR"
|
||||
- "75grandPrix"
|
||||
- "76chevyKseries"
|
||||
- "76chryslerNewYorker"
|
||||
- "77firebird"
|
||||
- "78amgeneralM35A2"
|
||||
- "78amgeneralM35A2extra"
|
||||
- "78amgeneralM49A2C"
|
||||
- "78amgeneralM50A3"
|
||||
- "78amgeneralM62"
|
||||
- "78lamboCountach"
|
||||
- "79camaro"
|
||||
- "80manKat1"
|
||||
- "81deloreanDMC12"
|
||||
- "82firebird"
|
||||
- "82jeepJ10"
|
||||
- "82jeepJ10t"
|
||||
- "82oshkoshM911"
|
||||
- "82porsche911"
|
||||
- "84buickElectra"
|
||||
- "84cadillacDeVille"
|
||||
- "84corvette"
|
||||
- "84jeepXJ"
|
||||
- "84merc"
|
||||
- "84oldsmobile98"
|
||||
- "85buickLeSabre"
|
||||
- "85chevyCaprice"
|
||||
- "85chevyStepVan"
|
||||
- "85chevyStepVanexpanded"
|
||||
- "85oldsmobileDelta88"
|
||||
- "85pontiacParisienne"
|
||||
- "86chevyCUCV"
|
||||
- "86fordE150"
|
||||
- "86fordE150expanded"
|
||||
- "86fordE150pd"
|
||||
- "86oshkoshP19A"
|
||||
- "87buickRegal"
|
||||
- "87chevySuburban"
|
||||
- "87fordB700"
|
||||
- "87toyotaMR2"
|
||||
- "88chevyS10"
|
||||
- "88toyotaHilux"
|
||||
- "89defender"
|
||||
- "89dodgeCaravan"
|
||||
- "89fordBronco"
|
||||
- "89trooper"
|
||||
- "89volvo200"
|
||||
- "90bmwE30"
|
||||
- "90fordF350ambulance"
|
||||
- "90pierceArrow"
|
||||
- "91fordLTD"
|
||||
- "91fordRanger"
|
||||
- "91geoMetro"
|
||||
- "91lexusLS400"
|
||||
- "91nissan240sx"
|
||||
- "91range"
|
||||
- "92amgeneralM998"
|
||||
- "92amgeneralM998extra"
|
||||
- "92fordCVPI"
|
||||
- "92jeepYJ"
|
||||
- "92jeepYJJP18"
|
||||
- "92nissanGTR"
|
||||
- "93chevySuburban"
|
||||
- "93chevySuburbanExpanded"
|
||||
- "93fordElgin"
|
||||
- "93fordF350"
|
||||
- "93fordTaurus"
|
||||
- "93mustangSSP"
|
||||
- "93townCar"
|
||||
- "99fordCVPI"
|
||||
- "ReactiveSE"
|
||||
- "HereGoesTheSun"
|
||||
- "RainCleansBlood"
|
||||
- "CheckthePapers"
|
||||
- "DeadMansDossier"
|
||||
- "KnoxDetectionKit"
|
||||
- "ZVirusVaccine42BETA"
|
||||
- "LifestyleHobbies"
|
||||
- "GuitarMusicAddon"
|
||||
- "DeltaSeries"
|
||||
- "AP"
|
||||
- "AutoReload"
|
||||
- "AutoTailoring"
|
||||
- "better-auto-mechanics"
|
||||
- "CaffeineMakesSense"
|
||||
- "Drink_Smart"
|
||||
- "dustinguished_bolt_cutters"
|
||||
- "FasterHoodOpening"
|
||||
- "FWOFitnessWorkoutOverhaul"
|
||||
- "FWOBenchPress&Treadmill"
|
||||
- "KeepRadioOnVanillaFriendly"
|
||||
- "MoreUsesForLightersAndMatches"
|
||||
- "NoLighterNeeded"
|
||||
- "OCsPacking"
|
||||
- "SmarterStorage"
|
||||
- "STA_PryOpen"
|
||||
- "stanks_suicide"
|
||||
- "SwapIt"
|
||||
- "throw-your-bag-across"
|
||||
- "UseItemsWhileWalkingB42"
|
||||
- "OpenAllContainers"
|
||||
- "RebalancedPropMoving"
|
||||
- "RepairAnyClothes"
|
||||
- "ReducedWeightWalkieTalkies"
|
||||
- "WashFix"
|
||||
- "w_betterwringing"
|
||||
- "DRAW_ON_MAP"
|
||||
- "FirstAidVHSTapes"
|
||||
- "NepCanteenSlot"
|
||||
- "TacHold AR"
|
||||
- "TacHold Pistol AR"
|
||||
- "ShelterHold_Beehive"
|
||||
- "Ladders42131"
|
||||
- "JWJ"
|
||||
- "ClothingLine"
|
||||
- "ComputerModkum"
|
||||
- "Buttstroke"
|
||||
- "CVI"
|
||||
- "CVLS"
|
||||
- "FunctionalAppliances2"
|
||||
- "JB_BugsInLights"
|
||||
- "LWBetterElectronics"
|
||||
- "MoreDamagedObjects"
|
||||
- "PIP"
|
||||
- "playable_pool"
|
||||
- "PSR"
|
||||
- "RepairWallCracks_B42"
|
||||
- "SomewhatWater"
|
||||
- "Snowman"
|
||||
- "SkillBookExpansionB42"
|
||||
- "UseulTents"
|
||||
- "WorkingVending"
|
||||
- "Video_Game_Consoles"
|
||||
- "VGC_Addon_NESGames"
|
||||
- "VGC_Addon_SEGAGames"
|
||||
- "VGC_Addon_SNESGames"
|
||||
- "VGC_Addon_SNESGames2"
|
||||
- "TVRadio_ReInvented"
|
||||
- "NewMusic"
|
||||
- "NewMusicG70s"
|
||||
- "NewMusicG80s"
|
||||
- "NewMusicG90s"
|
||||
- "NMGreatestHits"
|
||||
- "NewMusic_OrchestraMix"
|
||||
- "Project_Cook"
|
||||
- "B42Horticulture"
|
||||
- "SKITTLE_LongTermPreservation4220"
|
||||
- "VanillaFoodsExpanded"
|
||||
- "SapphCooking_B42"
|
||||
- "Makedriedfruit"
|
||||
- "Makedriedvegetable"
|
||||
- "Makefruitinjar"
|
||||
- "Mushroomjar"
|
||||
- "ServingPlatesB42"
|
||||
- "Edwin'sCannedTent"
|
||||
- "Edwin'sCannedTobaccoKit"
|
||||
- "Edwin'sSurvivalTabsMod"
|
||||
- "dustinguished_cleaning_wipes"
|
||||
- "ExtraBombs2"
|
||||
- "ammomaker"
|
||||
- "B42Survival"
|
||||
- "SWMG"
|
||||
- "MarzGuns"
|
||||
- "LEGION18"
|
||||
- "SpnOpenCloth"
|
||||
- "SpnCloth"
|
||||
- "FH"
|
||||
- "SpnHair"
|
||||
- "VanillaGearExpanded"
|
||||
- "ArmoredVests"
|
||||
- "ArmoredVestsPatch"
|
||||
- "AliceGear"
|
||||
- "[J&G] Firefighter Uniform"
|
||||
- "[J&G] SWAT Uniform"
|
||||
- "[J&G] Trauma Responder Uniform"
|
||||
- "[J&G] Desert Multicam Uniform"
|
||||
- "DarkWpnSlings"
|
||||
- "GanydeBielovzki's Frockin Splendor!"
|
||||
- "GanydeBielovzki's Frockin Splendor! Vol.2"
|
||||
- "GanydeBielovzki's Frockin Splendor! Vol.3"
|
||||
- "GanydeBielovzki's Frockin Splendor! Vol.4"
|
||||
- "GanydeBielovzki's Frockin Splendor! Vol.5"
|
||||
- "GanydeBielovzki's Frockin Shirts n Ties"
|
||||
- "GanydeBielovzki's Frockin Wiseguys"
|
||||
- "GanydeBielovzki's Frockin Stompers! VFR"
|
||||
- "CorvusNVG"
|
||||
- "AccCamos"
|
||||
- "AccCamosPeriodPack"
|
||||
- "TombBodyCompat"
|
||||
- "TombBodyTex"
|
||||
- "TombBodyCustom"
|
||||
- "ProjectArcade"
|
||||
- "BCR"
|
||||
- "DG_MIVehicles"
|
||||
- "WayMoreCars"
|
||||
- "VMZNEW"
|
||||
- "VVR"
|
||||
- "RealisticDash"
|
||||
- "GasPumpIndicator"
|
||||
- "immersive_hotwire"
|
||||
- "NepHighBeams"
|
||||
- "TaillightsAndStoplights"
|
||||
- "VehicleDoorsHotkey"
|
||||
- "VehicleRepairOverhaul"
|
||||
- "VehicleSalvageOverhaulB42"
|
||||
|
||||
# Numeric Steam Workshop page IDs -- the `WorkshopItems=` server key.
|
||||
# These are what SteamCMD actually downloads.
|
||||
zomboid_sophie_workshop_items:
|
||||
- "3423660713"
|
||||
- "3171167894"
|
||||
- "3624971238"
|
||||
- "3077900375"
|
||||
- "2896041179"
|
||||
- "3155647374"
|
||||
- "3396446795"
|
||||
- "3395171770"
|
||||
- "3508537032"
|
||||
- "3610677934"
|
||||
- "3470422050"
|
||||
- "2812326159"
|
||||
- "3429790870"
|
||||
- "2625625421"
|
||||
- "3543229299"
|
||||
- "2926455909"
|
||||
- "3437629766"
|
||||
- "3710913197"
|
||||
- "3477237184"
|
||||
- "3387957272"
|
||||
- "3101668503"
|
||||
- "3641048285"
|
||||
- "3414634809"
|
||||
- "3734639991"
|
||||
- "3760534563"
|
||||
- "3536052310"
|
||||
- "3540503606"
|
||||
- "3502080466"
|
||||
- "3674800555"
|
||||
- "3417051694"
|
||||
- "3386486141"
|
||||
- "3569718104"
|
||||
- "3772979228"
|
||||
- "3391323469"
|
||||
- "3402171609"
|
||||
- "3389609367"
|
||||
- "3386906181"
|
||||
- "2840805724"
|
||||
- "3765241705"
|
||||
- "3567695139"
|
||||
- "3717099183"
|
||||
- "2503622437"
|
||||
- "2776874515"
|
||||
- "2544353492"
|
||||
- "3422220305"
|
||||
- "2769706949"
|
||||
- "2553809727"
|
||||
- "3711044244"
|
||||
- "3670064951"
|
||||
- "3330403100"
|
||||
- "3572122233"
|
||||
- "2900580391"
|
||||
- "2772575623"
|
||||
- "3005903549"
|
||||
- "3041122351"
|
||||
- "3566868353"
|
||||
- "3447272250"
|
||||
- "2478247379"
|
||||
- "3026723485"
|
||||
- "3258343790"
|
||||
- "2991201484"
|
||||
- "3631989559"
|
||||
- "3756938756"
|
||||
- "2937786633"
|
||||
- "2913633066"
|
||||
- "3766571591"
|
||||
- "2873290424"
|
||||
- "3670063857"
|
||||
- "3642935062"
|
||||
- "3490370700"
|
||||
- "3743371090"
|
||||
- "3213391371"
|
||||
- "3161951724"
|
||||
- "3730833846"
|
||||
- "3346905070"
|
||||
- "2799152995"
|
||||
- "3726526329"
|
||||
- "3703948448"
|
||||
- "3248388837"
|
||||
- "3253385114"
|
||||
- "3320947974"
|
||||
- "2886832257"
|
||||
- "2618213077"
|
||||
- "3379334330"
|
||||
- "3596903773"
|
||||
- "3592777775"
|
||||
- "3684254299"
|
||||
- "3409287192"
|
||||
- "2805630347"
|
||||
- "3601417745"
|
||||
- "3418252689"
|
||||
- "3413704851"
|
||||
- "3614034284"
|
||||
- "3418253716"
|
||||
- "3413706334"
|
||||
- "3428008364"
|
||||
- "2870394916"
|
||||
- "2566953935"
|
||||
- "3226885926"
|
||||
- "3196180339"
|
||||
- "3110911330"
|
||||
- "3052360250"
|
||||
- "2886832936"
|
||||
- "3435796523"
|
||||
- "3570973322"
|
||||
- "3034636011"
|
||||
- "2886833398"
|
||||
- "2932549988"
|
||||
- "3292659291"
|
||||
- "3110913021"
|
||||
- "2952802178"
|
||||
- "2942793445"
|
||||
- "3366300557"
|
||||
- "3539691958"
|
||||
- "3008795514"
|
||||
- "3770890864"
|
||||
- "3504401781"
|
||||
- "2409333430"
|
||||
- "2642541073"
|
||||
- "2962175696"
|
||||
- "3287727378"
|
||||
- "2846036306"
|
||||
- "3152529790"
|
||||
- "2969343830"
|
||||
- "3073430075"
|
||||
- "3088951320"
|
||||
- "3001592312"
|
||||
- "2932547723"
|
||||
- "2971246021"
|
||||
- "3656190498"
|
||||
- "3618557184"
|
||||
- "2956146279"
|
||||
- "3580513066"
|
||||
- "3675740871"
|
||||
- "3688879406"
|
||||
- "3615135168"
|
||||
- "3403870858"
|
||||
- "3412944806"
|
||||
- "3660235667"
|
||||
- "3766508989"
|
||||
- "3389448389"
|
||||
- "3388183573"
|
||||
- "3635856965"
|
||||
- "3682557825"
|
||||
- "3447775367"
|
||||
- "3671176591"
|
||||
- "2584112711"
|
||||
- "2940354599"
|
||||
- "3558422176"
|
||||
- "3533994661"
|
||||
- "2714198296"
|
||||
- "3626823538"
|
||||
- "3290232938"
|
||||
- "3579640010"
|
||||
- "3426448380"
|
||||
- "2366717227"
|
||||
- "3391902125"
|
||||
- "3615422211"
|
||||
- "3465040406"
|
||||
- "2699828474"
|
||||
- "2142622992"
|
||||
- "2981090458"
|
||||
- "3460812542"
|
||||
- "3408740647"
|
||||
- "2804531012"
|
||||
- "3153010942"
|
||||
- "3393080905"
|
||||
- "3677017550"
|
||||
- "3680633169"
|
||||
- "3596827035"
|
||||
- "3629835761"
|
||||
- "2868828908"
|
||||
- "3282215361"
|
||||
- "3725497089"
|
||||
- "3394044313"
|
||||
- "3749026793"
|
||||
- "3719237374"
|
||||
- "3042138819"
|
||||
- "3717902500"
|
||||
- "2999183635"
|
||||
- "3413150945"
|
||||
- "3739173520"
|
||||
- "3716934959"
|
||||
- "3725311427"
|
||||
- "3684808606"
|
||||
- "3582960654"
|
||||
- "3669451917"
|
||||
- "3625874308"
|
||||
- "2934155949"
|
||||
- "2831786301"
|
||||
- "3413237147"
|
||||
- "3423725989"
|
||||
- "3408186716"
|
||||
- "3488113291"
|
||||
- "3420581050"
|
||||
- "3739256725"
|
||||
- "3765077081"
|
||||
- "3765079521"
|
||||
- "3765670104"
|
||||
- "3751364201"
|
||||
- "3760471726"
|
||||
- "3490188370"
|
||||
- "3650168851"
|
||||
- "3774789651"
|
||||
- "3577903007"
|
||||
- "3409143790"
|
||||
- "3417502641"
|
||||
- "3418061837"
|
||||
- "3432006285"
|
||||
- "3416833689"
|
||||
- "3399320470"
|
||||
- "3453369692"
|
||||
- "3455296935"
|
||||
- "3453175422"
|
||||
- "3755185651"
|
||||
- "3652008781"
|
||||
- "2788256295"
|
||||
- "3402516345"
|
||||
- "3722064198"
|
||||
- "3722134990"
|
||||
- "3665548194"
|
||||
- "3660382016"
|
||||
- "3557111695"
|
||||
- "2684285534"
|
||||
- "2447729538"
|
||||
- "2463184726"
|
||||
- "3401134276"
|
||||
- "1962761540"
|
||||
- "3610005735"
|
||||
- "3521263771"
|
||||
- "3457969588"
|
||||
- "3443982326"
|
||||
- "3299457564"
|
||||
- "3307376332"
|
||||
- "3411888105"
|
||||
- "3769335201"
|
||||
- "3431256608"
|
||||
- "3475347500"
|
||||
- "3635394848"
|
||||
- "3453676250"
|
||||
- "3538760023"
|
||||
- "3736813592"
|
||||
- "3454414190"
|
||||
- "3431734923"
|
||||
- "3645980077"
|
||||
- "3162566044"
|
||||
- "3520758551"
|
||||
- "3436499337"
|
||||
- "3423424077"
|
||||
- "3635591071"
|
||||
- "3755993986"
|
||||
- "3653092321"
|
||||
- "3438126404"
|
||||
- "3687394815"
|
||||
- "2290459371"
|
||||
- "2757712197"
|
||||
Binary file not shown.
@@ -15,7 +15,7 @@ Usage:
|
||||
|
||||
# Output formats
|
||||
--json Output raw JSON data
|
||||
--ansible Output workshop_items and mod_ids strings for ansible config
|
||||
--ansible Output the modlist as YAML lists for vars/zomboid_sophie_mods.yml
|
||||
--report Human-readable report (default)
|
||||
|
||||
Examples:
|
||||
@@ -302,18 +302,21 @@ def output_ansible(data: Dict) -> None:
|
||||
seen_mod_ids.add(mod_id)
|
||||
mod_ids.append(mod_id)
|
||||
|
||||
# Format for Build 42 (backslash prefix)
|
||||
workshop_str = ";".join(workshop_items)
|
||||
mod_ids_str = ";".join(f"\\{mid}" for mid in mod_ids)
|
||||
|
||||
print("\n# Ansible Configuration for zomboid_mods")
|
||||
print("# Copy these values to ansible/roles/podman/defaults/main.yml")
|
||||
# No backslash prefix: Build 42 required one only during its unstable
|
||||
# period and rejects it on 42.20+.
|
||||
print("\n# Modlist for ansible/roles/podman/vars/zomboid_sophie_mods.yml")
|
||||
print("# Replaces the file body wholesale -- keep the header comment.")
|
||||
print("")
|
||||
print("zomboid_mods:")
|
||||
print(" workshop_items: >-")
|
||||
print(f" {workshop_str}")
|
||||
print(" mod_ids: >-")
|
||||
print(f" {mod_ids_str}")
|
||||
print("# Mod IDs (from each package's mod.info) -- the `Mods=` server key.")
|
||||
print("# ORDER IS SIGNIFICANT: this is the mod load order. Do not sort.")
|
||||
print("zomboid_sophie_mod_ids:")
|
||||
for mod_id in mod_ids:
|
||||
print(f" - {json.dumps(mod_id, ensure_ascii=False)}")
|
||||
print("")
|
||||
print("# Numeric Steam Workshop page IDs -- the `WorkshopItems=` server key.")
|
||||
print("zomboid_sophie_workshop_items:")
|
||||
for workshop_id in workshop_items:
|
||||
print(f' - "{workshop_id}"')
|
||||
|
||||
if data["duplicates"]:
|
||||
print("\n# WARNING: The following Mod IDs had duplicates (kept first occurrence):")
|
||||
|
||||
Reference in New Issue
Block a user