Added minimal deck editor.

This commit is contained in:
Filipe Rodrigues 2021-06-01 02:06:06 +01:00
parent f384b95825
commit 98e9bf8a76
6 changed files with 480 additions and 71 deletions

View File

@ -25,5 +25,6 @@ members = [
"dcb-tools/dcb-unbin",
"dcb-tools/dcb-unmsd",
"dcb-tools/dcb-card-editor",
"dcb-tools/dcb-deck-editor",
"dcb-tools/dcb-file-editor",
]

View File

@ -0,0 +1,32 @@
[package]
name = "dcb-deck-editor"
version = "0.1.0"
authors = ["Filipe Rodrigues <filipejacintorodrigues1@gmail.com>"]
edition = "2018"
[dependencies]
# Dcb
dcb = { path = "../../dcb" }
dcb-bytes = { path = "../../dcb-bytes" }
dcb-util = { path = "../../dcb-util" }
dcb-cdrom-xa = { path = "../../dcb-cdrom-xa" }
# Gui
eframe = { git = "https://github.com/emilk/egui" }
# Logging
log = "0.4.14"
simplelog = "0.10.0"
# Error
anyhow = "1.0.40"
# Utils
native-dialog = "0.5.5"
either = "1.6.1"
ref-cast = "1.0.6"
derive_more = "0.99.13"
strum = { version = "0.20.0", features = ["derive"] }
# Serde
serde_yaml = "0.8.17"

View File

@ -0,0 +1,55 @@
//! Ascii text buffer
use dcb_util::{ascii_str_arr::AsciiChar, AsciiStrArr};
/// An ascii text buffer
#[derive(PartialEq, Default, Clone, Debug, derive_more::Display)]
#[derive(ref_cast::RefCast)]
#[repr(transparent)]
pub struct AsciiTextBuffer<const N: usize>(pub AsciiStrArr<N>);
// Truncates any extra characters and ignores non-ascii
impl<const N: usize> From<String> for AsciiTextBuffer<N> {
fn from(s: String) -> Self {
let mut buffer = Self::default();
for ch in s.chars() {
match AsciiChar::from_ascii(ch) {
Ok(ch) => match buffer.0.push(ch) {
Ok(_) => continue,
Err(_) => break,
},
Err(_) => continue,
}
}
buffer
}
}
impl<const N: usize> From<AsciiTextBuffer<N>> for String {
fn from(buffer: AsciiTextBuffer<N>) -> Self {
buffer.as_ref().to_owned()
}
}
impl<const N: usize> AsRef<str> for AsciiTextBuffer<N> {
fn as_ref(&self) -> &str {
self.0.as_str()
}
}
// Note: In ascii, the character index is the same
// as the byte index.
impl<const N: usize> eframe::egui::widgets::TextBuffer for AsciiTextBuffer<N> {
fn insert_text(&mut self, text: &str, ch_idx: usize) -> usize {
text.chars()
.filter_map(|ch| AsciiChar::from_ascii(ch).ok())
.enumerate()
.take_while(|&(idx, ch)| self.0.insert(ch_idx + idx, ch).is_ok())
.count()
}
fn delete_char_range(&mut self, ch_range: std::ops::Range<usize>) {
self.0.drain_range(ch_range);
}
}

View File

