Add admin system, registration gate, mudtool database editor, and test checklist
- Add is_admin flag to player DB schema with migration for existing databases - Add server_settings table for key-value config (registration_open, etc.) - Add 10 in-game admin commands: promote, demote, kick, teleport, registration, announce, heal, info, setattitude, list — all gated behind admin flag - Registration gate: new players rejected when registration_open=false, existing players can still reconnect - Add mudtool binary with CLI mode (players/settings/attitudes CRUD) and interactive ratatui TUI with tabbed interface for database management - Restructure to lib.rs + main.rs so mudtool shares DB code with server - Add TESTING.md with comprehensive pre-commit checklist and smoke test script - Stats and who commands show [ADMIN] badge; help shows admin section for admins Made-with: Cursor
This commit is contained in:
625
src/bin/mudtool.rs
Normal file
625
src/bin/mudtool.rs
Normal file
@@ -0,0 +1,625 @@
|
||||
use std::io;
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
|
||||
use crossterm::event::{self, Event, KeyCode, KeyModifiers};
|
||||
use crossterm::terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen};
|
||||
use crossterm::ExecutableCommand;
|
||||
use ratatui::prelude::*;
|
||||
use ratatui::widgets::*;
|
||||
|
||||
use mudserver::db::{GameDb, NpcAttitudeRow, SavedPlayer, SqliteDb};
|
||||
use mudserver::world::Attitude;
|
||||
|
||||
fn main() {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
let mut db_path = PathBuf::from("./mudserver.db");
|
||||
let mut cmd_args: Vec<String> = Vec::new();
|
||||
|
||||
let mut i = 1;
|
||||
while i < args.len() {
|
||||
match args[i].as_str() {
|
||||
"--db" | "-d" => {
|
||||
i += 1;
|
||||
db_path = PathBuf::from(args.get(i).expect("--db requires a path"));
|
||||
}
|
||||
"--help" | "-h" => {
|
||||
print_help();
|
||||
return;
|
||||
}
|
||||
_ => cmd_args.push(args[i].clone()),
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
|
||||
let db = match SqliteDb::open(&db_path) {
|
||||
Ok(db) => db,
|
||||
Err(e) => {
|
||||
eprintln!("Error: {e}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
if cmd_args.is_empty() {
|
||||
print_help();
|
||||
return;
|
||||
}
|
||||
|
||||
match cmd_args[0].as_str() {
|
||||
"tui" => run_tui(db),
|
||||
"players" => cmd_players(&db, &cmd_args[1..]),
|
||||
"settings" => cmd_settings(&db, &cmd_args[1..]),
|
||||
"attitudes" => cmd_attitudes(&db, &cmd_args[1..]),
|
||||
_ => {
|
||||
eprintln!("Unknown command: {}", cmd_args[0]);
|
||||
print_help();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn print_help() {
|
||||
eprintln!("mudtool - MUD Server Database Manager");
|
||||
eprintln!();
|
||||
eprintln!("Usage: mudtool [--db <path>] <command> [args...]");
|
||||
eprintln!();
|
||||
eprintln!("Commands:");
|
||||
eprintln!(" tui Interactive TUI editor");
|
||||
eprintln!(" players list List all players");
|
||||
eprintln!(" players show <name> Show player details");
|
||||
eprintln!(" players set-admin <name> <bool> Set admin flag");
|
||||
eprintln!(" players delete <name> Delete a player");
|
||||
eprintln!(" settings list List all settings");
|
||||
eprintln!(" settings get <key> Get a setting value");
|
||||
eprintln!(" settings set <key> <value> Set a setting value");
|
||||
eprintln!(" attitudes list <player> List NPC attitudes");
|
||||
eprintln!(" attitudes set <player> <npc> <v> Set attitude value");
|
||||
eprintln!();
|
||||
eprintln!("Options:");
|
||||
eprintln!(" --db, -d <path> Database path (default: ./mudserver.db)");
|
||||
}
|
||||
|
||||
// ============ CLI Commands ============
|
||||
|
||||
fn cmd_players(db: &SqliteDb, args: &[String]) {
|
||||
if args.is_empty() {
|
||||
eprintln!("Usage: mudtool players <list|show|set-admin|delete> [args]");
|
||||
return;
|
||||
}
|
||||
match args[0].as_str() {
|
||||
"list" | "ls" => {
|
||||
let players = db.list_all_players();
|
||||
if players.is_empty() {
|
||||
println!("No players found.");
|
||||
return;
|
||||
}
|
||||
println!("{:<20} {:<12} {:<12} {:<5} {:<10} {:<20} {}", "NAME", "RACE", "CLASS", "LVL", "HP", "ROOM", "ADMIN");
|
||||
println!("{}", "-".repeat(90));
|
||||
for p in &players {
|
||||
println!("{:<20} {:<12} {:<12} {:<5} {:<10} {:<20} {}",
|
||||
p.name, short_id(&p.race_id), short_id(&p.class_id),
|
||||
p.level, format!("{}/{}", p.hp, p.max_hp), p.room_id,
|
||||
if p.is_admin { "YES" } else { "no" });
|
||||
}
|
||||
println!("\n{} player(s) total.", players.len());
|
||||
}
|
||||
"show" => {
|
||||
let name = args.get(1).map(|s| s.as_str()).unwrap_or("");
|
||||
if name.is_empty() { eprintln!("Usage: mudtool players show <name>"); return; }
|
||||
match db.load_player(name) {
|
||||
Some(p) => {
|
||||
println!("Player: {}", p.name);
|
||||
println!(" Race: {} | Class: {}", p.race_id, p.class_id);
|
||||
println!(" Level: {} | XP: {}", p.level, p.xp);
|
||||
println!(" HP: {}/{} | ATK: {} | DEF: {}", p.hp, p.max_hp, p.attack, p.defense);
|
||||
println!(" Room: {}", p.room_id);
|
||||
println!(" Admin: {}", p.is_admin);
|
||||
println!(" Inventory: {}", p.inventory_json);
|
||||
if let Some(ref w) = p.equipped_weapon_json { println!(" Weapon: {w}"); }
|
||||
if let Some(ref a) = p.equipped_armor_json { println!(" Armor: {a}"); }
|
||||
let attitudes = db.load_attitudes(name);
|
||||
if !attitudes.is_empty() {
|
||||
println!(" Attitudes:");
|
||||
for att in &attitudes {
|
||||
println!(" {}: {} ({})", att.npc_id, att.value, Attitude::from_value(att.value).label());
|
||||
}
|
||||
}
|
||||
}
|
||||
None => eprintln!("Player '{name}' not found."),
|
||||
}
|
||||
}
|
||||
"set-admin" => {
|
||||
let name = args.get(1).map(|s| s.as_str()).unwrap_or("");
|
||||
let val = args.get(2).map(|s| s.as_str()).unwrap_or("");
|
||||
if name.is_empty() || val.is_empty() { eprintln!("Usage: mudtool players set-admin <name> <true|false>"); return; }
|
||||
let is_admin = matches!(val, "true" | "1" | "yes");
|
||||
if db.set_admin(name, is_admin) {
|
||||
println!("Set {name} admin = {is_admin}");
|
||||
} else {
|
||||
eprintln!("Player '{name}' not found.");
|
||||
}
|
||||
}
|
||||
"delete" => {
|
||||
let name = args.get(1).map(|s| s.as_str()).unwrap_or("");
|
||||
if name.is_empty() { eprintln!("Usage: mudtool players delete <name>"); return; }
|
||||
if db.load_player(name).is_some() {
|
||||
db.delete_player(name);
|
||||
println!("Deleted player '{name}' and their attitudes.");
|
||||
} else {
|
||||
eprintln!("Player '{name}' not found.");
|
||||
}
|
||||
}
|
||||
_ => eprintln!("Unknown subcommand: players {}", args[0]),
|
||||
}
|
||||
}
|
||||
|
||||
fn cmd_settings(db: &SqliteDb, args: &[String]) {
|
||||
if args.is_empty() {
|
||||
eprintln!("Usage: mudtool settings <list|get|set> [args]");
|
||||
return;
|
||||
}
|
||||
match args[0].as_str() {
|
||||
"list" | "ls" => {
|
||||
let settings = db.list_settings();
|
||||
if settings.is_empty() { println!("No settings configured."); return; }
|
||||
println!("{:<30} {}", "KEY", "VALUE");
|
||||
println!("{}", "-".repeat(50));
|
||||
for (k, v) in &settings { println!("{:<30} {v}", k); }
|
||||
}
|
||||
"get" => {
|
||||
let key = args.get(1).map(|s| s.as_str()).unwrap_or("");
|
||||
if key.is_empty() { eprintln!("Usage: mudtool settings get <key>"); return; }
|
||||
match db.get_setting(key) {
|
||||
Some(v) => println!("{key} = {v}"),
|
||||
None => println!("{key} is not set (will use default)."),
|
||||
}
|
||||
}
|
||||
"set" => {
|
||||
let key = args.get(1).map(|s| s.as_str()).unwrap_or("");
|
||||
let val = args.get(2).map(|s| s.as_str()).unwrap_or("");
|
||||
if key.is_empty() || val.is_empty() { eprintln!("Usage: mudtool settings set <key> <value>"); return; }
|
||||
db.set_setting(key, val);
|
||||
println!("Set {key} = {val}");
|
||||
}
|
||||
_ => eprintln!("Unknown subcommand: settings {}", args[0]),
|
||||
}
|
||||
}
|
||||
|
||||
fn cmd_attitudes(db: &SqliteDb, args: &[String]) {
|
||||
if args.is_empty() {
|
||||
eprintln!("Usage: mudtool attitudes <list|set> [args]");
|
||||
return;
|
||||
}
|
||||
match args[0].as_str() {
|
||||
"list" | "ls" => {
|
||||
let player = args.get(1).map(|s| s.as_str()).unwrap_or("");
|
||||
if player.is_empty() { eprintln!("Usage: mudtool attitudes list <player>"); return; }
|
||||
let attitudes = db.load_attitudes(player);
|
||||
if attitudes.is_empty() { println!("No attitudes for '{player}'."); return; }
|
||||
println!("Attitudes for {player}:");
|
||||
println!("{:<30} {:<8} {}", "NPC", "VALUE", "LABEL");
|
||||
println!("{}", "-".repeat(50));
|
||||
for att in &attitudes {
|
||||
println!("{:<30} {:<8} {}", att.npc_id, att.value, Attitude::from_value(att.value).label());
|
||||
}
|
||||
}
|
||||
"set" => {
|
||||
let player = args.get(1).map(|s| s.as_str()).unwrap_or("");
|
||||
let npc = args.get(2).map(|s| s.as_str()).unwrap_or("");
|
||||
let val = args.get(3).and_then(|s| s.parse::<i32>().ok());
|
||||
if player.is_empty() || npc.is_empty() || val.is_none() {
|
||||
eprintln!("Usage: mudtool attitudes set <player> <npc_id> <value>");
|
||||
return;
|
||||
}
|
||||
let v = val.unwrap().clamp(-100, 100);
|
||||
db.save_attitude(player, npc, v);
|
||||
println!("Set {npc} attitude toward {player} = {v} ({})", Attitude::from_value(v).label());
|
||||
}
|
||||
_ => eprintln!("Unknown subcommand: attitudes {}", args[0]),
|
||||
}
|
||||
}
|
||||
|
||||
fn short_id(id: &str) -> &str {
|
||||
id.split(':').last().unwrap_or(id)
|
||||
}
|
||||
|
||||
// ============ TUI ============
|
||||
|
||||
struct App {
|
||||
db: SqliteDb,
|
||||
tab: usize,
|
||||
running: bool,
|
||||
|
||||
players: Vec<SavedPlayer>,
|
||||
player_state: TableState,
|
||||
|
||||
settings: Vec<(String, String)>,
|
||||
setting_state: TableState,
|
||||
|
||||
attitude_players: Vec<String>,
|
||||
attitude_player_idx: usize,
|
||||
attitudes: Vec<NpcAttitudeRow>,
|
||||
attitude_state: TableState,
|
||||
|
||||
mode: AppMode,
|
||||
input_buf: String,
|
||||
status: String,
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
enum AppMode {
|
||||
Normal,
|
||||
EditSetting { key: String },
|
||||
EditAttitude { npc_id: String },
|
||||
ConfirmDelete { name: String },
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn new(db: SqliteDb) -> Self {
|
||||
let mut app = App {
|
||||
db,
|
||||
tab: 0,
|
||||
running: true,
|
||||
players: Vec::new(),
|
||||
player_state: TableState::default(),
|
||||
settings: Vec::new(),
|
||||
setting_state: TableState::default(),
|
||||
attitude_players: Vec::new(),
|
||||
attitude_player_idx: 0,
|
||||
attitudes: Vec::new(),
|
||||
attitude_state: TableState::default(),
|
||||
mode: AppMode::Normal,
|
||||
input_buf: String::new(),
|
||||
status: String::new(),
|
||||
};
|
||||
app.refresh_all();
|
||||
app
|
||||
}
|
||||
|
||||
fn refresh_all(&mut self) {
|
||||
self.players = self.db.list_all_players();
|
||||
if !self.players.is_empty() && self.player_state.selected().is_none() {
|
||||
self.player_state.select(Some(0));
|
||||
}
|
||||
self.settings = self.db.list_settings();
|
||||
if !self.settings.is_empty() && self.setting_state.selected().is_none() {
|
||||
self.setting_state.select(Some(0));
|
||||
}
|
||||
self.attitude_players = self.players.iter().map(|p| p.name.clone()).collect();
|
||||
self.refresh_attitudes();
|
||||
}
|
||||
|
||||
fn refresh_attitudes(&mut self) {
|
||||
if let Some(name) = self.attitude_players.get(self.attitude_player_idx) {
|
||||
self.attitudes = self.db.load_attitudes(name);
|
||||
} else {
|
||||
self.attitudes.clear();
|
||||
}
|
||||
if !self.attitudes.is_empty() && self.attitude_state.selected().is_none() {
|
||||
self.attitude_state.select(Some(0));
|
||||
}
|
||||
}
|
||||
|
||||
fn selected_player(&self) -> Option<&SavedPlayer> {
|
||||
self.player_state.selected().and_then(|i| self.players.get(i))
|
||||
}
|
||||
}
|
||||
|
||||
fn run_tui(db: SqliteDb) {
|
||||
enable_raw_mode().unwrap();
|
||||
io::stdout().execute(EnterAlternateScreen).unwrap();
|
||||
let backend = CrosstermBackend::new(io::stdout());
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
|
||||
let mut app = App::new(db);
|
||||
|
||||
while app.running {
|
||||
terminal.draw(|f| ui(f, &mut app)).unwrap();
|
||||
if event::poll(Duration::from_millis(100)).unwrap() {
|
||||
if let Event::Key(key) = event::read().unwrap() {
|
||||
handle_key(&mut app, key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
disable_raw_mode().unwrap();
|
||||
io::stdout().execute(LeaveAlternateScreen).unwrap();
|
||||
}
|
||||
|
||||
fn ui(f: &mut Frame, app: &mut App) {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
Constraint::Length(3),
|
||||
Constraint::Min(5),
|
||||
Constraint::Length(3),
|
||||
])
|
||||
.split(f.area());
|
||||
|
||||
// Tab bar
|
||||
let tabs = Tabs::new(vec!["[1] Players", "[2] Settings", "[3] Attitudes"])
|
||||
.select(app.tab)
|
||||
.highlight_style(Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD))
|
||||
.block(Block::default().borders(Borders::ALL).title(" MUD Tool "));
|
||||
f.render_widget(tabs, chunks[0]);
|
||||
|
||||
// Content
|
||||
match app.tab {
|
||||
0 => render_players(f, app, chunks[1]),
|
||||
1 => render_settings(f, app, chunks[1]),
|
||||
2 => render_attitudes(f, app, chunks[1]),
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// Status bar
|
||||
let status_text = if app.mode != AppMode::Normal {
|
||||
match &app.mode {
|
||||
AppMode::EditSetting { key } => format!("Edit {key}: {}_", app.input_buf),
|
||||
AppMode::EditAttitude { npc_id } => format!("Set {npc_id} value: {}_", app.input_buf),
|
||||
AppMode::ConfirmDelete { name } => format!("Delete '{name}'? (y/n)"),
|
||||
_ => String::new(),
|
||||
}
|
||||
} else if !app.status.is_empty() {
|
||||
app.status.clone()
|
||||
} else {
|
||||
match app.tab {
|
||||
0 => "↑↓ nav | a toggle admin | d delete | 1/2/3 tabs | q quit".into(),
|
||||
1 => "↑↓ nav | Enter edit | n new | d delete | 1/2/3 tabs | q quit".into(),
|
||||
2 => "↑↓ nav | Enter edit | ←→ player | 1/2/3 tabs | q quit".into(),
|
||||
_ => String::new(),
|
||||
}
|
||||
};
|
||||
let status = Paragraph::new(status_text)
|
||||
.block(Block::default().borders(Borders::ALL));
|
||||
f.render_widget(status, chunks[2]);
|
||||
}
|
||||
|
||||
fn render_players(f: &mut Frame, app: &mut App, area: Rect) {
|
||||
let header = Row::new(vec!["Name", "Race", "Class", "Lvl", "HP", "Room", "Admin"])
|
||||
.style(Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD));
|
||||
|
||||
let rows: Vec<Row> = app.players.iter().map(|p| {
|
||||
Row::new(vec![
|
||||
p.name.clone(),
|
||||
short_id(&p.race_id).to_string(),
|
||||
short_id(&p.class_id).to_string(),
|
||||
p.level.to_string(),
|
||||
format!("{}/{}", p.hp, p.max_hp),
|
||||
p.room_id.clone(),
|
||||
if p.is_admin { "YES".into() } else { "no".into() },
|
||||
])
|
||||
}).collect();
|
||||
|
||||
let widths = [
|
||||
Constraint::Min(18), Constraint::Length(10), Constraint::Length(10),
|
||||
Constraint::Length(5), Constraint::Length(10), Constraint::Min(18),
|
||||
Constraint::Length(6),
|
||||
];
|
||||
let table = Table::new(rows, widths)
|
||||
.header(header)
|
||||
.row_highlight_style(Style::default().bg(Color::DarkGray).add_modifier(Modifier::BOLD))
|
||||
.highlight_symbol("▸ ")
|
||||
.block(Block::default().borders(Borders::ALL).title(format!(" Players ({}) ", app.players.len())));
|
||||
|
||||
f.render_stateful_widget(table, area, &mut app.player_state);
|
||||
}
|
||||
|
||||
fn render_settings(f: &mut Frame, app: &mut App, area: Rect) {
|
||||
let header = Row::new(vec!["Key", "Value"])
|
||||
.style(Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD));
|
||||
|
||||
let rows: Vec<Row> = app.settings.iter().map(|(k, v)| {
|
||||
Row::new(vec![k.clone(), v.clone()])
|
||||
}).collect();
|
||||
|
||||
let widths = [Constraint::Min(30), Constraint::Min(30)];
|
||||
let table = Table::new(rows, widths)
|
||||
.header(header)
|
||||
.row_highlight_style(Style::default().bg(Color::DarkGray).add_modifier(Modifier::BOLD))
|
||||
.highlight_symbol("▸ ")
|
||||
.block(Block::default().borders(Borders::ALL).title(format!(" Settings ({}) ", app.settings.len())));
|
||||
|
||||
f.render_stateful_widget(table, area, &mut app.setting_state);
|
||||
}
|
||||
|
||||
fn render_attitudes(f: &mut Frame, app: &mut App, area: Rect) {
|
||||
let player_name = app.attitude_players.get(app.attitude_player_idx)
|
||||
.cloned().unwrap_or_else(|| "(none)".into());
|
||||
|
||||
let header = Row::new(vec!["NPC", "Value", "Attitude"])
|
||||
.style(Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD));
|
||||
|
||||
let rows: Vec<Row> = app.attitudes.iter().map(|att| {
|
||||
let label = Attitude::from_value(att.value).label().to_string();
|
||||
Row::new(vec![att.npc_id.clone(), att.value.to_string(), label])
|
||||
}).collect();
|
||||
|
||||
let widths = [Constraint::Min(30), Constraint::Length(8), Constraint::Length(12)];
|
||||
let title = format!(" Attitudes for: {} (←→ {}/{}) ", player_name,
|
||||
app.attitude_player_idx + 1, app.attitude_players.len().max(1));
|
||||
let table = Table::new(rows, widths)
|
||||
.header(header)
|
||||
.row_highlight_style(Style::default().bg(Color::DarkGray).add_modifier(Modifier::BOLD))
|
||||
.highlight_symbol("▸ ")
|
||||
.block(Block::default().borders(Borders::ALL).title(title));
|
||||
|
||||
f.render_stateful_widget(table, area, &mut app.attitude_state);
|
||||
}
|
||||
|
||||
fn handle_key(app: &mut App, key: event::KeyEvent) {
|
||||
// Handle input modes first
|
||||
match &app.mode {
|
||||
AppMode::EditSetting { .. } | AppMode::EditAttitude { .. } => {
|
||||
match key.code {
|
||||
KeyCode::Enter => {
|
||||
let val = app.input_buf.clone();
|
||||
match std::mem::replace(&mut app.mode, AppMode::Normal) {
|
||||
AppMode::EditSetting { key } => {
|
||||
app.db.set_setting(&key, &val);
|
||||
app.status = format!("Set {key} = {val}");
|
||||
}
|
||||
AppMode::EditAttitude { npc_id } => {
|
||||
if let Ok(v) = val.parse::<i32>() {
|
||||
let v = v.clamp(-100, 100);
|
||||
if let Some(pname) = app.attitude_players.get(app.attitude_player_idx) {
|
||||
app.db.save_attitude(pname, &npc_id, v);
|
||||
app.status = format!("Set {npc_id} = {v}");
|
||||
}
|
||||
} else {
|
||||
app.status = "Invalid number.".into();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
app.input_buf.clear();
|
||||
app.refresh_all();
|
||||
}
|
||||
KeyCode::Esc => {
|
||||
app.mode = AppMode::Normal;
|
||||
app.input_buf.clear();
|
||||
app.status.clear();
|
||||
}
|
||||
KeyCode::Backspace => { app.input_buf.pop(); }
|
||||
KeyCode::Char(c) => app.input_buf.push(c),
|
||||
_ => {}
|
||||
}
|
||||
return;
|
||||
}
|
||||
AppMode::ConfirmDelete { .. } => {
|
||||
match key.code {
|
||||
KeyCode::Char('y') | KeyCode::Char('Y') => {
|
||||
if let AppMode::ConfirmDelete { name } = std::mem::replace(&mut app.mode, AppMode::Normal) {
|
||||
app.db.delete_player(&name);
|
||||
app.status = format!("Deleted {name}.");
|
||||
app.player_state.select(Some(0));
|
||||
app.refresh_all();
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
app.mode = AppMode::Normal;
|
||||
app.status = "Cancelled.".into();
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
AppMode::Normal => {}
|
||||
}
|
||||
|
||||
// Normal mode
|
||||
if key.code == KeyCode::Char('c') && key.modifiers.contains(KeyModifiers::CONTROL) {
|
||||
app.running = false;
|
||||
return;
|
||||
}
|
||||
|
||||
match key.code {
|
||||
KeyCode::Char('q') | KeyCode::Esc => app.running = false,
|
||||
KeyCode::Char('1') => { app.tab = 0; app.status.clear(); }
|
||||
KeyCode::Char('2') => { app.tab = 1; app.status.clear(); }
|
||||
KeyCode::Char('3') => { app.tab = 2; app.status.clear(); }
|
||||
KeyCode::Tab => { app.tab = (app.tab + 1) % 3; app.status.clear(); }
|
||||
KeyCode::Up => nav_up(app),
|
||||
KeyCode::Down => nav_down(app),
|
||||
KeyCode::Left => {
|
||||
if app.tab == 2 && !app.attitude_players.is_empty() {
|
||||
app.attitude_player_idx = app.attitude_player_idx.checked_sub(1)
|
||||
.unwrap_or(app.attitude_players.len() - 1);
|
||||
app.attitude_state.select(Some(0));
|
||||
app.refresh_attitudes();
|
||||
}
|
||||
}
|
||||
KeyCode::Right => {
|
||||
if app.tab == 2 && !app.attitude_players.is_empty() {
|
||||
app.attitude_player_idx = (app.attitude_player_idx + 1) % app.attitude_players.len();
|
||||
app.attitude_state.select(Some(0));
|
||||
app.refresh_attitudes();
|
||||
}
|
||||
}
|
||||
KeyCode::Char('a') if app.tab == 0 => {
|
||||
if let Some(p) = app.selected_player() {
|
||||
let name = p.name.clone();
|
||||
let new_val = !p.is_admin;
|
||||
app.db.set_admin(&name, new_val);
|
||||
app.status = format!("{name} admin = {new_val}");
|
||||
app.refresh_all();
|
||||
}
|
||||
}
|
||||
KeyCode::Char('d') if app.tab == 0 => {
|
||||
if let Some(p) = app.selected_player() {
|
||||
app.mode = AppMode::ConfirmDelete { name: p.name.clone() };
|
||||
}
|
||||
}
|
||||
KeyCode::Char('d') if app.tab == 1 => {
|
||||
if let Some(idx) = app.setting_state.selected() {
|
||||
if let Some((key, _)) = app.settings.get(idx) {
|
||||
let key = key.clone();
|
||||
// Delete by setting empty then removing via raw SQL isn't in trait, just set ""
|
||||
app.db.set_setting(&key, "");
|
||||
app.status = format!("Cleared {key}");
|
||||
app.refresh_all();
|
||||
}
|
||||
}
|
||||
}
|
||||
KeyCode::Enter => {
|
||||
match app.tab {
|
||||
1 => {
|
||||
if let Some(idx) = app.setting_state.selected() {
|
||||
if let Some((key, val)) = app.settings.get(idx) {
|
||||
app.mode = AppMode::EditSetting { key: key.clone() };
|
||||
app.input_buf = val.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
2 => {
|
||||
if let Some(idx) = app.attitude_state.selected() {
|
||||
if let Some(att) = app.attitudes.get(idx) {
|
||||
app.mode = AppMode::EditAttitude { npc_id: att.npc_id.clone() };
|
||||
app.input_buf = att.value.to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
KeyCode::Char('n') if app.tab == 1 => {
|
||||
app.mode = AppMode::EditSetting { key: "new_key".into() };
|
||||
app.input_buf.clear();
|
||||
app.status = "Enter value for 'new_key' (rename key later with CLI):".into();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn nav_up(app: &mut App) {
|
||||
let state = match app.tab {
|
||||
0 => &mut app.player_state,
|
||||
1 => &mut app.setting_state,
|
||||
2 => &mut app.attitude_state,
|
||||
_ => return,
|
||||
};
|
||||
let len = match app.tab {
|
||||
0 => app.players.len(),
|
||||
1 => app.settings.len(),
|
||||
2 => app.attitudes.len(),
|
||||
_ => 0,
|
||||
};
|
||||
if len == 0 { return; }
|
||||
let i = state.selected().unwrap_or(0);
|
||||
state.select(Some(if i == 0 { len - 1 } else { i - 1 }));
|
||||
}
|
||||
|
||||
fn nav_down(app: &mut App) {
|
||||
let state = match app.tab {
|
||||
0 => &mut app.player_state,
|
||||
1 => &mut app.setting_state,
|
||||
2 => &mut app.attitude_state,
|
||||
_ => return,
|
||||
};
|
||||
let len = match app.tab {
|
||||
0 => app.players.len(),
|
||||
1 => app.settings.len(),
|
||||
2 => app.attitudes.len(),
|
||||
_ => 0,
|
||||
};
|
||||
if len == 0 { return; }
|
||||
let i = state.selected().unwrap_or(0);
|
||||
state.select(Some((i + 1) % len));
|
||||
}
|
||||
Reference in New Issue
Block a user