termfront 0.1.3 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +34 -0
- data/README.md +14 -1
- data/data/events/final_push.json +0 -11
- data/lib/termfront/audio_manager.rb +6 -2
- data/lib/termfront/mission/final_push.rb +1 -1
- data/lib/termfront/network/client.rb +27 -54
- data/lib/termfront/network/connection.rb +8 -0
- data/lib/termfront/network/server.rb +459 -91
- data/lib/termfront/network/wavesfight_client.rb +104 -55
- data/lib/termfront/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2169347d66dd50b7734b9de8afc2398a0b979649cd98cb36aa6cc6dcdc9373f5
|
|
4
|
+
data.tar.gz: afd578bf2aabf93476c81d96fdcceeb5d55289d6137dade78429b75bdd84753a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4bf5e4ee9f0c736626f7c73f05ce04a5a2c128f1c94193d9f74ae6c7788297561f24244d7ae65d03f5b6955ba10d772bfe9ba3a017de4d2bac18b8a527020a8b
|
|
7
|
+
data.tar.gz: d317a9c0ac3ba86c633c3fb1f8b5af951bda951bcc2418213457535ec32d3e0d2471fca9c3e34588f5f134800c46a495a68cbfa58f86f9349739e1dc206ffe7d
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,40 @@ The format is based on Keep a Changelog, and this project follows Semantic Versi
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.1.34] - 2026-05-25
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- Fixed PvP `route_hit` so the server always sends the fixed `Config::PVP_HIT_DMG` damage value, ignoring the attacker-supplied `d` field
|
|
14
|
+
- Require `TERMFRONT_TLS_CERT_FILE` and `TERMFRONT_TLS_KEY_FILE` to be set and point to existing PEM files; removed the self-signed certificate generation fallback
|
|
15
|
+
- Enforce TLS 1.2 as the minimum protocol version on both the multiplayer server and client
|
|
16
|
+
- Stop logging client peer IP addresses on the multiplayer server
|
|
17
|
+
- Connect multiplayer clients to the official server address only; remove the free-form server address input
|
|
18
|
+
- Restrict audio manifest entries to paths under `data/audio/`
|
|
19
|
+
- Reject Wavesfight co-op queue requests with unknown mission ids
|
|
20
|
+
- Guard match worker threads against uncaught exceptions and ensure player sockets are closed
|
|
21
|
+
- Cap each matchmaking queue at 64 waiting players and reject excess connections
|
|
22
|
+
- Move per-connection handshake off the accept loop and drop silent clients after a short timeout
|
|
23
|
+
- Cap server and client receive buffers and disconnect peers that flood bytes without a newline
|
|
24
|
+
- End multiplayer matches after a maximum duration or when all players have been idle
|
|
25
|
+
- Restrict the weapon field on multiplayer state messages to the legal loadout (`pistol`, `ar`)
|
|
26
|
+
- Validate enemy / weapon / projectile type symbols received from the server against a fixed whitelist on the client side before converting to symbols
|
|
27
|
+
- Validate position, ammo, and fire-flash fields on Wavesfight co-op state messages; reject the update when position is non-finite or outside the map
|
|
28
|
+
- Validate PvP state fields (position, shield, health, ammo, fire-flash) before relaying to opponents; drop the relay when position is non-finite or outside the map
|
|
29
|
+
- Reject multiplayer state messages whose position delta exceeds the maximum physical step from the previous server-known position
|
|
30
|
+
- Rate-limit incoming multiplayer messages per type per player; sustained overflow ends the match for the offending client
|
|
31
|
+
- Track PvP shield and health on the server; the server applies hit damage, regenerates shields between hits, and uses authoritative values when relaying state to opponents
|
|
32
|
+
- Determine PvP hit targets server-side via raycast from the attacker's known position and facing; enforce weapon cooldown, firing cone, line-of-sight, and team checks instead of trusting the attacker-supplied target id
|
|
33
|
+
- Wavesfight co-op shield no longer stays depleted: the server now regenerates shield and health after `Config::SHIELD_DELAY`, and the client plays the shield regeneration loop SE while regen is active
|
|
34
|
+
- Wavesfight co-op now restores shield, health, and revives downed players between waves to match singleplayer behavior
|
|
35
|
+
- Final Push: relocated the rightmost crawler off the dividing wall so it can be killed and the mission can complete
|
|
36
|
+
|
|
37
|
+
### Added
|
|
38
|
+
|
|
39
|
+
- Honor `TERMFRONT_TLS_CA_FILE` on multiplayer clients to trust an additional CA certificate
|
|
40
|
+
- Optional shared-token authentication via `TERMFRONT_PVP_TOKEN`; when set on the server, queue requests must carry a matching token (sent automatically when the client has the same env var)
|
|
41
|
+
- Wavesfight co-op now generates weapon drops when enemies are killed; the `E` key picks up the nearest drop within `Config::PICKUP_RADIUS`, swapping the current weapon (which is dropped at the player's position) and tracking obtained weapons per-player so cheaters cannot claim weapons they have not picked up
|
|
42
|
+
|
|
9
43
|
## [0.1.3] - 2026-05-24
|
|
10
44
|
|
|
11
45
|
### Fixed
|
data/README.md
CHANGED
|
@@ -54,7 +54,7 @@ Start a PvP server:
|
|
|
54
54
|
termfront-server
|
|
55
55
|
```
|
|
56
56
|
|
|
57
|
-
Use
|
|
57
|
+
TLS certificate / key paths are **required** for the server to start. Use a fullchain certificate (e.g. issued by Let's Encrypt):
|
|
58
58
|
|
|
59
59
|
```bash
|
|
60
60
|
TERMFRONT_TLS_CERT_FILE=/path/to/fullchain.pem \
|
|
@@ -72,6 +72,19 @@ Default PvP port is `7777`.
|
|
|
72
72
|
|
|
73
73
|
The default multiplayer client address is `termfront.gamelinks007.net:443`.
|
|
74
74
|
|
|
75
|
+
Set `TERMFRONT_TLS_CA_FILE` to trust an additional CA certificate when running the client against a server whose certificate chain is not in the system trust store:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
TERMFRONT_TLS_CA_FILE=/path/to/ca.pem termfront
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Set `TERMFRONT_PVP_TOKEN` on the server to require clients to present the same token before they can queue. Useful for limiting participation to a known group:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
TERMFRONT_PVP_TOKEN=event_token termfront-server # server side
|
|
85
|
+
TERMFRONT_PVP_TOKEN=event_token termfront # each authorized client
|
|
86
|
+
```
|
|
87
|
+
|
|
75
88
|
## Controls
|
|
76
89
|
|
|
77
90
|
- `W` `A` `S` `D`: move
|
data/data/events/final_push.json
CHANGED
|
@@ -19,17 +19,6 @@
|
|
|
19
19
|
"id": "final_push_outro",
|
|
20
20
|
"trigger": { "type": "mission_complete" },
|
|
21
21
|
"actions": [
|
|
22
|
-
{
|
|
23
|
-
"type": "demo",
|
|
24
|
-
"duration": 3.0,
|
|
25
|
-
"caption": "ARCHIVE VAULT / SIGNAL STILL PRESENT",
|
|
26
|
-
"path": [
|
|
27
|
-
{ "x": 20.5, "y": 9.5, "angle": 3.14, "t": 0.0 },
|
|
28
|
-
{ "x": 16.5, "y": 9.5, "angle": 3.05, "t": 1.0 },
|
|
29
|
-
{ "x": 11.5, "y": 9.5, "angle": 2.95, "t": 2.0 },
|
|
30
|
-
{ "x": 7.5, "y": 8.5, "angle": 2.75, "t": 3.0 }
|
|
31
|
-
]
|
|
32
|
-
},
|
|
33
22
|
{ "type": "title_card", "text": "FORTRESS CLEARED\nSIGNAL SOURCE UNRESOLVED" },
|
|
34
23
|
{ "type": "dialogue", "speaker": "OPS", "text": "Fortress cleared. The signal is still active somewhere below you." },
|
|
35
24
|
{ "type": "dialogue", "speaker": "OPS", "text": "This facility did not fail containment. Someone revoked it." },
|
|
@@ -170,10 +170,14 @@ module Termfront
|
|
|
170
170
|
|
|
171
171
|
def asset_path(kind, name)
|
|
172
172
|
relative = @manifest.fetch(kind.to_s, {})[name.to_s]
|
|
173
|
-
return unless relative
|
|
173
|
+
return unless relative.is_a?(String) && !relative.empty?
|
|
174
174
|
|
|
175
|
+
root = File.expand_path("../../data/audio", __dir__)
|
|
175
176
|
path = File.expand_path("../../#{relative}", __dir__)
|
|
176
|
-
|
|
177
|
+
return unless path.start_with?(root + File::SEPARATOR)
|
|
178
|
+
return unless File.file?(path)
|
|
179
|
+
|
|
180
|
+
path
|
|
177
181
|
end
|
|
178
182
|
|
|
179
183
|
def spawn_player(player, path, loop_playback:, detach: false)
|
|
@@ -4,6 +4,7 @@ module Termfront
|
|
|
4
4
|
module Network
|
|
5
5
|
class Client
|
|
6
6
|
TEAM_SIZES = [1, 2, 4].freeze
|
|
7
|
+
ALLOWED_WEAPONS = %w[pistol ar].freeze
|
|
7
8
|
|
|
8
9
|
def initialize(stdout)
|
|
9
10
|
@stdout = stdout
|
|
@@ -14,16 +15,16 @@ module Termfront
|
|
|
14
15
|
end
|
|
15
16
|
|
|
16
17
|
def run
|
|
17
|
-
addr = prompt_address
|
|
18
|
-
return unless addr
|
|
19
|
-
|
|
20
18
|
team_size = prompt_team_size
|
|
21
19
|
return unless team_size
|
|
22
20
|
|
|
23
|
-
host, port =
|
|
21
|
+
host, port = Config::PVP_DEFAULT_ADDRESS.split(":", 2).then { |h, p| [h, p.to_i] }
|
|
22
|
+
queue_msg = { t: "queue", team_size: team_size }
|
|
23
|
+
token = ENV["TERMFRONT_PVP_TOKEN"]
|
|
24
|
+
queue_msg[:token] = token if token && !token.empty?
|
|
24
25
|
begin
|
|
25
|
-
@conn.connect(host, port)
|
|
26
|
-
@conn.send_msg(
|
|
26
|
+
@conn.connect(host, port, ca_file: ENV["TERMFRONT_TLS_CA_FILE"])
|
|
27
|
+
@conn.send_msg(queue_msg)
|
|
27
28
|
rescue StandardError => e
|
|
28
29
|
show_error("Connection failed: #{e.message}")
|
|
29
30
|
return
|
|
@@ -45,52 +46,6 @@ module Termfront
|
|
|
45
46
|
|
|
46
47
|
private
|
|
47
48
|
|
|
48
|
-
def prompt_address
|
|
49
|
-
input = Config::PVP_DEFAULT_ADDRESS
|
|
50
|
-
|
|
51
|
-
STDIN.raw do |stdin|
|
|
52
|
-
loop do
|
|
53
|
-
rows, cols = @stdout.winsize
|
|
54
|
-
buf = TerminalOutput.begin_frame(home: true, clear: true)
|
|
55
|
-
lines = Array.new(rows) { " " * cols }
|
|
56
|
-
|
|
57
|
-
title = "PvP - Enter Server Address"
|
|
58
|
-
tc = [(cols - title.size) / 2 + 1, 1].max
|
|
59
|
-
lines[rows / 2 - 3] = TerminalOutput.fit_ansi("#{" " * (tc - 1)}\e[1;96m#{title}\e[0m", cols)
|
|
60
|
-
|
|
61
|
-
prompt = "> #{input}_"
|
|
62
|
-
pc = [(cols - prompt.size) / 2 + 1, 1].max
|
|
63
|
-
lines[rows / 2 - 1] = TerminalOutput.fit_ansi("#{" " * (pc - 1)}\e[97m> #{input}\e[5m_\e[0m", cols)
|
|
64
|
-
|
|
65
|
-
hint = "(Enter to continue, ESC to cancel)"
|
|
66
|
-
hc = [(cols - hint.size) / 2 + 1, 1].max
|
|
67
|
-
lines[rows / 2 + 1] = TerminalOutput.fit_ansi("#{" " * (hc - 1)}\e[90m#{hint}\e[0m", cols)
|
|
68
|
-
|
|
69
|
-
lines.each_with_index do |line, index|
|
|
70
|
-
buf << line
|
|
71
|
-
buf << "\r\n" if index < rows - 1
|
|
72
|
-
end
|
|
73
|
-
buf << TerminalOutput.end_frame
|
|
74
|
-
TerminalOutput.write_all(@stdout, buf)
|
|
75
|
-
|
|
76
|
-
next unless IO.select([stdin], nil, nil, Config::FRAME_DT)
|
|
77
|
-
|
|
78
|
-
begin
|
|
79
|
-
data = stdin.read_nonblock(64)
|
|
80
|
-
data.each_byte do |b|
|
|
81
|
-
case b
|
|
82
|
-
when 27 then return nil
|
|
83
|
-
when 13, 10 then return input.empty? ? Config::PVP_DEFAULT_ADDRESS : input
|
|
84
|
-
when 127, 8 then input = input[0...-1] unless input.empty?
|
|
85
|
-
when 32..126 then input << b.chr
|
|
86
|
-
end
|
|
87
|
-
end
|
|
88
|
-
rescue IO::WaitReadable
|
|
89
|
-
end
|
|
90
|
-
end
|
|
91
|
-
end
|
|
92
|
-
end
|
|
93
|
-
|
|
94
49
|
def prompt_team_size
|
|
95
50
|
selected = 0
|
|
96
51
|
|
|
@@ -310,7 +265,15 @@ module Termfront
|
|
|
310
265
|
when "state"
|
|
311
266
|
update_remote_state(msg)
|
|
312
267
|
when "hit"
|
|
313
|
-
|
|
268
|
+
if msg.key?(:s) && msg.key?(:h)
|
|
269
|
+
@player.shield = msg[:s]
|
|
270
|
+
@player.health = msg[:h]
|
|
271
|
+
@player.last_damage = @player.game_time
|
|
272
|
+
@player.damage_flash = 3
|
|
273
|
+
@player.dead = msg[:h] <= 0
|
|
274
|
+
else
|
|
275
|
+
@player.apply_damage(msg[:d] || Config::PVP_HIT_DMG)
|
|
276
|
+
end
|
|
314
277
|
@audio.play_se(:damage)
|
|
315
278
|
notify_death_if_needed
|
|
316
279
|
when "dead"
|
|
@@ -334,7 +297,8 @@ module Termfront
|
|
|
334
297
|
remote[:current].angle = msg[:a]
|
|
335
298
|
remote[:current].shield = msg[:s]
|
|
336
299
|
remote[:current].health = msg[:h]
|
|
337
|
-
|
|
300
|
+
weapon = safe_weapon(msg[:w])
|
|
301
|
+
remote[:current].weapon = weapon if weapon
|
|
338
302
|
remote[:current].ammo = msg[:am]
|
|
339
303
|
spawn_remote_projectile_effect(msg) if (remote[:current].fire_flash || 0) <= 0 && (msg[:ff] || 0) > 0
|
|
340
304
|
remote[:current].fire_flash = msg[:ff] || 0
|
|
@@ -945,6 +909,15 @@ module Termfront
|
|
|
945
909
|
remain = Config::FRAME_DT - spent
|
|
946
910
|
sleep(remain) if remain > 0
|
|
947
911
|
end
|
|
912
|
+
|
|
913
|
+
def safe_weapon(value)
|
|
914
|
+
return nil unless value.is_a?(String) || value.is_a?(Symbol)
|
|
915
|
+
|
|
916
|
+
name = value.to_s
|
|
917
|
+
return nil unless ALLOWED_WEAPONS.include?(name)
|
|
918
|
+
|
|
919
|
+
name.to_sym
|
|
920
|
+
end
|
|
948
921
|
end
|
|
949
922
|
end
|
|
950
923
|
end
|
|
@@ -8,6 +8,8 @@ require "time"
|
|
|
8
8
|
module Termfront
|
|
9
9
|
module Network
|
|
10
10
|
class Connection
|
|
11
|
+
MAX_MSG_BYTES = 64 * 1024
|
|
12
|
+
|
|
11
13
|
PeerInfo = Struct.new(
|
|
12
14
|
:certificate_sha256,
|
|
13
15
|
:public_key_sha256,
|
|
@@ -64,6 +66,11 @@ module Termfront
|
|
|
64
66
|
data = @sock.read_nonblock(4096)
|
|
65
67
|
@buf << data
|
|
66
68
|
|
|
69
|
+
if @buf.bytesize > MAX_MSG_BYTES
|
|
70
|
+
close
|
|
71
|
+
break
|
|
72
|
+
end
|
|
73
|
+
|
|
67
74
|
while (nl = @buf.index("\n"))
|
|
68
75
|
line = @buf.slice!(0, nl + 1)
|
|
69
76
|
begin
|
|
@@ -120,6 +127,7 @@ module Termfront
|
|
|
120
127
|
ctx = OpenSSL::SSL::SSLContext.new
|
|
121
128
|
ctx.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
|
122
129
|
ctx.verify_hostname = true if ctx.respond_to?(:verify_hostname=)
|
|
130
|
+
ctx.min_version = OpenSSL::SSL::TLS1_2_VERSION
|
|
123
131
|
ctx.cert_store = build_cert_store(ca_file: ca_file)
|
|
124
132
|
ctx
|
|
125
133
|
end
|