Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions frametap.toml.example
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,8 @@ stick_deadzone = 0.5

# 画面に積む試行の数。
trials_shown = 5

# 何も押していない区間を行として出すか。既定は出さない。
# 出さないのは、その行が持続 F しか持たず、読む行数だけが倍になるため。
# 入力と入力の間合いは、押下 F の差で読める。
show_released = false
1 change: 1 addition & 0 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ fn settings_from(config: &Config) -> ui::Settings {
ui::Settings {
fps: config.fps,
trials_shown: config.trials_shown,
show_released: config.show_released,
}
}

Expand Down
57 changes: 50 additions & 7 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ pub const DEFAULT_STICK_DEADZONE: f32 = 0.5;
/// 縦リストに積む試行数の既定値。
pub const DEFAULT_TRIALS_SHOWN: usize = 5;

/// 離した区間を行にするかの既定値。既定で出さないのは、その行が持続 F しか持たず、
/// 読む行数だけが倍になるため。間合いは押下 F の差で読める。
pub const DEFAULT_SHOW_RELEASED: bool = false;

/// 設定の値。
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Config {
Expand All @@ -37,6 +41,8 @@ pub struct Config {
pub stick_deadzone: f32,
/// 縦リストに積む試行数。
pub trials_shown: usize,
/// 何も押していない区間を行にするか。
pub show_released: bool,
}

impl Default for Config {
Expand All @@ -46,6 +52,7 @@ impl Default for Config {
trial_gap_ms: DEFAULT_TRIAL_GAP_MS,
stick_deadzone: DEFAULT_STICK_DEADZONE,
trials_shown: DEFAULT_TRIALS_SHOWN,
show_released: DEFAULT_SHOW_RELEASED,
}
}
}
Expand Down Expand Up @@ -81,7 +88,7 @@ pub fn read(path: &Path) -> Loaded {
// 置いてあるのに読めないのは権限か壊れた文字である。既定値で起動は続けるが黙らない。
Err(err) => Loaded {
config: Config::default(),
warnings: vec![format!("{} を読めない: {err}", path.display())],
warnings: vec![format!("cannot read {}: {err}", path.display())],
},
}
}
Expand All @@ -102,20 +109,20 @@ pub fn parse(text: &str) -> Loaded {
// section は読まない。黙って飛ばすと、中に書いたキーが効かない理由が分からない。
if line.starts_with('[') {
warnings.push(format!(
"{number} 行目: section を読まない。`{line}` を無視した"
"line {number}: sections are not read, ignored `{line}`"
));
continue;
}

let Some((key, value)) = line.split_once('=') else {
warnings.push(format!("{number} 行目: `=` が無い。`{line}` を無視した"));
warnings.push(format!("line {number}: no `=`, ignored `{line}`"));
continue;
};
let (key, value) = (key.trim(), value.trim());

if seen.contains(&key) {
warnings.push(format!(
"{number} 行目: `{key}` が 2 回ある。後ろの値を使う"
"line {number}: `{key}` appears twice, using the last value"
));
}
seen.push(key);
Expand Down Expand Up @@ -165,7 +172,17 @@ pub fn parse(text: &str) -> Loaded {
&DEFAULT_TRIALS_SHOWN.to_string(),
)),
},
_ => warnings.push(format!("{number} 行目: `{key}` は知らないキーである")),
"show_released" => match value {
"true" => config.show_released = true,
"false" => config.show_released = false,
_ => warnings.push(not_a_boolean(
number,
key,
value,
&DEFAULT_SHOW_RELEASED.to_string(),
)),
},
_ => warnings.push(format!("line {number}: unknown key `{key}`")),
}
}

Expand All @@ -186,11 +203,15 @@ fn strip_comment(line: &str) -> &str {
}

fn not_a_number(number: usize, key: &str, value: &str, fallback: &str) -> String {
format!("{number} 行目: `{key}` の `{value}` を数として読めない。既定の {fallback} を使う")
format!("line {number}: `{key}` = `{value}` is not a number, using default {fallback}")
}

fn not_a_boolean(number: usize, key: &str, value: &str, fallback: &str) -> String {
format!("line {number}: `{key}` = `{value}` is not true or false, using default {fallback}")
}

fn out_of_range(number: usize, key: &str, value: &str, fallback: &str) -> String {
format!("{number} 行目: `{key}` の `{value}` は受け付けない。既定の {fallback} を使う")
format!("line {number}: `{key}` = `{value}` is out of range, using default {fallback}")
}

#[cfg(test)]
Expand Down Expand Up @@ -293,6 +314,28 @@ mod tests {
}
}

#[test]
fn show_released_reads_true_and_false() {
assert!(parse("show_released = true\n").config.show_released);
assert!(!parse("show_released = false\n").config.show_released);
assert!(parse("show_released = true\n").warnings.is_empty());
}

/// 既定は非表示。離した区間の行は持続 F しか持たず、読む列を増やすだけになる。
#[test]
fn show_released_defaults_to_false() {
assert!(!Config::default().show_released);
}

