termfront 0.1.7 → 0.1.9

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b6d91fa7b7758b2151fc31bb9f7862dbda3ad311edea638aa9b11ca50a8b9a8d
4
- data.tar.gz: 3a8ce081de76ddd04c45949c490ba4b513f42eb94eb39fce37fef292abdbb004
3
+ metadata.gz: 5396a2b08e03e482e2e5c8015a05399380e9e95ccee69389ca3969d41f3a1061
4
+ data.tar.gz: 9899861875106c3edb5db94f3b046abe1759168674e8223d5ee73ec7a943e22b
5
5
  SHA512:
6
- metadata.gz: a79ab596683eea6f8914c17cf88ad000d7a1b33453820526310f25fd43dbabb1f21e6f65f16a390fa92e491d6cd2da86e981c17bc971161316a6d4f7b431c98a
7
- data.tar.gz: 556667b34ba430c7af848fca7d31f5937e24d328480a29a3191db88ca4286d579081b0f6e18c34622d87cb9ea401c20e19da6aaf0a6fbc2b892f15fdbb9f7b76
6
+ metadata.gz: 2637febc2bf4863cf446692a837622353084558ee15c385e5d81081aeb022b791decce8c52f5a2a000e085e828a425bb7e78d815f503b4d54ae9c4104cbb69f7
7
+ data.tar.gz: ee9d71def5cbb215ff4a83573cfe665acd033ce9c8fc3a99d1a501e74f91f37a7765f57aafedf81cb5887bdb08d0714eb6ebcaba2fd7065736d19b85f73d4445
data/CHANGELOG.md CHANGED
@@ -6,6 +6,22 @@ The format is based on Keep a Changelog, and this project follows Semantic Versi
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.1.9] - 2026-05-28
10
+
11
+ ### Changed
12
+
13
+ - Move the TLS handshake off the multiplayer server's accept loop. `OpenSSL::SSL::SSLServer#start_immediately` is now `false`, so `accept` returns as soon as the TCP accept completes and the `SSLSocket#accept` handshake runs inside the per-connection thread under a new `TLS_HANDSHAKE_TIMEOUT` deadline (5 s). Previously every TLS handshake — the RSA private-key operation, the cert-chain bytes, and any network waits — ran serially on the single accept-loop thread, so a synchronized join of N players took roughly N × per-handshake-time before the last one was usable; a stalled mid-handshake client also blocked every other pending connection (head-of-line blocking at the TLS layer). With the handshake hoisted into its own thread, connections only contend on the GVL for the actual CPU work, a slow client only blocks itself, and `configure_client` is applied before the handshake so TCP_NODELAY is in effect for the handshake's own small records
14
+
15
+ ## [0.1.8] - 2026-05-26
16
+
17
+ ### Fixed
18
+
19
+ - Stop the `AudioManager` BGM and looping-SE threads from spinning at 100 % CPU when the audio player spawns but exits immediately (e.g. no usable audio device, codec missing, or backend not running). The non-looping playback loops now bail out if `spawn_player` returns `nil` or if the previous playback finished in under 500 ms, instead of relentlessly respawning a process that never plays anything. On a tester's CPU-bound host this was over 95 % of the process's CPU budget, making the renderer look heavy when the real culprit was the audio thread
20
+
21
+ ### Changed
22
+
23
+ - Adapt the in-game render rate to the host's headroom: an `AdaptiveRenderRate` module tracks how often a frame exceeds 85 % of the 60 Hz budget and, after 30 consecutive over-budget frames, downshifts the singleplayer / Wavesfight render rate to 30 Hz; if the host catches back up for 60 consecutive frames it upshifts back to 60 Hz. Capable machines keep running at 60 Hz, while testers on lower-spec CPUs / TDP-throttled laptops fall back to a steady 30 Hz instead of a stuttering 60 Hz, and the rate resets on every new game loop entry so a freshly launched match starts at 60 Hz
24
+
9
25
  ## [0.1.7] - 2026-05-26
10
26
 