@ -0,0 +1,379 @@
//! Deck editor
// Features
#![feature(array_map, with_options, format_args_capture, once_cell, never_type)]
// Modules
pub mod ascii_text_buffer;
// Exports
pub use ascii_text_buffer::AsciiTextBuffer;
// Imports
use anyhow::Context;
use dcb::{CardTable, Deck, DeckTable};
use dcb_util::StrContainsCaseInsensitive;
use eframe::{egui, epi, NativeOptions};
use native_dialog::{FileDialog, MessageDialog, MessageType};
use ref_cast::RefCast;
use std::{
fs,
io::{self, Read, Seek},
ops::Range,
path::{Path, PathBuf},
};
fn main() {
// Initialize the logger
simplelog::TermLogger::init(
log::LevelFilter::Debug,
simplelog::Config::default(),
simplelog::TerminalMode::Stderr,
simplelog::ColorChoice::Auto,
)
.expect("Unable to initialize logger");
// Crate the app and run it
let app = DeckEditor::default();
eframe::run_native(Box::new(app), NativeOptions::default());
}
pub struct DeckEditor {
/// File path
file_path: Option<PathBuf>,
/// Loaded game
loaded_game: Option<LoadedGame>,
/// Deck search
deck_search: String,
/// All selected edit screens
open_edit_screens: Vec<EditScreen>,
}
impl DeckEditor {
/// Card table offset
pub const CARD_TABLE_OFFSET: u64 = 0x216d000;
/// Card table size
pub const CARD_TABLE_SIZE: u64 = 0x14958;
/// Deck table offset
pub const DECK_TABLE_OFFSET: u64 = 0x21a6800;
/// Deck table size
pub const DECK_TABLE_SIZE: u64 = 0x445a;
/// Parses the card table from file
pub fn parse_card_table(file_path: &Path) -> Result<CardTable, anyhow::Error> {
// Open the file
let file = fs::File::open(file_path).context("Unable to open file")?;
let mut file = dcb_cdrom_xa::CdRomCursor::new(file);
// Seek to the card file position and limit our reading to the file size
file.seek(io::SeekFrom::Start(Self::CARD_TABLE_OFFSET))
.context("Unable to seek to card table")?;
let mut file = file.take(Self::CARD_TABLE_SIZE);
// Then parse it
let card_table = CardTable::deserialize(&mut file).context("Unable to parse table")?;
Ok(card_table)
}
/// Parses the deck table from file
pub fn parse_deck_table(file_path: &Path) -> Result<DeckTable, anyhow::Error> {
// Open the file
let file = fs::File::open(file_path).context("Unable to open file")?;
let mut file = dcb_cdrom_xa::CdRomCursor::new(file);
// Seek to the deck file position and limit our reading to the file size
file.seek(io::SeekFrom::Start(Self::DECK_TABLE_OFFSET))
.context("Unable to seek to deck table")?;
let mut file = file.take(Self::DECK_TABLE_SIZE);
// Then parse it
let deck_table = DeckTable::deserialize(&mut file).context("Unable to parse table")?;
Ok(deck_table)
}
/// Saves the deck table to file
pub fn save_deck_table(file_path: &Path, deck_table: &DeckTable) -> Result<(), anyhow::Error> {
// Open the file
let file = fs::File::with_options()
.write(true)
.open(file_path)
.context("Unable to open file")?;
let mut file = dcb_cdrom_xa::CdRomCursor::new(file);
// Seek to the deck file position and limit our writing to the file size
file.seek(io::SeekFrom::Start(Self::DECK_TABLE_OFFSET))
.context("Unable to seek to deck table")?;
let mut file = dcb_util::WriteTake::new(file, Self::DECK_TABLE_SIZE);
// Then serialize it
deck_table.serialize(&mut file).context("Unable to serialize table")?;
Ok(())
}
/// Returns the digimon's indexes
pub fn digimon_idxs(card_table: &CardTable) -> Range<usize> {
0..card_table.digimons.len()
}
/// Returns the item's indexes
pub fn item_idxs(card_table: &CardTable) -> Range<usize> {
card_table.digimons.len()..(card_table.digimons.len() + card_table.items.len())
}
/// Returns the digivolve's indexes
pub fn digivolve_idxs(card_table: &CardTable) -> Range<usize> {
(card_table.digimons.len() + card_table.items.len())..
(card_table.digimons.len() + card_table.items.len() + card_table.digivolves.len())
}
/// Returns a card given it's index
pub fn get_card_from_idx(card_table: &mut CardTable, idx: usize) -> Card {
let digimons_len = card_table.digimons.len();
let items_len = card_table.items.len();
if Self::digimon_idxs(card_table).contains(&idx) {
Card::Digimon(&mut card_table.digimons[idx])
} else if Self::item_idxs(card_table).contains(&idx) {
Card::Item(&mut card_table.items[idx - digimons_len])
} else if Self::digivolve_idxs(card_table).contains(&idx) {
Card::Digivolve(&mut card_table.digivolves[idx - digimons_len - items_len])
} else {
panic!("Invalid card index");
}
}
}
impl Default for DeckEditor {
fn default() -> Self {
Self {
file_path: None,
loaded_game: None,
deck_search: String::new(),
open_edit_screens: vec![],
}
}
}
impl epi::App for DeckEditor {
fn update(&mut self, ctx: &egui::CtxRef, frame: &mut epi::Frame<'_>) {
let Self {
file_path,
loaded_game,
deck_search,
open_edit_screens,
} = self;
// Top panel
egui::TopBottomPanel::top("top_panel").show(ctx, |ui| {
egui::menu::bar(ui, |ui| {
egui::menu::menu(ui, "File", |ui| {
// On open, ask the user and open the file
if ui.button("Open").clicked() {
let cur_dir_path = std::env::current_dir().expect("Unable to get current directory path");
*file_path = FileDialog::new()
.set_location(&cur_dir_path)
.add_filter("Game file", &["bin"])
.show_open_single_file()
.expect("Unable to ask user for file");
// Then load the card table if we got a file
if let Some(file_path) = file_path {
match (
Self::parse_card_table(file_path).context("Unable to load card table"),
Self::parse_deck_table(file_path).context("Unable to load deck table"),
) {
(Ok(card_table), Ok(deck_table)) => {
*loaded_game = Some(LoadedGame { card_table, deck_table });
},
(Err(err), _) => MessageDialog::new()
.set_text(&format!("Unable to open file: {:?}", err))
.set_type(MessageType::Error)
.show_alert()
.expect("Unable to alert user"),
(_, Err(err)) => MessageDialog::new()
.set_text(&format!("Unable to open file: {:?}", err))
.set_type(MessageType::Error)
.show_alert()
.expect("Unable to alert user"),
}
}
}
// On save, if we have a file, save it to there, else tell error
if ui.button("Save").clicked() {
match (&file_path, &mut *loaded_game) {
(Some(file_path), Some(loaded_game)) => {
match Self::save_deck_table(file_path, &loaded_game.deck_table) {
Ok(()) => MessageDialog::new()
.set_text("Successfully saved!")
.set_type(MessageType::Info)
.show_alert()
.expect("Unable to alert user"),
Err(err) => MessageDialog::new()
.set_text(&format!("Unable to save file: {:?}", err))
.set_type(MessageType::Error)
.show_alert()
.expect("Unable to alert user"),
}
},
_ => MessageDialog::new()
.set_text("You must first open a file to save")
.set_type(MessageType::Warning)
.show_alert()
.expect("Unable to alert user"),
}
}
if ui.button("Quit").clicked() {
frame.quit();
}
});
/*
egui::menu::menu(ui, "Edit", |ui| {
if loaded_game.is_some() && ui.button("Swap").clicked() {
todo!();
}
});
*/
});
});
egui::SidePanel::left("side_panel").show(ctx, |ui| {
ui.heading("Deck list");
ui.vertical(|ui| {
ui.label("Search");
ui.text_edit_singleline(deck_search);
});
// If we have a loaded game, display all decks
if let Some(loaded_game) = &loaded_game {
let names = loaded_game
.deck_table
.decks
.iter()
.map(|deck| deck.name)
.enumerate()
.map(|(idx, name)| (idx, format!("{idx}. {name}")))
.filter(|(_, name)| name.contains_case_insensitive(deck_search));
egui::ScrollArea::auto_sized().show(ui, |ui| {
for (idx, name) in names {
// If clicked, open/close a new screen
let screen_idx = open_edit_screens.iter().position(|screen| screen.deck_idx == idx);
if ui.selectable_label(screen_idx.is_some(), name).clicked() {
match screen_idx {
Some(screen_idx) => {
open_edit_screens.remove(screen_idx);
},
None => open_edit_screens.push(EditScreen { deck_idx: idx }),
}
}
}
});
}
});
// For every screen, display it
if let Some(loaded_game) = loaded_game {
egui::CentralPanel::default().show(ctx, |ui| {
let screens_len = open_edit_screens.len();
for screen in open_edit_screens {
let deck = &mut loaded_game.deck_table.decks[screen.deck_idx];
let card_table = &mut loaded_game.card_table;
let total_available_width = ui.available_width();
let default_width = total_available_width / (screens_len as f32);
egui::SidePanel::left((screen as *const _, "panel", default_width.to_bits()))
.default_width(default_width)
.show(ctx, |ui| {
// Header for the card
ui.vertical(|ui| {
ui.heading(deck.name.as_str());
ui.separator();
});
egui::ScrollArea::auto_sized().show(ui, |ui| {
self::render_deck(ui, deck, card_table);
});
});
}
});
}
}
fn on_exit(&mut self) {
todo!();
}
fn name(&self) -> &str {
"Dcb deck editor"
}
}
/// An edit screen
pub struct EditScreen {
/// Currently selected deck
deck_idx: usize,
}
/// Loaded game
pub struct LoadedGame {
/// Card table
card_table: CardTable,
/// Deck table
deck_table: DeckTable,
}
/// Digimon, Item or digivolve
pub enum Card<'a> {
Digimon(&'a mut dcb::Digimon),
Item(&'a mut dcb::Item),
Digivolve(&'a mut dcb::Digivolve),
}
impl<'a> Card<'a> {
/// Returns the name of this card
pub fn name(&self) -> &str {
match self {
Card::Digimon(digimon) => digimon.name.as_str(),
Card::Item(item) => item.name.as_str(),
Card::Digivolve(digivolve) => digivolve.name.as_str(),
}
}
}
/// Renders a deck
fn render_deck(ui: &mut egui::Ui, deck: &mut Deck, card_table: &mut CardTable) {
// Name
ui.horizontal(|ui| {
ui.label("Name");
ui.text_edit_singleline(AsciiTextBuffer::ref_cast_mut(&mut deck.name));
});
// Owner
ui.horizontal(|ui| {
ui.label("Owner");
ui.text_edit_singleline(AsciiTextBuffer::ref_cast_mut(&mut deck.owner));
});
ui.group(|ui| {
ui.label("Cards");
for card_id in &mut deck.cards {
ui.horizontal(|ui| {
let card = DeckEditor::get_card_from_idx(card_table, usize::from(card_id.0));
ui.add(egui::Slider::new(&mut card_id.0, 0..=300).clamp_to_range(true));
ui.label(card.name());
});
}
});
}

View File

@ -8,9 +8,10 @@ pub use error::{DeserializeError, SerializeError};
// Imports
use crate::Deck;
use byteorder::{ByteOrder, LittleEndian};
use dcb_bytes::Bytes;
use dcb_io::GameFile;
use std::io;
use dcb_util::array_split_mut;
use std::{convert::TryInto, io};
/// The decks table, where all decks are stored
#[derive(PartialEq, Eq, Clone, Debug)]
@ -18,7 +19,7 @@ use std::io;
#[allow(clippy::unsafe_derive_deserialize)] // False positive
pub struct Table {
/// All decks
decks: Vec<Deck>,
pub decks: Vec<Deck>,
}
// Constants
@ -28,27 +29,15 @@ impl Table {
/// The magic in the table header
/// = "33KD"
pub const HEADER_MAGIC: u32 = 0x444b3033;
/*
/// The max size of the deck table
// TODO: Verify this
pub const MAX_BYTE_SIZE: usize = 0x4452;
/// The start address of the decks table
const START_ADDRESS: Data = Data::from_u64(0x21a6800);
*/
}
impl Table {
/// Deserializes the deck table from `file`.
pub fn deserialize<R: io::Read>(_file: &mut GameFile<R>) -> Result<Self, DeserializeError> {
todo!();
/*
// Seek to the beginning of the deck table
file.seek(std::io::SeekFrom::Start(Self::START_ADDRESS.as_u64()))
.map_err(DeserializeError::Seek)?;
pub fn deserialize<R: io::Read>(file: &mut R) -> Result<Self, DeserializeError> {
// Read header
let mut header_bytes = [0u8; Self::HEADER_BYTE_SIZE];
file.read_exact(&mut header_bytes).map_err(DeserializeError::ReadHeader)?;
file.read_exact(&mut header_bytes)
.map_err(DeserializeError::ReadHeader)?;
// Check if the magic is right
let magic = LittleEndian::read_u32(&header_bytes[0x0..0x4]);
@ -60,17 +49,13 @@ impl Table {
let decks_count: usize = header_bytes[0x4].into();
log::trace!("Found {decks_count} decks");
// If there are too many decks, return Err
if decks_count * std::mem::size_of::<<Deck as Bytes>::ByteArray>() > Self::MAX_BYTE_SIZE {
return Err(DeserializeError::TooManyDecks { decks_count });
}
// Then get each deck
let mut decks = vec![];
for id in 0..decks_count {
// Read all bytes of the deck
let mut bytes = [0; 0x6e];
file.read_exact(&mut bytes).map_err(|err| DeserializeError::ReadDeck { id, err })?;
file.read_exact(&mut bytes)
.map_err(|err| DeserializeError::ReadDeck { id, err })?;
// And try to serialize the deck
let deck = Deck::from_bytes(&bytes).map_err(|err| DeserializeError::DeserializeDeck { id, err })?;
@ -84,25 +69,10 @@ impl Table {
// And return the table
Ok(Self { decks })
*/
}
/// Serializes the deck table to `file`
pub fn serialize<R: io::Write>(&self, _file: &mut GameFile<R>) -> Result<(), SerializeError> {
let _ = self;
todo!();
/*
// If the total table size is bigger than the max, return Err
if self.decks.len() * std::mem::size_of::<<Deck as Bytes>::ByteArray>() > Self::MAX_BYTE_SIZE {
return Err(SerializeError::TooManyDecks {
decks_count: self.decks.len(),
});
}
// Seek to the beginning of the deck table
file.seek(std::io::SeekFrom::Start(Self::START_ADDRESS.as_u64()))
.map_err(SerializeError::Seek)?;
pub fn serialize<R: io::Write>(&self, file: &mut R) -> Result<(), SerializeError> {
// Write header
let mut header_bytes = [0u8; 0x8];
let header = array_split_mut!(&mut header_bytes,
@ -129,11 +99,11 @@ impl Table {
deck.to_bytes(&mut bytes).into_ok();
// And write them to file
file.write(&bytes).map_err(|err| SerializeError::WriteDeck { id, err })?;
file.write(&bytes)
.map_err(|err| SerializeError::WriteDeck { id, err })?;
}
// And return Ok
Ok(())
*/
}
}

View File

@ -1,16 +1,12 @@
//! Errors
// Imports
use super::{Bytes, Deck, Table};
use super::Table;
use crate::deck::deck;
/// Error type for [`Table::deserialize`]
#[derive(Debug, thiserror::Error)]
pub enum DeserializeError {
/// Unable to seek game file
#[error("Unable to seek game file to card table")]
Seek(#[source] std::io::Error),
/// Unable to read table header
#[error("Unable to read table header")]
ReadHeader(#[source] std::io::Error),
@ -26,16 +22,6 @@ pub enum DeserializeError {
magic: u32,
},
/// There were too many decks
#[error(
"Too many decks in table ({decks_count} decks, {} / 0 bytes max)",
decks_count * std::mem::size_of::<<Deck as Bytes>::ByteArray>(),
)]
TooManyDecks {
/// Number of decks
decks_count: usize,
},
/// Could not read a deck entry
#[error("Unable to read deck entry with id {}", id)]
ReadDeck {
@ -62,20 +48,6 @@ pub enum DeserializeError {
/// Error type for [`Table::serialize`]
#[derive(Debug, thiserror::Error)]
pub enum SerializeError {
/// Unable to seek game file
#[error("Unable to seek game file to card table")]
Seek(#[source] std::io::Error),
/// There were too many decks
#[error(
"Too many decks in table ({decks_count} decks, {} / 0 bytes max)",
decks_count * std::mem::size_of::<<Deck as Bytes>::ByteArray>(),
)]
TooManyDecks {
/// Number of decks
decks_count: usize,
},
/// Unable to write table header
#[error("Unable to write table header")]
WriteHeader(#[source] std::io::Error),