#[test]
fn a_show_released_that_is_not_a_boolean_keeps_the_default_and_warns() {
let loaded = parse("show_released = yes\n");

assert!(!loaded.config.show_released);
assert_eq!(loaded.warnings.len(), 1);
assert!(loaded.warnings[0].contains("show_released"));
}

/// 0 件だと画面に何も出ない。設定として通さない。
#[test]
fn a_zero_trials_shown_keeps_the_default_and_warns() {
Expand Down
16 changes: 8 additions & 8 deletions src/hid_source.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ mod platform {

/// 最初に見つかった対象デバイスを読み取り専用で開く。
pub fn open() -> Result<Connection, String> {
let api = HidApi::new().map_err(|err| format!("HidApi の初期化に失敗した: {err}"))?;
let api = HidApi::new().map_err(|err| format!("failed to initialize HidApi: {err}"))?;

let (product_id, name, kind) = api
.device_list()
Expand All @@ -89,11 +89,11 @@ mod platform {
.find(|(product_id, _, _)| *product_id == info.product_id())
.copied()
})
.ok_or_else(|| format!("対象デバイスが見つからない (VID {SONY_VENDOR_ID:#06X})"))?;
.ok_or_else(|| format!("no target device found (VID {SONY_VENDOR_ID:#06X})"))?;

let device = api
.open(SONY_VENDOR_ID, product_id)
.map_err(|err| format!("{name} を開けない: {err}"))?;
.map_err(|err| format!("cannot open {name}: {err}"))?;

Ok(Connection {
device,
Expand All @@ -120,7 +120,7 @@ mod platform {
let len = self
.device
.read_timeout(&mut buffer, BLOCK_UNTIL_REPORT)
.map_err(|err| format!("{} の読み取りに失敗した: {err}", self.name))?;
.map_err(|err| format!("read failed on {}: {err}", self.name))?;

Ok(Report {
buffer,
Expand All @@ -137,7 +137,7 @@ mod platform {
let mut ticks = 0i64;
// SAFETY: ticks はスタック上の有効な i64。
unsafe { QueryPerformanceCounter(&mut ticks) }
.map_err(|err| format!("QueryPerformanceCounter に失敗した: {err}"))?;
.map_err(|err| format!("QueryPerformanceCounter failed: {err}"))?;

Ok((i128::from(ticks) * 1_000_000 / i128::from(frequency)) as u64)
}
Expand All @@ -150,9 +150,9 @@ mod platform {
let mut frequency = 0i64;
// SAFETY: frequency はスタック上の有効な i64。
unsafe { QueryPerformanceFrequency(&mut frequency) }
.map_err(|err| format!("QueryPerformanceFrequency に失敗した: {err}"))?;
.map_err(|err| format!("QueryPerformanceFrequency failed: {err}"))?;
if frequency <= 0 {
return Err(format!("QPC の周波数が {frequency} で使えない"));
return Err(format!("unusable QPC frequency: {frequency}"));
}

Ok(*QPC_FREQUENCY.get_or_init(|| frequency))
Expand All @@ -168,7 +168,7 @@ mod platform {
pub enum Connection {}

pub fn open() -> Result<Connection, String> {
Err("HID の読み取りは Windows でのみ動く".to_owned())
Err("HID reading works on Windows only".to_owned())
}

impl Connection {
Expand Down
6 changes: 3 additions & 3 deletions src/timeline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,10 +90,10 @@ const STICK_DIRECTIONS: [Direction; 8] = [
];

/// 十字キーの方向名。並びは [`direction_index`] に合わせる。
const DPAD_NAMES: [&str; 8] = ["D↑", "D↗", "D→", "D↘", "D↓", "D↙", "D←", "D↖"];
const DPAD_NAMES: [&str; 8] = ["DP8", "DP9", "DP6", "DP3", "DP2", "DP1", "DP4", "DP7"];

/// 左スティックの方向名。並びは [`direction_index`] に合わせる。
const STICK_NAMES: [&str; 8] = ["L↑", "L↗", "L→", "L↘", "L↓", "L↙", "L←", "L↖"];
const STICK_NAMES: [&str; 8] = ["LS8", "LS9", "LS6", "LS3", "LS2", "LS1", "LS4", "LS7"];

/// ボタン名。単一ビットの値だけを引ける。
const BUTTON_NAMES: [(Buttons, &str); 14] = [
Expand Down Expand Up @@ -267,7 +267,7 @@ impl Timeline {
let deviation = (estimated - nominal).abs() / nominal;
if deviation >= SCALE_WARNING_RATIO {
self.scale_warning = Some(format!(
"デバイス時刻の単位が公称値から {:.0}% 外れている (推定 {estimated:.3}µs/tick、公称 {nominal:.2}µs/tick)",
"device clock unit is {:.0}% off nominal (estimated {estimated:.3} us/tick, nominal {nominal:.2} us/tick)",
deviation * 100.0
));
}
Expand Down
Loading
Loading