11
27
  ### Changed
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Termfront
4
+ module AdaptiveRenderRate
5
+ OVER_BUDGET_RATIO = 0.85
6
+ UNDER_BUDGET_RATIO = 0.5
7
+ DOWNSHIFT_FRAMES = 30
8
+ UPSHIFT_FRAMES = 60
9
+
10
+ @current_dt = Config::RENDER_DT
11
+ @over_budget_count = 0
12
+ @under_budget_count = 0
13
+
14
+ class << self
15
+ def current_dt
16
+ @current_dt
17
+ end
18
+
19
+ def observe(spent)
20
+ if spent > Config::FRAME_DT * OVER_BUDGET_RATIO
21
+ @over_budget_count += 1
22
+ @under_budget_count = 0
23
+ if @over_budget_count >= DOWNSHIFT_FRAMES && @current_dt < Config::RENDER_DT_LOW
24
+ @current_dt = Config::RENDER_DT_LOW
25
+ end
26
+ elsif spent < Config::FRAME_DT * UNDER_BUDGET_RATIO
27
+ @under_budget_count += 1
28
+ @over_budget_count = 0
29
+ if @under_budget_count >= UPSHIFT_FRAMES && @current_dt > Config::RENDER_DT
30
+ @current_dt = Config::RENDER_DT
31
+ end
32
+ end
33
+ end
34
+
35
+ def reset!
36
+ @current_dt = Config::RENDER_DT
37
+ @over_budget_count = 0
38
+ @under_budget_count = 0
39
+ end
40
+ end
41
+ end
42
+ end
@@ -41,9 +41,13 @@ module Termfront
41
41
  loop do
42
42
  break if channel_stopped?(:bgm)
43
43
 
44
+ start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
44
45
  @bgm_pid = spawn_player(@bgm_player, path, loop_playback: false)
46
+ break unless @bgm_pid
47
+
45
48
  wait_for_channel(:bgm)
46
49
  break if channel_stopped?(:bgm)
50
+ break if Process.clock_gettime(Process::CLOCK_MONOTONIC) - start < 0.5
47
51
  end
48
52
  end
49
53
  rescue StandardError
@@ -101,9 +105,13 @@ module Termfront
101
105
  loop do
102
106
  break if channel_stopped?(:loop_se)
103
107
 
108
+ start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
104
109
  @loop_se_pid = spawn_player(@loop_se_player, path, loop_playback: false)
110
+ break unless @loop_se_pid
111
+
105
112
  wait_for_channel(:loop_se)
106
113
  break if channel_stopped?(:loop_se)
114
+ break if Process.clock_gettime(Process::CLOCK_MONOTONIC) - start < 0.5
107
115
  end
108
116
  end
109
117
  rescue StandardError
@@ -4,6 +4,7 @@ module Termfront
4
4
  module Config
5
5
  FRAME_DT = 1.0 / 60.0
6
6
  RENDER_DT = 1.0 / 60.0
7
+ RENDER_DT_LOW = 1.0 / 30.0
7
8
  MAX_DT = 1.0 / 20.0
8
9
  FOV = 66.0 * Math::PI / 180.0
9
10
  PLAYER_RADIUS = 0.2
@@ -130,9 +130,10 @@ module Termfront
130
130
  end
131
131
 
132
132
  def run_game_loop(show_complete_banner: true)
133
+ AdaptiveRenderRate.reset!
133
134
  STDIN.raw do |stdin|
134
135
  last_time = clock
135
- last_render = last_time - Config::RENDER_DT
136
+ last_render = last_time - AdaptiveRenderRate.current_dt
136
137
 
137
138
  loop do
138
139
  now = clock
@@ -149,7 +150,7 @@ module Termfront
149
150
  end
150
151
 
151
152
  update(dt)
