Compare commits
14 Commits
3baa0091f9
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 394a9b8355 | |||
| 7bab50b431 | |||
|
|
b81362d4d1 | ||
| 93b1c1e301 | |||
| d9f2929c0c | |||
|
|
2689f9e29e | ||
| 8b49ef2c46 | |||
|
|
1f4955db82 | ||
| 03122f2901 | |||
|
|
678543dd9a | ||
|
|
0914b5a32b | ||
|
|
1a545bbae7 | ||
|
|
3a2a606c4a | ||
|
|
f183daa16c |
@@ -2,19 +2,27 @@ name: Smoke tests
|
|||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
push:
|
push:
|
||||||
|
pull_request:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
smoke:
|
smoke:
|
||||||
name: Build and smoke test
|
name: Build and smoke test
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
TEST_DB: ./mudserver.db.test
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Code
|
- name: Checkout Code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install Rust
|
- name: Install needed tools
|
||||||
|
env:
|
||||||
|
DEBIAN_FRONTEND: noninteractive
|
||||||
run: |
|
run: |
|
||||||
|
set -e
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y --no-install-recommends openssh-client netcat-openbsd ca-certificates curl
|
||||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
||||||
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
|
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: cargo build
|
run: cargo build
|
||||||
@@ -22,5 +30,202 @@ jobs:
|
|||||||
- name: Validate world data
|
- name: Validate world data
|
||||||
run: ./target/debug/mudtool validate -w ./world
|
run: ./target/debug/mudtool validate -w ./world
|
||||||
|
|
||||||
- name: Run smoke tests
|
- name: Reset smoke database
|
||||||
run: ./run-tests.sh
|
run: rm -f "$TEST_DB"
|
||||||
|
|
||||||
|
- name: Smoke - new player and basics
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
RUST_LOG=info ./target/debug/mudserver -d "$TEST_DB" &
|
||||||
|
MUD_PID=$!
|
||||||
|
trap 'kill $MUD_PID 2>/dev/null || true' EXIT
|
||||||
|
bash scripts/ci/wait-for-tcp.sh 127.0.0.1 2222
|
||||||
|
set +e
|
||||||
|
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p 2222 smoketest@localhost <<'EOF'
|
||||||
|
1
|
||||||
|
1
|
||||||
|
look
|
||||||
|
stats
|
||||||
|
go south
|
||||||
|
go down
|
||||||
|
go north
|
||||||
|
talk barkeep
|
||||||
|
go south
|
||||||
|
go south
|
||||||
|
examine thief
|
||||||
|
attack thief
|
||||||
|
flee
|
||||||
|
quit
|
||||||
|
EOF
|
||||||
|
r=$?
|
||||||
|
set -e
|
||||||
|
[[ $r -eq 0 || $r -eq 255 ]]
|
||||||
|
|
||||||
|
- name: Smoke - weather and time
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
RUST_LOG=info ./target/debug/mudserver -d "$TEST_DB" &
|
||||||
|
MUD_PID=$!
|
||||||
|
trap 'kill $MUD_PID 2>/dev/null || true' EXIT
|
||||||
|
bash scripts/ci/wait-for-tcp.sh 127.0.0.1 2222
|
||||||
|
set +e
|
||||||
|
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p 2222 smoketest@localhost <<'EOF' > weather_test.out
|
||||||
|
go south
|
||||||
|
go down
|
||||||
|
look
|
||||||
|
quit
|
||||||
|
EOF
|
||||||
|
r=$?
|
||||||
|
set -e
|
||||||
|
[[ $r -eq 0 || $r -eq 255 ]]
|
||||||
|
if ! grep -q "The sky is\|raining\|storm\|snow\|fog" weather_test.out; then
|
||||||
|
echo "Error: Weather info not found in look output"
|
||||||
|
cat weather_test.out
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if ! grep -q "\[Night\]\|\[Morning\]\|\[Afternoon\]\|\[Evening\]" weather_test.out; then
|
||||||
|
echo "Error: Time of day info not found in look output"
|
||||||
|
cat weather_test.out
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
rm weather_test.out
|
||||||
|
|
||||||
|
- name: Smoke - persistence (reconnect)
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
RUST_LOG=info ./target/debug/mudserver -d "$TEST_DB" &
|
||||||
|
MUD_PID=$!
|
||||||
|
trap 'kill $MUD_PID 2>/dev/null || true' EXIT
|
||||||
|
bash scripts/ci/wait-for-tcp.sh 127.0.0.1 2222
|
||||||
|
set +e
|
||||||
|
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p 2222 smoketest@localhost <<'EOF'
|
||||||
|
look
|
||||||
|
stats
|
||||||
|
quit
|
||||||
|
EOF
|
||||||
|
r=$?
|
||||||
|
set -e
|
||||||
|
[[ $r -eq 0 || $r -eq 255 ]]
|
||||||
|
|
||||||
|
- name: Smoke - mudtool admin
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
./target/debug/mudtool -d "$TEST_DB" players list
|
||||||
|
./target/debug/mudtool -d "$TEST_DB" players set-admin smoketest true
|
||||||
|
./target/debug/mudtool -d "$TEST_DB" players show smoketest
|
||||||
|
./target/debug/mudtool -d "$TEST_DB" settings set registration_open false
|
||||||
|
./target/debug/mudtool -d "$TEST_DB" settings list
|
||||||
|
|
||||||
|
- name: Smoke - in-game admin
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
RUST_LOG=info ./target/debug/mudserver -d "$TEST_DB" &
|
||||||
|
MUD_PID=$!
|
||||||
|
trap 'kill $MUD_PID 2>/dev/null || true' EXIT
|
||||||
|
bash scripts/ci/wait-for-tcp.sh 127.0.0.1 2222
|
||||||
|
set +e
|
||||||
|
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p 2222 smoketest@localhost <<'EOF'
|
||||||
|
admin help
|
||||||
|
admin list
|
||||||
|
admin registration on
|
||||||
|
admin info smoketest
|
||||||
|
quit
|
||||||
|
EOF
|
||||||
|
r=$?
|
||||||
|
set -e
|
||||||
|
[[ $r -eq 0 || $r -eq 255 ]]
|
||||||
|
|
||||||
|
- name: Smoke - registration gate
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
./target/debug/mudtool -d "$TEST_DB" settings set registration_open false
|
||||||
|
RUST_LOG=info ./target/debug/mudserver -d "$TEST_DB" &
|
||||||
|
MUD_PID=$!
|
||||||
|
trap 'kill $MUD_PID 2>/dev/null || true' EXIT
|
||||||
|
bash scripts/ci/wait-for-tcp.sh 127.0.0.1 2222
|
||||||
|
set +e
|
||||||
|
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p 2222 newplayer@localhost <<'EOF'
|
||||||
|
quit
|
||||||
|
EOF
|
||||||
|
r=$?
|
||||||
|
set -e
|
||||||
|
[[ $r -eq 0 || $r -eq 255 ]]
|
||||||
|
|
||||||
|
- name: Smoke - tick-based combat
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
./target/debug/mudtool -d "$TEST_DB" settings set registration_open true
|
||||||
|
./target/debug/mudtool -d "$TEST_DB" players delete smoketest
|
||||||
|
RUST_LOG=info ./target/debug/mudserver -d "$TEST_DB" &
|
||||||
|
MUD_PID=$!
|
||||||
|
trap 'kill $MUD_PID 2>/dev/null || true' EXIT
|
||||||
|
bash scripts/ci/wait-for-tcp.sh 127.0.0.1 2222
|
||||||
|
set +e
|
||||||
|
(
|
||||||
|
echo "1"
|
||||||
|
echo "1"
|
||||||
|
echo "go south"
|
||||||
|
echo "go down"
|
||||||
|
echo "go south"
|
||||||
|
echo "attack thief"
|
||||||
|
sleep 15
|
||||||
|
echo "stats"
|
||||||
|
echo "quit"
|
||||||
|
) | ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p 2222 smoketest@localhost
|
||||||
|
r=$?
|
||||||
|
set -e
|
||||||
|
[[ $r -eq 0 || $r -eq 255 ]]
|
||||||
|
./target/debug/mudtool -d "$TEST_DB" settings set registration_open true
|
||||||
|
./target/debug/mudtool -d "$TEST_DB" players delete smoketest
|
||||||
|
|
||||||
|
- name: Smoke - JSON-RPC list_commands
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
RUST_LOG=info ./target/debug/mudserver -d "$TEST_DB" &
|
||||||
|
MUD_PID=$!
|
||||||
|
trap 'kill $MUD_PID 2>/dev/null || true' EXIT
|
||||||
|
bash scripts/ci/wait-for-tcp.sh 127.0.0.1 2222
|
||||||
|
set +e
|
||||||
|
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p 2222 rpctest@localhost <<'EOF'
|
||||||
|
1
|
||||||
|
1
|
||||||
|
quit
|
||||||
|
EOF
|
||||||
|
r=$?
|
||||||
|
set -e
|
||||||
|
[[ $r -eq 0 || $r -eq 255 ]]
|
||||||
|
echo '{"_jsonrpc": "2.0", "method": "login", "params": {"username": "rpctest"}, "id": 1}' | nc -w 2 localhost 2223 > rpc_resp.json
|
||||||
|
echo '{"_jsonrpc": "2.0", "method": "list_commands", "params": {}, "id": 2}' | nc -w 2 localhost 2223 >> rpc_resp.json
|
||||||
|
grep -q '"shop"' rpc_resp.json
|
||||||
|
rm rpc_resp.json
|
||||||
|
./target/debug/mudtool -d "$TEST_DB" players delete rpctest
|
||||||
|
|
||||||
|
- name: Verify logging
|
||||||
|
run: |
|
||||||
|
set +e
|
||||||
|
if [ ! -d "logs" ]; then
|
||||||
|
echo "Error: logs directory not found"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
FAILED=0
|
||||||
|
|
||||||
|
echo "Checking mudserver logs..."
|
||||||
|
grep -q "World '.*': .* rooms" logs/mudserver_*.log || { echo "Failed: World loading log missing"; FAILED=1; }
|
||||||
|
grep -q "MUD server listening on" logs/mudserver_*.log || { echo "Failed: Listen log missing"; FAILED=1; }
|
||||||
|
grep -q "New character created: smoketest" logs/mudserver_*.log || { echo "Failed: smoketest creation log missing"; FAILED=1; }
|
||||||
|
grep -q "Admin action: registration setting updated: '.*'" logs/mudserver_*.log || { echo "Failed: Admin action log missing"; FAILED=1; }
|
||||||
|
|
||||||
|
echo "Checking combat logs..."
|
||||||
|
grep -q "Combat: Player 'smoketest' (ID .*) engaged NPC 'Shadowy Thief'" logs/combat_*.log || { echo "Failed: Combat engagement log missing"; FAILED=1; }
|
||||||
|
grep -q "Combat: Player 'smoketest' (ID .*) killed NPC 'Shadowy Thief'" logs/combat_*.log || { echo "Failed: NPC kill log missing"; FAILED=1; }
|
||||||
|
|
||||||
|
if [ $FAILED -ne 0 ]; then
|
||||||
|
echo "--- LOG VERIFICATION FAILED ---"
|
||||||
|
echo "--- MUDSERVER LOG CONTENTS ---"
|
||||||
|
cat logs/mudserver_*.log
|
||||||
|
echo "--- COMBAT LOG CONTENTS ---"
|
||||||
|
cat logs/combat_*.log
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Logging verification passed."
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -2,3 +2,5 @@
|
|||||||
*.db
|
*.db
|
||||||
*.db-shm
|
*.db-shm
|
||||||
*.db-wal
|
*.db-wal
|
||||||
|
/logs
|
||||||
|
/manual_logs
|
||||||
|
|||||||
174
Cargo.lock
generated
174
Cargo.lock
generated
@@ -61,56 +61,6 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "anstream"
|
|
||||||
version = "0.6.21"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a"
|
|
||||||
dependencies = [
|
|
||||||
"anstyle",
|
|
||||||
"anstyle-parse",
|
|
||||||
"anstyle-query",
|
|
||||||
"anstyle-wincon",
|
|
||||||
"colorchoice",
|
|
||||||
"is_terminal_polyfill",
|
|
||||||
"utf8parse",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "anstyle"
|
|
||||||
version = "1.0.14"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "anstyle-parse"
|
|
||||||
version = "0.2.7"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
|
|
||||||
dependencies = [
|
|
||||||
"utf8parse",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "anstyle-query"
|
|
||||||
version = "1.1.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
|
|
||||||
dependencies = [
|
|
||||||
"windows-sys 0.61.2",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "anstyle-wincon"
|
|
||||||
version = "3.0.11"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
|
|
||||||
dependencies = [
|
|
||||||
"anstyle",
|
|
||||||
"once_cell_polyfill",
|
|
||||||
"windows-sys 0.61.2",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anyhow"
|
name = "anyhow"
|
||||||
version = "1.0.102"
|
version = "1.0.102"
|
||||||
@@ -335,12 +285,6 @@ dependencies = [
|
|||||||
"inout",
|
"inout",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "colorchoice"
|
|
||||||
version = "1.0.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "compact_str"
|
name = "compact_str"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
@@ -385,6 +329,30 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossbeam-channel"
|
||||||
|
version = "0.5.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
|
||||||
|
dependencies = [
|
||||||
|
"crossbeam-utils",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossbeam-queue"
|
||||||
|
version = "0.3.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115"
|
||||||
|
dependencies = [
|
||||||
|
"crossbeam-utils",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossbeam-utils"
|
||||||
|
version = "0.8.21"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crossterm"
|
name = "crossterm"
|
||||||
version = "0.28.1"
|
version = "0.28.1"
|
||||||
@@ -694,29 +662,6 @@ dependencies = [
|
|||||||
"syn 2.0.117",
|
"syn 2.0.117",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "env_filter"
|
|
||||||
version = "1.0.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7a1c3cc8e57274ec99de65301228b537f1e4eedc1b8e0f9411c6caac8ae7308f"
|
|
||||||
dependencies = [
|
|
||||||
"log",
|
|
||||||
"regex",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "env_logger"
|
|
||||||
version = "0.11.9"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b2daee4ea451f429a58296525ddf28b45a3b64f1acf6587e2067437bb11e218d"
|
|
||||||
dependencies = [
|
|
||||||
"anstream",
|
|
||||||
"anstyle",
|
|
||||||
"env_filter",
|
|
||||||
"jiff",
|
|
||||||
"log",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "equivalent"
|
name = "equivalent"
|
||||||
version = "1.0.2"
|
version = "1.0.2"
|
||||||
@@ -809,6 +754,21 @@ version = "0.4.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
|
checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "flexi_logger"
|
||||||
|
version = "0.29.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "88a5a6882b2e137c4f2664562995865084eb5a00611fba30c582ef10354c4ad8"
|
||||||
|
dependencies = [
|
||||||
|
"chrono",
|
||||||
|
"crossbeam-channel",
|
||||||
|
"crossbeam-queue",
|
||||||
|
"log",
|
||||||
|
"nu-ansi-term",
|
||||||
|
"regex",
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fnv"
|
name = "fnv"
|
||||||
version = "1.0.7"
|
version = "1.0.7"
|
||||||
@@ -1166,12 +1126,6 @@ dependencies = [
|
|||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "is_terminal_polyfill"
|
|
||||||
version = "1.70.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itertools"
|
name = "itertools"
|
||||||
version = "0.14.0"
|
version = "0.14.0"
|
||||||
@@ -1187,30 +1141,6 @@ version = "1.0.17"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
|
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "jiff"
|
|
||||||
version = "0.2.23"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1a3546dc96b6d42c5f24902af9e2538e82e39ad350b0c766eb3fbf2d8f3d8359"
|
|
||||||
dependencies = [
|
|
||||||
"jiff-static",
|
|
||||||
"log",
|
|
||||||
"portable-atomic",
|
|
||||||
"portable-atomic-util",
|
|
||||||
"serde_core",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "jiff-static"
|
|
||||||
version = "0.2.23"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "2a8c8b344124222efd714b73bb41f8b5120b27a7cc1c75593a6ff768d9d05aa4"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn 2.0.117",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "js-sys"
|
name = "js-sys"
|
||||||
version = "0.3.91"
|
version = "0.3.91"
|
||||||
@@ -1387,7 +1317,7 @@ name = "mudserver"
|
|||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"crossterm 0.28.1",
|
"crossterm 0.28.1",
|
||||||
"env_logger",
|
"flexi_logger",
|
||||||
"log",
|
"log",
|
||||||
"rand",
|
"rand",
|
||||||
"ratatui",
|
"ratatui",
|
||||||
@@ -1423,6 +1353,15 @@ dependencies = [
|
|||||||
"minimal-lexical",
|
"minimal-lexical",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nu-ansi-term"
|
||||||
|
version = "0.50.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
|
||||||
|
dependencies = [
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-bigint"
|
name = "num-bigint"
|
||||||
version = "0.4.6"
|
version = "0.4.6"
|
||||||
@@ -1512,12 +1451,6 @@ version = "1.21.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "once_cell_polyfill"
|
|
||||||
version = "1.70.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "opaque-debug"
|
name = "opaque-debug"
|
||||||
version = "0.3.1"
|
version = "0.3.1"
|
||||||
@@ -1815,15 +1748,6 @@ version = "1.13.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49"
|
checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "portable-atomic-util"
|
|
||||||
version = "0.2.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "091397be61a01d4be58e7841595bd4bfedb15f1cd54977d79b8271e94ed799a3"
|
|
||||||
dependencies = [
|
|
||||||
"portable-atomic",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "powerfmt"
|
name = "powerfmt"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
|
|||||||
@@ -13,6 +13,6 @@ rusqlite = { version = "0.35", features = ["bundled"] }
|
|||||||
ratatui = "0.30"
|
ratatui = "0.30"
|
||||||
crossterm = "0.28"
|
crossterm = "0.28"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
env_logger = "0.11"
|
flexi_logger = { version = "0.29", features = ["async"] }
|
||||||
regex = "1"
|
regex = "1"
|
||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
- **Shops / economy** — NPCs that buy and sell; currency and pricing.
|
- **Shops / economy** — NPCs that buy and sell; currency and pricing.
|
||||||
- **Enhanced NPC Interactions** — Keyword-based dialogue system.
|
- **Enhanced NPC Interactions** — Keyword-based dialogue system.
|
||||||
- **Aggressive NPC AI** — NPCs with Aggressive attitude now correctly initiate combat.
|
- **Aggressive NPC AI** — NPCs with Aggressive attitude now correctly initiate combat.
|
||||||
|
- **Weather** — Weather system (e.g., rain, snow, fog) affecting areas or atmosphere.
|
||||||
|
- **Day/night or time of day** — Time cycle affecting room descriptions, spawns, or NPC behavior.
|
||||||
|
|
||||||
## Easy
|
## Easy
|
||||||
|
|
||||||
@@ -18,8 +20,7 @@ Content-only or minimal code; add TOML/data and existing systems already support
|
|||||||
|
|
||||||
New state, commands, or mechanics with bounded scope.
|
New state, commands, or mechanics with bounded scope.
|
||||||
|
|
||||||
- **Weather** — Weather system (e.g., rain, snow, fog) affecting areas or atmosphere; scope TBD.
|
- **Robust Logging** — Structured logging, rotation, and persistence; better visibility into server state and player actions.
|
||||||
- **Day/night or time of day** — Time cycle affecting room descriptions, spawns, or NPC behavior; lighter than full weather.
|
|
||||||
- **Quests or objectives** — Simple “kill X” / “bring Y” goals; quest state in DB and hooks in combat/loot/NPCs.
|
- **Quests or objectives** — Simple “kill X” / “bring Y” goals; quest state in DB and hooks in combat/loot/NPCs.
|
||||||
- **Player parties** — Group formation, shared objectives, party-only chat or visibility; new state and commands.
|
- **Player parties** — Group formation, shared objectives, party-only chat or visibility; new state and commands.
|
||||||
- **PvP** — Player-vs-player combat; consent/flagging, safe zones, and balance TBD.
|
- **PvP** — Player-vs-player combat; consent/flagging, safe zones, and balance TBD.
|
||||||
|
|||||||
96
TESTING.md
96
TESTING.md
@@ -6,9 +6,9 @@
|
|||||||
./run-tests.sh
|
./run-tests.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
This builds the server and mudtool, starts the server with a temporary DB, runs the smoke test below (new player, persistence, admin, registration gate, combat), then cleans up. Use it locally to match what Gitea Actions run on push/pull_request. The script uses `MUD_TEST_DB` (default `./mudserver.db.test`) so it does not overwrite your normal `mudserver.db`.
|
This builds the server and mudtool, starts the server with a temporary DB, and runs the same sequence as the smoke steps in [`.gitea/workflows/smoke-tests.yml`](.gitea/workflows/smoke-tests.yml) (new player, persistence, mudtool admin, in-game admin, registration gate, tick combat), then cleans up. Use `MUD_TEST_DB` (default `./mudserver.db.test`) so you do not overwrite your normal `mudserver.db`.
|
||||||
|
|
||||||
Prerequisites: Rust toolchain (cargo), ssh client. In CI, Rust is installed by the workflow.
|
Prerequisites: Rust toolchain (cargo), OpenSSH client, and OpenBSD `nc` (`netcat-openbsd` on Debian/Ubuntu) for [`scripts/ci/wait-for-tcp.sh`](scripts/ci/wait-for-tcp.sh). CI installs these explicitly before the smoke steps.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -113,6 +113,13 @@ Run through the checks below before every commit to ensure consistent feature co
|
|||||||
- [ ] Negative status effects cleared on player death/respawn
|
- [ ] Negative status effects cleared on player death/respawn
|
||||||
- [ ] Status effects on offline players resolve by wall-clock time on next login
|
- [ ] Status effects on offline players resolve by wall-clock time on next login
|
||||||
|
|
||||||
|
## Weather & Time
|
||||||
|
- [ ] Outdoor rooms display time of day (e.g., `[Night]`, `[Morning]`).
|
||||||
|
- [ ] Outdoor rooms display current weather (e.g., `The sky is clear`, `It is raining`).
|
||||||
|
- [ ] Indoor rooms do not show weather or time of day.
|
||||||
|
- [ ] Rain or storm applies the `wet` status effect to players in outdoor rooms.
|
||||||
|
- [ ] Weather changes periodically and broadcasts messages to players in outdoor rooms.
|
||||||
|
|
||||||
## Guilds
|
## Guilds
|
||||||
- [ ] `guild list` shows all available guilds with descriptions
|
- [ ] `guild list` shows all available guilds with descriptions
|
||||||
- [ ] `guild info <name>` shows guild details, growth stats, and spell list
|
- [ ] `guild info <name>` shows guild details, growth stats, and spell list
|
||||||
@@ -238,82 +245,13 @@ Run through the checks below before every commit to ensure consistent feature co
|
|||||||
- [ ] ←→ switches player on Attitudes tab
|
- [ ] ←→ switches player on Attitudes tab
|
||||||
- [ ] 'q' exits TUI
|
- [ ] 'q' exits TUI
|
||||||
|
|
||||||
|
## JSON-RPC Interface
|
||||||
|
- [ ] `list_commands` returns the currently handleable command list
|
||||||
|
- [ ] New commands added in `commands.rs` are automatically discovered
|
||||||
|
- [ ] `login` accepts an existing player name (requires character to be created first)
|
||||||
|
- [ ] Command output is stripped of ANSI color codes for API consumption
|
||||||
|
- [ ] Verify manually with: `echo '{"_jsonrpc": "2.0", "method": "list_commands", "params": {}, "id": 1}' | nc localhost 2223`
|
||||||
|
|
||||||
## Quick Smoke Test Script
|
## Quick Smoke Test Script
|
||||||
|
|
||||||
The canonical implementation is **`./run-tests.sh`** (see top of this file). The following is the same sequence for reference; when writing or extending tests, keep `run-tests.sh` and this section in sync.
|
**CI:** each scenario is a separate step in [`.gitea/workflows/smoke-tests.yml`](.gitea/workflows/smoke-tests.yml) (each SSH step starts `mudserver`, runs the block, stops it; the same `TEST_DB` file carries state between steps). The last step exercises JSON-RPC `login` / `list_commands` on port 2223 (expects `shop` in the command list). **Local:** **`./run-tests.sh`** runs the full sequence in one process. When you add or change coverage, update the workflow steps and `run-tests.sh` together, and keep the checklist sections above aligned.
|
||||||
|
|
||||||
```bash
|
|
||||||
# Start server in background (use -d for test DB so you don't overwrite mudserver.db)
|
|
||||||
TEST_DB=./mudserver.db.test
|
|
||||||
RUST_LOG=info ./target/debug/mudserver -d "$TEST_DB" &
|
|
||||||
SERVER_PID=$!
|
|
||||||
sleep 2
|
|
||||||
|
|
||||||
# Test 1: New player creation + basic commands
|
|
||||||
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p 2222 smoketest@localhost <<'EOF'
|
|
||||||
1
|
|
||||||
1
|
|
||||||
look
|
|
||||||
stats
|
|
||||||
go north
|
|
||||||
talk barkeep
|
|
||||||
go south
|
|
||||||
go south
|
|
||||||
examine thief
|
|
||||||
attack thief
|
|
||||||
flee
|
|
||||||
quit
|
|
||||||
EOF
|
|
||||||
|
|
||||||
# Test 2: Persistence - reconnect
|
|
||||||
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p 2222 smoketest@localhost <<'EOF'
|
|
||||||
look
|
|
||||||
stats
|
|
||||||
quit
|
|
||||||
EOF
|
|
||||||
|
|
||||||
# Test 3: Admin via mudtool (use same test DB)
|
|
||||||
./target/debug/mudtool -d "$TEST_DB" players list
|
|
||||||
./target/debug/mudtool -d "$TEST_DB" players set-admin smoketest true
|
|
||||||
./target/debug/mudtool -d "$TEST_DB" players show smoketest
|
|
||||||
./target/debug/mudtool -d "$TEST_DB" settings set registration_open false
|
|
||||||
./target/debug/mudtool -d "$TEST_DB" settings list
|
|
||||||
|
|
||||||
# Test 4: Admin commands in-game
|
|
||||||
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p 2222 smoketest@localhost <<'EOF'
|
|
||||||
admin help
|
|
||||||
admin list
|
|
||||||
admin registration on
|
|
||||||
admin info smoketest
|
|
||||||
quit
|
|
||||||
EOF
|
|
||||||
|
|
||||||
# Test 5: Registration gate
|
|
||||||
./target/debug/mudtool -d "$TEST_DB" settings set registration_open false
|
|
||||||
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p 2222 newplayer@localhost <<'EOF'
|
|
||||||
quit
|
|
||||||
EOF
|
|
||||||
|
|
||||||
# Test 6: Tick-based combat (connect and wait for ticks)
|
|
||||||
./target/debug/mudtool -d "$TEST_DB" settings set registration_open true
|
|
||||||
./target/debug/mudtool -d "$TEST_DB" players delete smoketest
|
|
||||||
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p 2222 smoketest@localhost <<'EOF'
|
|
||||||
1
|
|
||||||
1
|
|
||||||
go south
|
|
||||||
go south
|
|
||||||
attack thief
|
|
||||||
EOF
|
|
||||||
# Wait for several combat ticks to resolve
|
|
||||||
sleep 8
|
|
||||||
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p 2222 smoketest@localhost <<'EOF'
|
|
||||||
stats
|
|
||||||
quit
|
|
||||||
EOF
|
|
||||||
# Verify XP changed (combat happened via ticks)
|
|
||||||
|
|
||||||
# Cleanup
|
|
||||||
./target/debug/mudtool -d "$TEST_DB" settings set registration_open true
|
|
||||||
./target/debug/mudtool -d "$TEST_DB" players delete smoketest
|
|
||||||
kill $SERVER_PID
|
|
||||||
```
|
|
||||||
|
|||||||
50
run-tests.sh
50
run-tests.sh
@@ -1,10 +1,12 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -ex
|
set -ex
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
cd "$ROOT"
|
||||||
|
|
||||||
TEST_DB=${MUD_TEST_DB:-./mudserver.db.test}
|
TEST_DB=${MUD_TEST_DB:-./mudserver.db.test}
|
||||||
SERVER_PID=
|
SERVER_PID=
|
||||||
|
|
||||||
# SSH returns 255 when MUD closes connection after quit — treat as success
|
|
||||||
ssh_mud() {
|
ssh_mud() {
|
||||||
set +e
|
set +e
|
||||||
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p 2222 "$@"
|
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p 2222 "$@"
|
||||||
@@ -23,9 +25,8 @@ trap cleanup EXIT
|
|||||||
cargo build
|
cargo build
|
||||||
RUST_LOG=info ./target/debug/mudserver -d "$TEST_DB" &
|
RUST_LOG=info ./target/debug/mudserver -d "$TEST_DB" &
|
||||||
SERVER_PID=$!
|
SERVER_PID=$!
|
||||||
sleep 2
|
bash "$ROOT/scripts/ci/wait-for-tcp.sh" 127.0.0.1 2222
|
||||||
|
|
||||||
# Test 1: New player creation + basic commands
|
|
||||||
ssh_mud smoketest@localhost <<'EOF'
|
ssh_mud smoketest@localhost <<'EOF'
|
||||||
1
|
1
|
||||||
1
|
1
|
||||||
@@ -43,21 +44,37 @@ flee
|
|||||||
quit
|
quit
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
# Test 2: Persistence - reconnect
|
ssh_mud smoketest@localhost <<'EOF' > weather_test.out
|
||||||
|
go south
|
||||||
|
go down
|
||||||
|
look
|
||||||
|
quit
|
||||||
|
EOF
|
||||||
|
|
||||||
|
if ! grep -q "The sky is\|raining\|storm\|snow\|fog" weather_test.out; then
|
||||||
|
echo "Error: Weather info not found in look output"
|
||||||
|
cat weather_test.out
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if ! grep -q "\[Night\]\|\[Morning\]\|\[Afternoon\]\|\[Evening\]" weather_test.out; then
|
||||||
|
echo "Error: Time of day info not found in look output"
|
||||||
|
cat weather_test.out
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
rm weather_test.out
|
||||||
|
|
||||||
ssh_mud smoketest@localhost <<'EOF'
|
ssh_mud smoketest@localhost <<'EOF'
|
||||||
look
|
look
|
||||||
stats
|
stats
|
||||||
quit
|
quit
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
# Test 3: Admin via mudtool (use same test DB)
|
|
||||||
./target/debug/mudtool -d "$TEST_DB" players list
|
./target/debug/mudtool -d "$TEST_DB" players list
|
||||||
./target/debug/mudtool -d "$TEST_DB" players set-admin smoketest true
|
./target/debug/mudtool -d "$TEST_DB" players set-admin smoketest true
|
||||||
./target/debug/mudtool -d "$TEST_DB" players show smoketest
|
./target/debug/mudtool -d "$TEST_DB" players show smoketest
|
||||||
./target/debug/mudtool -d "$TEST_DB" settings set registration_open false
|
./target/debug/mudtool -d "$TEST_DB" settings set registration_open false
|
||||||
./target/debug/mudtool -d "$TEST_DB" settings list
|
./target/debug/mudtool -d "$TEST_DB" settings list
|
||||||
|
|
||||||
# Test 4: Admin commands in-game
|
|
||||||
ssh_mud smoketest@localhost <<'EOF'
|
ssh_mud smoketest@localhost <<'EOF'
|
||||||
admin help
|
admin help
|
||||||
admin list
|
admin list
|
||||||
@@ -66,16 +83,13 @@ admin info smoketest
|
|||||||
quit
|
quit
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
# Test 5: Registration gate
|
|
||||||
./target/debug/mudtool -d "$TEST_DB" settings set registration_open false
|
./target/debug/mudtool -d "$TEST_DB" settings set registration_open false
|
||||||
ssh_mud newplayer@localhost <<'EOF'
|
ssh_mud newplayer@localhost <<'EOF'
|
||||||
quit
|
quit
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
# Test 6: Tick-based combat (connect and wait for ticks)
|
|
||||||
./target/debug/mudtool -d "$TEST_DB" settings set registration_open true
|
./target/debug/mudtool -d "$TEST_DB" settings set registration_open true
|
||||||
./target/debug/mudtool -d "$TEST_DB" players delete smoketest
|
./target/debug/mudtool -d "$TEST_DB" players delete smoketest
|
||||||
# Use subshell to pipe commands with a delay between them while staying connected
|
|
||||||
(
|
(
|
||||||
echo "1"
|
echo "1"
|
||||||
echo "1"
|
echo "1"
|
||||||
@@ -88,6 +102,22 @@ EOF
|
|||||||
echo "quit"
|
echo "quit"
|
||||||
) | ssh_mud smoketest@localhost
|
) | ssh_mud smoketest@localhost
|
||||||
|
|
||||||
# Cleanup (trap handles server kill)
|
ssh_mud rpctest@localhost <<'EOF'
|
||||||
|
1
|
||||||
|
1
|
||||||
|
quit
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo '{"_jsonrpc": "2.0", "method": "login", "params": {"username": "rpctest"}, "id": 1}' | nc -w 2 localhost 2223 > rpc_resp.json
|
||||||
|
echo '{"_jsonrpc": "2.0", "method": "list_commands", "params": {}, "id": 2}' | nc -w 2 localhost 2223 >> rpc_resp.json
|
||||||
|
|
||||||
|
if ! grep -q '"shop"' rpc_resp.json; then
|
||||||
|
echo "Error: 'shop' command missing from JSON-RPC list_commands"
|
||||||
|
cat rpc_resp.json
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
rm rpc_resp.json
|
||||||
|
|
||||||
./target/debug/mudtool -d "$TEST_DB" settings set registration_open true
|
./target/debug/mudtool -d "$TEST_DB" settings set registration_open true
|
||||||
./target/debug/mudtool -d "$TEST_DB" players delete smoketest
|
./target/debug/mudtool -d "$TEST_DB" players delete smoketest
|
||||||
|
./target/debug/mudtool -d "$TEST_DB" players delete rpctest
|
||||||
|
|||||||
13
scripts/ci/wait-for-tcp.sh
Executable file
13
scripts/ci/wait-for-tcp.sh
Executable file
@@ -0,0 +1,13 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -e
|
||||||
|
host=${1:-127.0.0.1}
|
||||||
|
port=${2:-2222}
|
||||||
|
max_attempts=${3:-30}
|
||||||
|
for _ in $(seq 1 "$max_attempts"); do
|
||||||
|
if nc -z -w 1 "$host" "$port" 2>/dev/null; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
echo "timeout waiting for $host:$port" >&2
|
||||||
|
exit 1
|
||||||
@@ -74,6 +74,7 @@ async fn admin_promote(target: &str, state: &SharedState) -> CommandResult {
|
|||||||
if target.is_empty() {
|
if target.is_empty() {
|
||||||
return simple(&format!("{}\r\n", ansi::error_msg("Usage: admin promote <player>")));
|
return simple(&format!("{}\r\n", ansi::error_msg("Usage: admin promote <player>")));
|
||||||
}
|
}
|
||||||
|
log::info!("Admin action: promote player '{}'", target);
|
||||||
let st = state.lock().await;
|
let st = state.lock().await;
|
||||||
if st.db.set_admin(target, true) {
|
if st.db.set_admin(target, true) {
|
||||||
// Also update in-memory if online
|
// Also update in-memory if online
|
||||||
@@ -124,6 +125,7 @@ async fn admin_demote(target: &str, state: &SharedState) -> CommandResult {
|
|||||||
if target.is_empty() {
|
if target.is_empty() {
|
||||||
return simple(&format!("{}\r\n", ansi::error_msg("Usage: admin demote <player>")));
|
return simple(&format!("{}\r\n", ansi::error_msg("Usage: admin demote <player>")));
|
||||||
}
|
}
|
||||||
|
log::info!("Admin action: demote player '{}'", target);
|
||||||
let st = state.lock().await;
|
let st = state.lock().await;
|
||||||
if st.db.set_admin(target, false) {
|
if st.db.set_admin(target, false) {
|
||||||
simple(&format!(
|
simple(&format!(
|
||||||
@@ -142,6 +144,7 @@ async fn admin_kick(target: &str, player_id: usize, state: &SharedState) -> Comm
|
|||||||
if target.is_empty() {
|
if target.is_empty() {
|
||||||
return simple(&format!("{}\r\n", ansi::error_msg("Usage: admin kick <player>")));
|
return simple(&format!("{}\r\n", ansi::error_msg("Usage: admin kick <player>")));
|
||||||
}
|
}
|
||||||
|
log::info!("Admin action: kick player '{}'", target);
|
||||||
let mut st = state.lock().await;
|
let mut st = state.lock().await;
|
||||||
let low = target.to_lowercase();
|
let low = target.to_lowercase();
|
||||||
|
|
||||||
@@ -237,6 +240,7 @@ async fn admin_teleport(room_id: &str, player_id: usize, state: &SharedState) ->
|
|||||||
ansi::error_msg("Usage: admin teleport <room_id>")
|
ansi::error_msg("Usage: admin teleport <room_id>")
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
log::info!("Admin action: teleport player ID {} to '{}'", player_id, room_id);
|
||||||
let mut st = state.lock().await;
|
let mut st = state.lock().await;
|
||||||
if st.world.get_room(room_id).is_none() {
|
if st.world.get_room(room_id).is_none() {
|
||||||
let rooms: Vec<&String> = st.world.rooms.keys().collect();
|
let rooms: Vec<&String> = st.world.rooms.keys().collect();
|
||||||
@@ -329,6 +333,7 @@ async fn admin_teleport(room_id: &str, player_id: usize, state: &SharedState) ->
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn admin_registration(args: &str, state: &SharedState) -> CommandResult {
|
async fn admin_registration(args: &str, state: &SharedState) -> CommandResult {
|
||||||
|
log::info!("Admin action: registration setting updated: '{}'", args);
|
||||||
let st = state.lock().await;
|
let st = state.lock().await;
|
||||||
match args.to_lowercase().as_str() {
|
match args.to_lowercase().as_str() {
|
||||||
"on" | "true" | "open" => {
|
"on" | "true" | "open" => {
|
||||||
@@ -365,6 +370,7 @@ async fn admin_announce(msg: &str, player_id: usize, state: &SharedState) -> Com
|
|||||||
ansi::error_msg("Usage: admin announce <message>")
|
ansi::error_msg("Usage: admin announce <message>")
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
log::info!("Admin action: announcement by player ID {}: '{}'", player_id, msg);
|
||||||
let st = state.lock().await;
|
let st = state.lock().await;
|
||||||
let announcement = CryptoVec::from(
|
let announcement = CryptoVec::from(
|
||||||
format!(
|
format!(
|
||||||
@@ -405,6 +411,7 @@ async fn admin_announce(msg: &str, player_id: usize, state: &SharedState) -> Com
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn admin_heal(args: &str, player_id: usize, state: &SharedState) -> CommandResult {
|
async fn admin_heal(args: &str, player_id: usize, state: &SharedState) -> CommandResult {
|
||||||
|
log::info!("Admin action: heal player '{}' (empty means self)", args);
|
||||||
let mut st = state.lock().await;
|
let mut st = state.lock().await;
|
||||||
|
|
||||||
if args.is_empty() {
|
if args.is_empty() {
|
||||||
@@ -564,6 +571,7 @@ async fn admin_info(target: &str, state: &SharedState) -> CommandResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn admin_setattitude(args: &str, state: &SharedState) -> CommandResult {
|
async fn admin_setattitude(args: &str, state: &SharedState) -> CommandResult {
|
||||||
|
log::info!("Admin action: setattitude '{}'", args);
|
||||||
let parts: Vec<&str> = args.splitn(3, ' ').collect();
|
let parts: Vec<&str> = args.splitn(3, ' ').collect();
|
||||||
if parts.len() < 3 {
|
if parts.len() < 3 {
|
||||||
return simple(&format!(
|
return simple(&format!(
|
||||||
|
|||||||
@@ -69,6 +69,8 @@ pub fn resolve_combat_tick(
|
|||||||
));
|
));
|
||||||
|
|
||||||
if new_npc_hp <= 0 {
|
if new_npc_hp <= 0 {
|
||||||
|
let player_name = state.players.get(&player_id).map(|c| c.player.name.clone()).unwrap_or_else(|| "Unknown".into());
|
||||||
|
log::info!(target: "{combat}", "Combat: Player '{}' (ID {}) killed NPC '{}' ({})", player_name, player_id, npc_template.name, npc_id);
|
||||||
if let Some(inst) = state.npc_instances.get_mut(&npc_id) {
|
if let Some(inst) = state.npc_instances.get_mut(&npc_id) {
|
||||||
inst.alive = false;
|
inst.alive = false;
|
||||||
inst.hp = 0;
|
inst.hp = 0;
|
||||||
@@ -351,7 +353,9 @@ pub fn player_death_respawn(player_id: usize, state: &mut GameState) -> String {
|
|||||||
.players
|
.players
|
||||||
.get(&player_id)
|
.get(&player_id)
|
||||||
.map(|c| c.player.name.clone())
|
.map(|c| c.player.name.clone())
|
||||||
.unwrap_or_default();
|
.unwrap_or_else(|| "Unknown".into());
|
||||||
|
|
||||||
|
log::info!(target: "{combat}", "Combat: Player '{}' (ID {}) died and respawned at {}", player_name, player_id, spawn_room);
|
||||||
|
|
||||||
if let Some(conn) = state.players.get_mut(&player_id) {
|
if let Some(conn) = state.players.get_mut(&player_id) {
|
||||||
conn.player.stats.hp = conn.player.stats.max_hp;
|
conn.player.stats.hp = conn.player.stats.max_hp;
|
||||||
|
|||||||
@@ -157,6 +157,15 @@ pub async fn execute(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_command_list() -> Vec<&'static str> {
|
||||||
|
vec![
|
||||||
|
"look", "go", "north", "south", "east", "west", "up", "down",
|
||||||
|
"say", "who", "take", "drop", "inventory", "equip", "use",
|
||||||
|
"examine", "talk", "attack", "defend", "flee", "cast",
|
||||||
|
"spells", "skills", "guild", "stats", "help", "shop",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
fn send(session: &mut Session, channel: ChannelId, text: &str) -> Result<(), russh::Error> {
|
fn send(session: &mut Session, channel: ChannelId, text: &str) -> Result<(), russh::Error> {
|
||||||
session.data(channel, CryptoVec::from(text.as_bytes()))?;
|
session.data(channel, CryptoVec::from(text.as_bytes()))?;
|
||||||
@@ -171,6 +180,14 @@ fn attitude_color(att: Attitude) -> &'static str {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_time_of_day(tick: u64) -> &'static str {
|
||||||
|
let day_tick = tick % 1440;
|
||||||
|
if day_tick < 360 { "Night" }
|
||||||
|
else if day_tick < 720 { "Morning" }
|
||||||
|
else if day_tick < 1080 { "Afternoon" }
|
||||||
|
else { "Evening" }
|
||||||
|
}
|
||||||
|
|
||||||
pub fn render_room_view(
|
pub fn render_room_view(
|
||||||
room_id: &str,
|
room_id: &str,
|
||||||
player_id: usize,
|
player_id: usize,
|
||||||
@@ -186,13 +203,19 @@ pub fn render_room_view(
|
|||||||
.map(|c| c.player.name.as_str())
|
.map(|c| c.player.name.as_str())
|
||||||
.unwrap_or("");
|
.unwrap_or("");
|
||||||
|
|
||||||
|
let time_of_day = get_time_of_day(st.tick_count);
|
||||||
let mut out = format!(
|
let mut out = format!(
|
||||||
"\r\n{} {}\r\n {}\r\n",
|
"\r\n{} {} {}\r\n {}\r\n",
|
||||||
ansi::room_name(&room.name),
|
ansi::room_name(&room.name),
|
||||||
ansi::system_msg(&format!("[{}]", room.region)),
|
ansi::system_msg(&format!("[{}]", room.region)),
|
||||||
|
ansi::color(ansi::YELLOW, &format!("[{}]", time_of_day)),
|
||||||
room.description
|
room.description
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if room.outdoors {
|
||||||
|
out.push_str(&format!(" {}\r\n", ansi::color(ansi::CYAN, st.weather.kind.description())));
|
||||||
|
}
|
||||||
|
|
||||||
let npc_strs: Vec<String> = room
|
let npc_strs: Vec<String> = room
|
||||||
.npcs
|
.npcs
|
||||||
.iter()
|
.iter()
|
||||||
@@ -1213,6 +1236,8 @@ async fn cmd_attack(pid: usize, target: &str, state: &SharedState) -> CommandRes
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log::info!(target: "{combat}", "Combat: Player '{}' (ID {}) engaged NPC '{}' ({}) in combat", pname, pid, npc_name, npc_id);
|
||||||
|
|
||||||
CommandResult {
|
CommandResult {
|
||||||
output: format!(
|
output: format!(
|
||||||
"{}\r\n{}\r\n{}",
|
"{}\r\n{}\r\n{}",
|
||||||
|
|||||||
34
src/game.rs
34
src/game.rs
@@ -9,6 +9,35 @@ use russh::ChannelId;
|
|||||||
use crate::db::{GameDb, SavedPlayer};
|
use crate::db::{GameDb, SavedPlayer};
|
||||||
use crate::world::{Attitude, Class, Object, Race, World};
|
use crate::world::{Attitude, Class, Object, Race, World};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum WeatherKind {
|
||||||
|
Clear,
|
||||||
|
Cloudy,
|
||||||
|
Rain,
|
||||||
|
Storm,
|
||||||
|
Snow,
|
||||||
|
Fog,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WeatherKind {
|
||||||
|
pub fn description(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
WeatherKind::Clear => "The sky is clear.",
|
||||||
|
WeatherKind::Cloudy => "The sky is overcast with clouds.",
|
||||||
|
WeatherKind::Rain => "It is raining.",
|
||||||
|
WeatherKind::Storm => "A powerful storm is raging.",
|
||||||
|
WeatherKind::Snow => "Soft snowflakes are falling from the sky.",
|
||||||
|
WeatherKind::Fog => "A thick fog blankets the area.",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct WeatherState {
|
||||||
|
pub kind: WeatherKind,
|
||||||
|
pub remaining_ticks: u32,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct PlayerStats {
|
pub struct PlayerStats {
|
||||||
pub max_hp: i32,
|
pub max_hp: i32,
|
||||||
@@ -142,6 +171,7 @@ pub struct GameState {
|
|||||||
pub npc_instances: HashMap<String, NpcInstance>,
|
pub npc_instances: HashMap<String, NpcInstance>,
|
||||||
pub rng: XorShift64,
|
pub rng: XorShift64,
|
||||||
pub tick_count: u64,
|
pub tick_count: u64,
|
||||||
|
pub weather: WeatherState,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type SharedState = Arc<Mutex<GameState>>;
|
pub type SharedState = Arc<Mutex<GameState>>;
|
||||||
@@ -226,6 +256,10 @@ impl GameState {
|
|||||||
npc_instances,
|
npc_instances,
|
||||||
rng,
|
rng,
|
||||||
tick_count: 0,
|
tick_count: 0,
|
||||||
|
weather: WeatherState {
|
||||||
|
kind: WeatherKind::Clear,
|
||||||
|
remaining_ticks: 100,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -126,12 +126,7 @@ async fn handle_request(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"list_commands" => {
|
"list_commands" => {
|
||||||
json!([
|
json!(commands::get_command_list())
|
||||||
"look", "go", "north", "south", "east", "west", "up", "down",
|
|
||||||
"say", "who", "take", "drop", "inventory", "equip", "use",
|
|
||||||
"examine", "talk", "attack", "defend", "flee", "cast",
|
|
||||||
"spells", "skills", "guild", "stats", "help"
|
|
||||||
])
|
|
||||||
},
|
},
|
||||||
"execute" => {
|
"execute" => {
|
||||||
if let Some(pid) = *current_player_id {
|
if let Some(pid) = *current_player_id {
|
||||||
|
|||||||
52
src/main.rs
52
src/main.rs
@@ -2,6 +2,8 @@ use std::path::PathBuf;
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
|
use flexi_logger::writers::FileLogWriter;
|
||||||
|
use flexi_logger::{Cleanup, Criterion, Duplicate, FileSpec, Logger, Naming, WriteMode};
|
||||||
use russh::keys::ssh_key::rand_core::OsRng;
|
use russh::keys::ssh_key::rand_core::OsRng;
|
||||||
use russh::server::Server as _;
|
use russh::server::Server as _;
|
||||||
use tokio::net::TcpListener;
|
use tokio::net::TcpListener;
|
||||||
@@ -18,12 +20,12 @@ const DEFAULT_DB_PATH: &str = "./mudserver.db";
|
|||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
|
|
||||||
|
|
||||||
let mut port = DEFAULT_PORT;
|
let mut port = DEFAULT_PORT;
|
||||||
let mut jsonrpc_port = 2223;
|
let mut jsonrpc_port = 2223;
|
||||||
let mut world_dir = PathBuf::from(DEFAULT_WORLD_DIR);
|
let mut world_dir = PathBuf::from(DEFAULT_WORLD_DIR);
|
||||||
let mut db_path = PathBuf::from(DEFAULT_DB_PATH);
|
let mut db_path = PathBuf::from(DEFAULT_DB_PATH);
|
||||||
|
let mut log_dir = "logs".to_string();
|
||||||
|
let mut log_level = "info".to_string();
|
||||||
|
|
||||||
let args: Vec<String> = std::env::args().collect();
|
let args: Vec<String> = std::env::args().collect();
|
||||||
let mut i = 1;
|
let mut i = 1;
|
||||||
@@ -51,12 +53,22 @@ async fn main() {
|
|||||||
i += 1;
|
i += 1;
|
||||||
db_path = PathBuf::from(args.get(i).expect("--db requires a path"));
|
db_path = PathBuf::from(args.get(i).expect("--db requires a path"));
|
||||||
}
|
}
|
||||||
|
"--log-dir" => {
|
||||||
|
i += 1;
|
||||||
|
log_dir = args.get(i).expect("--log-dir requires a path").to_string();
|
||||||
|
}
|
||||||
|
"--log-level" => {
|
||||||
|
i += 1;
|
||||||
|
log_level = args.get(i).expect("--log-level requires a level").to_string();
|
||||||
|
}
|
||||||
"--help" => {
|
"--help" => {
|
||||||
eprintln!("Usage: mudserver [OPTIONS]");
|
eprintln!("Usage: mudserver [OPTIONS]");
|
||||||
eprintln!(" --port, -p SSH listen port (default: {DEFAULT_PORT})");
|
eprintln!(" --port, -p SSH listen port (default: {DEFAULT_PORT})");
|
||||||
eprintln!(" --rpc-port JSON-RPC listen port (default: 2223)");
|
eprintln!(" --rpc-port JSON-RPC listen port (default: 2223)");
|
||||||
eprintln!(" --world, -w World directory (default: {DEFAULT_WORLD_DIR})");
|
eprintln!(" --world, -w World directory (default: {DEFAULT_WORLD_DIR})");
|
||||||
eprintln!(" --db, -d Database path (default: {DEFAULT_DB_PATH})");
|
eprintln!(" --db, -d Database path (default: {DEFAULT_DB_PATH})");
|
||||||
|
eprintln!(" --log-dir Directory for log files (default: logs)");
|
||||||
|
eprintln!(" --log-level Logging level (default: info)");
|
||||||
std::process::exit(0);
|
std::process::exit(0);
|
||||||
}
|
}
|
||||||
other => {
|
other => {
|
||||||
@@ -67,6 +79,42 @@ async fn main() {
|
|||||||
i += 1;
|
i += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure log directory exists
|
||||||
|
std::fs::create_dir_all(&log_dir).unwrap_or_else(|e| {
|
||||||
|
eprintln!("Failed to create log directory: {e}");
|
||||||
|
std::process::exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize logger
|
||||||
|
let combat_writer = FileLogWriter::builder(FileSpec::default().directory(&log_dir).basename("combat"))
|
||||||
|
.rotate(
|
||||||
|
Criterion::Size(10_000_000), // 10 MB
|
||||||
|
Naming::Numbers,
|
||||||
|
Cleanup::KeepLogFiles(7),
|
||||||
|
)
|
||||||
|
.append()
|
||||||
|
.write_mode(WriteMode::Direct)
|
||||||
|
.try_build()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
Logger::try_with_str(&log_level)
|
||||||
|
.unwrap()
|
||||||
|
.log_to_file(FileSpec::default().directory(&log_dir).basename("mudserver"))
|
||||||
|
.append()
|
||||||
|
.duplicate_to_stderr(Duplicate::All)
|
||||||
|
.rotate(
|
||||||
|
Criterion::Size(10_000_000), // 10 MB
|
||||||
|
Naming::Numbers,
|
||||||
|
Cleanup::KeepLogFiles(7),
|
||||||
|
)
|
||||||
|
.write_mode(WriteMode::Direct)
|
||||||
|
.add_writer("combat", Box::new(combat_writer))
|
||||||
|
.start()
|
||||||
|
.unwrap_or_else(|e| {
|
||||||
|
eprintln!("Failed to initialize logger: {e}");
|
||||||
|
std::process::exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
log::info!("Loading world from: {}", world_dir.display());
|
log::info!("Loading world from: {}", world_dir.display());
|
||||||
let loaded_world = world::World::load(&world_dir).unwrap_or_else(|e| {
|
let loaded_world = world::World::load(&world_dir).unwrap_or_else(|e| {
|
||||||
eprintln!("Failed to load world: {e}");
|
eprintln!("Failed to load world: {e}");
|
||||||
|
|||||||
@@ -83,6 +83,8 @@ impl MudHandler {
|
|||||||
state.load_existing_player(self.id, saved, Some(channel), Some(handle));
|
state.load_existing_player(self.id, saved, Some(channel), Some(handle));
|
||||||
drop(state);
|
drop(state);
|
||||||
|
|
||||||
|
log::info!("Player '{}' (id={}) logged in", self.username, self.id);
|
||||||
|
|
||||||
let msg = format!(
|
let msg = format!(
|
||||||
"{}\r\n",
|
"{}\r\n",
|
||||||
ansi::system_msg("Welcome back! Your character has been restored.")
|
ansi::system_msg("Welcome back! Your character has been restored.")
|
||||||
@@ -171,6 +173,13 @@ impl MudHandler {
|
|||||||
.map(|c| c.name.clone())
|
.map(|c| c.name.clone())
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
log::info!(
|
||||||
|
"New character created: {} (Race: {}, Class: {})",
|
||||||
|
self.username,
|
||||||
|
race_name,
|
||||||
|
class_name
|
||||||
|
);
|
||||||
|
|
||||||
state.create_new_player(
|
state.create_new_player(
|
||||||
self.id,
|
self.id,
|
||||||
self.username.clone(),
|
self.username.clone(),
|
||||||
|
|||||||
70
src/tick.rs
70
src/tick.rs
@@ -26,6 +26,48 @@ pub async fn run_tick_engine(state: SharedState) {
|
|||||||
st.tick_count += 1;
|
st.tick_count += 1;
|
||||||
let tick = st.tick_count;
|
let tick = st.tick_count;
|
||||||
|
|
||||||
|
let mut weather_msg = None;
|
||||||
|
st.weather.remaining_ticks = st.weather.remaining_ticks.saturating_sub(1);
|
||||||
|
if st.weather.remaining_ticks == 0 {
|
||||||
|
let old_kind = st.weather.kind;
|
||||||
|
st.weather.kind = match st.rng.next_range(0, 5) {
|
||||||
|
0 => crate::game::WeatherKind::Clear,
|
||||||
|
1 => crate::game::WeatherKind::Cloudy,
|
||||||
|
2 => crate::game::WeatherKind::Rain,
|
||||||
|
3 => crate::game::WeatherKind::Storm,
|
||||||
|
4 => crate::game::WeatherKind::Snow,
|
||||||
|
5 => crate::game::WeatherKind::Fog,
|
||||||
|
_ => crate::game::WeatherKind::Clear,
|
||||||
|
};
|
||||||
|
st.weather.remaining_ticks = st.rng.next_range(100, 400) as u32;
|
||||||
|
|
||||||
|
if old_kind != st.weather.kind {
|
||||||
|
weather_msg = Some(format!(
|
||||||
|
"\r\n {}\r\n",
|
||||||
|
ansi::color(ansi::CYAN, st.weather.kind.description())
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply "wet" effect if raining/storming
|
||||||
|
if st.weather.kind == crate::game::WeatherKind::Rain
|
||||||
|
|| st.weather.kind == crate::game::WeatherKind::Storm
|
||||||
|
{
|
||||||
|
let wet_players: Vec<String> = st
|
||||||
|
.players
|
||||||
|
.values()
|
||||||
|
.filter_map(|c| {
|
||||||
|
st.world
|
||||||
|
.get_room(&c.player.room_id)
|
||||||
|
.filter(|r| r.outdoors)
|
||||||
|
.map(|_| c.player.name.clone())
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
for name in wet_players {
|
||||||
|
st.db.save_effect(&name, "wet", 10, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
st.check_respawns();
|
st.check_respawns();
|
||||||
|
|
||||||
// --- NPC auto-aggro: hostile NPCs initiate combat with players in their room ---
|
// --- NPC auto-aggro: hostile NPCs initiate combat with players in their room ---
|
||||||
@@ -60,6 +102,16 @@ pub async fn run_tick_engine(state: SharedState) {
|
|||||||
|
|
||||||
let mut messages: HashMap<usize, String> = HashMap::new();
|
let mut messages: HashMap<usize, String> = HashMap::new();
|
||||||
|
|
||||||
|
if let Some(msg) = weather_msg {
|
||||||
|
for (&pid, conn) in st.players.iter() {
|
||||||
|
if let Some(room) = st.world.get_room(&conn.player.room_id) {
|
||||||
|
if room.outdoors {
|
||||||
|
messages.entry(pid).or_default().push_str(&msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (pid, npc_id) in &new_combats {
|
for (pid, npc_id) in &new_combats {
|
||||||
let npc_name = st
|
let npc_name = st
|
||||||
.world
|
.world
|
||||||
@@ -183,6 +235,24 @@ pub async fn run_tick_engine(state: SharedState) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
"wet" => {
|
||||||
|
let online_pid = st
|
||||||
|
.players
|
||||||
|
.iter()
|
||||||
|
.find(|(_, c)| c.player.name == eff.player_name)
|
||||||
|
.map(|(&id, _)| id);
|
||||||
|
|
||||||
|
if let Some(pid) = online_pid {
|
||||||
|
if let Some(_conn) = st.players.get_mut(&pid) {
|
||||||
|
if eff.remaining_ticks <= 0 {
|
||||||
|
messages.entry(pid).or_default().push_str(&format!(
|
||||||
|
"\r\n {} You dry off.\r\n",
|
||||||
|
ansi::color(ansi::CYAN, "~~"),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
"regen" => {
|
"regen" => {
|
||||||
let heal = eff.magnitude;
|
let heal = eff.magnitude;
|
||||||
let online_pid = st
|
let online_pid = st
|
||||||
|
|||||||
@@ -77,6 +77,8 @@ pub struct RoomFile {
|
|||||||
pub description: String,
|
pub description: String,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub exits: HashMap<String, String>,
|
pub exits: HashMap<String, String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub outdoors: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Clone)]
|
#[derive(Deserialize, Clone)]
|
||||||
@@ -413,6 +415,7 @@ pub struct Room {
|
|||||||
pub exits: HashMap<String, String>,
|
pub exits: HashMap<String, String>,
|
||||||
pub npcs: Vec<String>,
|
pub npcs: Vec<String>,
|
||||||
pub objects: Vec<String>,
|
pub objects: Vec<String>,
|
||||||
|
pub outdoors: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@@ -665,7 +668,7 @@ impl World {
|
|||||||
|
|
||||||
load_entities_from_dir(®ion_path.join("rooms"), ®ion_name, &mut |id, content| {
|
load_entities_from_dir(®ion_path.join("rooms"), ®ion_name, &mut |id, content| {
|
||||||
let rf: RoomFile = toml::from_str(content).map_err(|e| format!("Bad room {id}: {e}"))?;
|
let rf: RoomFile = toml::from_str(content).map_err(|e| format!("Bad room {id}: {e}"))?;
|
||||||
rooms.insert(id.clone(), Room { id: id.clone(), region: region_name.clone(), name: rf.name, description: rf.description, exits: rf.exits, npcs: Vec::new(), objects: Vec::new() });
|
rooms.insert(id.clone(), Room { id: id.clone(), region: region_name.clone(), name: rf.name, description: rf.description, exits: rf.exits, npcs: Vec::new(), objects: Vec::new(), outdoors: rf.outdoors });
|
||||||
Ok(())
|
Ok(())
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ description = """\
|
|||||||
Towering iron-reinforced wooden gates mark the southern edge of town. \
|
Towering iron-reinforced wooden gates mark the southern edge of town. \
|
||||||
Guards in dented armor lean on their spears, watching the dusty road \
|
Guards in dented armor lean on their spears, watching the dusty road \
|
||||||
that stretches into the wilderness beyond."""
|
that stretches into the wilderness beyond."""
|
||||||
|
outdoors = true
|
||||||
|
|
||||||
[exits]
|
[exits]
|
||||||
north = "town:dark_alley"
|
north = "town:dark_alley"
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ description = """\
|
|||||||
Colorful stalls line both sides of a narrow street. Merchants hawk their \
|
Colorful stalls line both sides of a narrow street. Merchants hawk their \
|
||||||
wares — bolts of cloth, exotic spices, gleaming trinkets. The air is thick \
|
wares — bolts of cloth, exotic spices, gleaming trinkets. The air is thick \
|
||||||
with competing smells and the chatter of commerce."""
|
with competing smells and the chatter of commerce."""
|
||||||
|
outdoors = true
|
||||||
|
|
||||||
[exits]
|
[exits]
|
||||||
west = "town:town_square"
|
west = "town:town_square"
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ description = """\
|
|||||||
You stand in the heart of Thornwall. A worn stone fountain sits at the \
|
You stand in the heart of Thornwall. A worn stone fountain sits at the \
|
||||||
center, water trickling quietly. Cobblestone paths branch in every \
|
center, water trickling quietly. Cobblestone paths branch in every \
|
||||||
direction. The sounds of merchants and travelers fill the air."""
|
direction. The sounds of merchants and travelers fill the air."""
|
||||||
|
outdoors = true
|
||||||
|
|
||||||
[exits]
|
[exits]
|
||||||
north = "town:tavern"
|
north = "town:tavern"
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ The well-maintained cobblestone of Thornwall yields to a winding dirt \
|
|||||||
path that disappears into the dense, dark eaves of the forest. The air \
|
path that disappears into the dense, dark eaves of the forest. The air \
|
||||||
is cooler here, smelling of damp earth and pine needles. The city \
|
is cooler here, smelling of damp earth and pine needles. The city \
|
||||||
gates loom to the north."""
|
gates loom to the north."""
|
||||||
|
outdoors = true
|
||||||
|
|
||||||
[exits]
|
[exits]
|
||||||
north = "town:gate"
|
north = "town:gate"
|
||||||
|
|||||||
Reference in New Issue
Block a user