Repository navigation
Expand file tree
/
Copy pathinput_handler.cpp
More file actions
201 lines (183 loc) · 7.41 KB
/
Copy pathinput_handler.cpp
File metadata and controls
201 lines (183 loc) · 7.41 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
// TermiosInputHandler 实现:termios 原始模式 + poll(2) 非阻塞采样。
//
// 长按节流原理(见 DESIGN.md §4):游戏 tick 周期为 333ms(≈3 Hz),
// 每个 poll 窗口至多返回一个 Command,宏观转向速率 = 每秒至多 3 次,
// 与终端自动重复率(xterm 默认约 25 Hz)解耦;窗口内聚合同方向的
// 重复次数存入 Command::repeats,方向取最后一次有效按键。
//
// 退出键:q / Q / Ctrl-C(原始模式下 ISIG 被关闭,Ctrl-C 以字节 0x03
// 到达,由本模块转换为 quit 请求,从而保证 TermiosGuard 正常析构恢复终端)。
#include "input/input_handler.hpp"
#include <poll.h>
#include <termios.h>
#include <unistd.h>
#include <algorithm>
#include <cerrno>
#include <chrono>
#include <cstdint>
#include <cstring>
#include <optional>
#include <string_view>
namespace snake::input {
namespace {
using core::Direction;
// ESC [ A/B/C/D 方向键序列的解析状态(ESC 可能跨 read 调用被切开,需保持)。
enum class EscState : int {
Idle = 0, // 空闲
Esc = 1, // 已收到 ESC
Bracket = 2 // 已收到 ESC [
};
// 把方向键序列的终码映射为方向;非方向终码返回 nullopt。
// Args:
// final: ESC [ 之后的终码字节(A/B/C/D 之一)。
// Returns: 命中的方向,或 nullopt。
std::optional<Direction> ArrowFromFinal(char final) {
switch (final) {
case 'A': return Direction::Up;
case 'B': return Direction::Down;
case 'C': return Direction::Right;
case 'D': return Direction::Left;
default: return std::nullopt;
}
}
// 把 WASD 键位映射为方向;其余按键返回 nullopt。
// Args:
// key: 单个按键字节(ASCII,大小写均可)。
// Returns: 命中的方向,或 nullopt。
std::optional<Direction> DirectionFromWASD(unsigned char key) {
switch (key) {
case 'w': case 'W': return Direction::Up;
case 'a': case 'A': return Direction::Left;
case 's': case 'S': return Direction::Down;
case 'd': case 'D': return Direction::Right;
default: return std::nullopt;
}
}
// 记录一次方向事件:同方向重复次数聚合,方向取最后一次有效键。
// Args:
// d: 命中的方向。
// last: 输入输出——窗口内最后一次命中的方向。
// repeats: 输入输出——窗口内“同方向”重复次数(方向改变时重置)。
void RecordDirection(Direction d, std::optional<Direction>& last,
std::uint8_t& repeats) {
if (last && *last == d) {
if (repeats < 255) ++repeats;
} else {
repeats = 1;
}
last = d;
}
// 逐字节喂入转义序列状态机,聚合方向键与退出键事件。
// Args:
// bytes: 本次 read 到的一批原始字节。
// esc: 输入输出的状态机状态(跨 read 调用保持)。
// last: 输出——窗口内最后一次命中的方向。
// repeats: 输出——窗口内“同方向”重复次数(方向改变时重置)。
// quit: 输出——q / Ctrl-C / 输入流关闭 时置位。
void FeedBytes(std::string_view bytes, EscState& esc,
std::optional<Direction>& last, std::uint8_t& repeats,
bool& quit) {
for (const char c : bytes) {
const unsigned char u = static_cast<unsigned char>(c);
switch (esc) {
case EscState::Idle:
if (u == 0x1B) {
esc = EscState::Esc;
} else if (u == 'q' || u == 'Q' || u == 0x03) {
quit = true; // q / Q / Ctrl-C
} else if (auto d = DirectionFromWASD(u); d) {
RecordDirection(*d, last, repeats); // WASD 键位
}
break;
case EscState::Esc:
// 仅识别 ESC [ 序列;其余 ESC 前缀序列丢弃(回到空闲)。
esc = (u == '[') ? EscState::Bracket : EscState::Idle;
break;
case EscState::Bracket:
esc = EscState::Idle;
if (auto d = ArrowFromFinal(static_cast<char>(u)); d) {
RecordDirection(*d, last, repeats); // 方向键
}
break;
}
if (quit) return;
}
}
class TermiosInputHandler final : public IInputHandler {
public:
// 非阻塞采样:poll 等待至 timeout,期间 drain stdin 并聚合按键。
// 无输入返回 nullopt;stdin 关闭或按下退出键后置 quit 标志。
std::optional<Command> poll(std::chrono::milliseconds timeout) noexcept override {
struct pollfd pfd = {STDIN_FILENO, POLLIN, 0};
std::optional<Direction> last;
std::uint8_t repeats = 0;
bool quit = false;
const auto deadline = std::chrono::steady_clock::now() + timeout;
for (;;) {
if (quit || quit_) break;
const auto now = std::chrono::steady_clock::now();
if (now >= deadline) break;
// 分片轮询(≤16ms),保证退出请求不被长窗口拖延。
const auto remain = std::chrono::duration_cast<std::chrono::milliseconds>(
deadline - now);
const int ms = static_cast<int>(std::clamp(remain.count(), 0L, 16L));
const int rc = ::poll(&pfd, 1, ms);
if (rc < 0) {
if (errno == EINTR) continue;
break;
}
if (rc == 0) continue;
// 可读(POLLIN)或对端关闭(POLLHUP/POLLERR/POLLNVAL)都进入 read:
// 管道写端关闭时 poll 只报 POLLHUP,read 返回 0 即为 EOF(退出)。
const short ready = pfd.revents & (POLLIN | POLLHUP | POLLERR | POLLNVAL);
if (ready == 0) continue;
char buf[64];
const ssize_t n = ::read(STDIN_FILENO, buf, sizeof buf);
if (n < 0) {
if (errno == EINTR) continue;
break;
}
if (n == 0) {
quit = true; // stdin 关闭(如管道结束)
break;
}
FeedBytes(std::string_view(buf, static_cast<std::size_t>(n)),
esc_state_, last, repeats, quit);
}
if (quit) quit_ = true;
if (!last) return std::nullopt;
return Command{*last, repeats};
}
bool quit_requested() const noexcept override {
return quit_;
}
private:
EscState esc_state_{EscState::Idle};
bool quit_{false};
};
} // namespace
// 构造:保存原终端属性并切换为原始模式(关 ICANON/ECHO/ISIG)。
// stdin 非 tty(tcgetattr 失败)时标记无效,成为空操作,便于无终端测试。
TermiosGuard::TermiosGuard() {
if (::tcgetattr(STDIN_FILENO, &orig_) != 0) {
valid_ = false;
return;
}
struct termios raw = orig_;
raw.c_lflag &= static_cast<tcflag_t>(~(ICANON | ECHO | ISIG));
raw.c_iflag &= static_cast<tcflag_t>(~(IXON | ICRNL));
raw.c_cc[VMIN] = 0; // 非阻塞读取:无数据立即返回(配合 poll 使用)
raw.c_cc[VTIME] = 0;
valid_ = (::tcsetattr(STDIN_FILENO, TCSANOW, &raw) == 0);
}
// 析构:恢复终端到进入游戏前的属性。
TermiosGuard::~TermiosGuard() {
if (valid_) {
::tcsetattr(STDIN_FILENO, TCSANOW, &orig_);
}
}
IInputHandler* make_input_handler() {
static TermiosInputHandler instance;
return &instance;
}
} // namespace snake::input