152
- if now - last_render >= Config::RENDER_DT
153
+ if now - last_render >= AdaptiveRenderRate.current_dt
153
154
  @renderer.render(
154
155
  player: @player, map: @map,
155
156
  enemies: @enemies, projectiles: @projectiles,
@@ -180,9 +181,10 @@ module Termfront
180
181
  end
181
182
 
182
183
  def run_wavesfight_loop
184
+ AdaptiveRenderRate.reset!
183
185
  STDIN.raw do |stdin|
184
186
  last_time = clock
185
- last_render = last_time - Config::RENDER_DT
187
+ last_render = last_time - AdaptiveRenderRate.current_dt
186
188
 
187
189
  loop do
188
190
  now = clock
@@ -199,7 +201,7 @@ module Termfront
199
201
  end
200
202
 
201
203
  update(dt)
202
- if now - last_render >= Config::RENDER_DT
204
+ if now - last_render >= AdaptiveRenderRate.current_dt
203
205
  @renderer.render(
204
206
  player: @player, map: @map,
205
207
  enemies: @enemies, projectiles: @projectiles,
@@ -220,7 +222,7 @@ module Termfront
220
222
  show_wave_clear
221
223
  start_wavesfight_wave
222
224
  last_time = clock
223
- last_render = clock - Config::RENDER_DT
225
+ last_render = clock - AdaptiveRenderRate.current_dt
224
226
  end
225
227
 
226
228
  cap_frame(now)
@@ -659,6 +661,7 @@ module Termfront
659
661
 
660
662
  def cap_frame(frame_start)
661
663
  spent = clock - frame_start
664
+ AdaptiveRenderRate.observe(spent)
662
665
  remain = Config::FRAME_DT - spent
663
666
  sleep(remain) if remain > 0
664
667
  end
@@ -907,6 +907,7 @@ module Termfront
907
907
 
908
908
  def cap_frame(frame_start)
909
909
  spent = clock - frame_start
910
+ AdaptiveRenderRate.observe(spent)
910
911
  remain = Config::FRAME_DT - spent
911
912
  sleep(remain) if remain > 0
912
913
  end
@@ -4,6 +4,7 @@ require "socket"
4
4
  require "openssl"
5
5
  require "json"
6
6
  require "set"
7
+ require "timeout"
7
8
 
8
9
  module Termfront
9
10
  module Network
@@ -11,6 +12,7 @@ module Termfront
11
12
  TEAM_SIZES = [1, 2, 4].freeze
12
13
  MAX_QUEUE_PER_MODE = 64
13
14
  QUEUE_HANDSHAKE_TIMEOUT = 5
15
+ TLS_HANDSHAKE_TIMEOUT = 5
14
16
  MAX_MSG_BYTES = 16 * 1024
15
17
  MATCH_MAX_DURATION = 30 * 60
16
18
  MATCH_IDLE_TIMEOUT = 5 * 60
@@ -77,28 +79,36 @@ module Termfront
77
79
 
78
80
  tcp_server = TCPServer.new("0.0.0.0", @port)
79
81
  ssl_server = OpenSSL::SSL::SSLServer.new(tcp_server, ctx)
80
- ssl_server.start_immediately = true
82
+ ssl_server.start_immediately = false
81
83
 
82
84
  puts "Termfront PvP server listening on 0.0.0.0:#{@port}"
83
85
 
84
86
  loop do
85
87
  begin
86
88
  client = ssl_server.accept
87
- configure_client(client)
88
- Thread.new(client) do |c|
89
- enqueue_player(c)
90
- rescue StandardError => e
91
- puts "Connection handler error: #{e.class}"
92
- begin
93
- c.close
94
- rescue StandardError
95
- nil
96
- end
97
- end
98
- rescue OpenSSL::SSL::SSLError => e
99
- puts "SSL handshake failed: #{e.class}"
100
89
  rescue StandardError => e
101
90
  puts "Accept error: #{e.class}"
91
+ next
92
+ end
93
+
94
+ Thread.new(client) do |c|
95
+ configure_client(c)
96
+ Timeout.timeout(TLS_HANDSHAKE_TIMEOUT) { c.accept }
97
+ enqueue_player(c)
98
+ rescue OpenSSL::SSL::SSLError, Timeout::Error => e
99
+ puts "SSL handshake failed: #{e.class}"
100
+ begin
101
+ c.close
102
+ rescue StandardError
103
+ nil
104
+ end
105
+ rescue StandardError => e
106
+ puts "Connection handler error: #{e.class}"
107
+ begin
108
+ c.close
109
+ rescue StandardError
110
+ nil
111
+ end
102
112
  end
103
113
  end
104
114
  end
@@ -406,6 +406,7 @@ module Termfront
406
406
 
407
407
  def cap_frame(frame_start)
408
408
  spent = clock - frame_start
409
+ AdaptiveRenderRate.observe(spent)
409
410
  remain = Config::FRAME_DT - spent
410
411
  sleep(remain) if remain > 0
411
412
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Termfront
4
- VERSION = "0.1.7"
4
+ VERSION = "0.1.9"
5
5
  end
data/lib/termfront.rb CHANGED
@@ -4,6 +4,7 @@ require "io/console"
4
4
 
5
5
  require_relative "termfront/version"
6
6
  require_relative "termfront/config"
7
+ require_relative "termfront/adaptive_render_rate"
7
8
  require_relative "termfront/color"
8
9
  require_relative "termfront/map"
9
10
  require_relative "termfront/weapon/base"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: termfront
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.7
4
+ version: 0.1.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - S-H-GAMELINKS
@@ -51,6 +51,7 @@ files:
51
51
  - exe/termfront
52
52
  - exe/termfront-server
53
53
  - lib/termfront.rb
54
+ - lib/termfront/adaptive_render_rate.rb
54
55
  - lib/termfront/async_writer.rb
55
56
  - lib/termfront/audio_manager.rb
56
57
  - lib/termfront/color.rb