Compare commits
3 Commits
642ba643ad
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| ccc9a894b7 | |||
| cdfab8fd1e | |||
| 99aecaefde |
@@ -44,7 +44,8 @@
|
|||||||
"Bash(timeout 10 cargo run:*)",
|
"Bash(timeout 10 cargo run:*)",
|
||||||
"Bash(timeout 60 cargo run:*)",
|
"Bash(timeout 60 cargo run:*)",
|
||||||
"Bash(DATABASE_URL=../cursebreaker.db diesel print-schema:*)",
|
"Bash(DATABASE_URL=../cursebreaker.db diesel print-schema:*)",
|
||||||
"Bash(DATABASE_URL=../cursebreaker.db diesel database:*)"
|
"Bash(DATABASE_URL=../cursebreaker.db diesel database:*)",
|
||||||
|
"Bash(DATABASE_URL=cursebreaker.db CB_ASSETS_PATH=/home/connor/repos/CBAssets cargo run:*)"
|
||||||
],
|
],
|
||||||
"additionalDirectories": [
|
"additionalDirectories": [
|
||||||
"/home/connor/repos/CBAssets/"
|
"/home/connor/repos/CBAssets/"
|
||||||
|
|||||||
1
.env
Normal file
1
.env
Normal file
@@ -0,0 +1 @@
|
|||||||
|
DATABASE_URL=/home/connor/repos/cursebreaker-parser-rust/cursebreaker.db
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -20,3 +20,4 @@ target/
|
|||||||
# Test data (cloned Unity projects for integration tests)
|
# Test data (cloned Unity projects for integration tests)
|
||||||
test_data/
|
test_data/
|
||||||
cursebreaker.db
|
cursebreaker.db
|
||||||
|
**/cursebreaker.db
|
||||||
|
|||||||
115
Cargo.lock
generated
115
Cargo.lock
generated
@@ -50,6 +50,56 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstream"
|
||||||
|
version = "0.6.21"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a"
|
||||||
|
dependencies = [
|
||||||
|
"anstyle",
|
||||||
|
"anstyle-parse",
|
||||||
|
"anstyle-query",
|
||||||
|
"anstyle-wincon",
|
||||||
|
"colorchoice",
|
||||||
|
"is_terminal_polyfill",
|
||||||
|
"utf8parse",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstyle"
|
||||||
|
version = "1.0.13"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstyle-parse"
|
||||||
|
version = "0.2.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
|
||||||
|
dependencies = [
|
||||||
|
"utf8parse",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstyle-query"
|
||||||
|
version = "1.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
|
||||||
|
dependencies = [
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstyle-wincon"
|
||||||
|
version = "3.0.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
|
||||||
|
dependencies = [
|
||||||
|
"anstyle",
|
||||||
|
"once_cell_polyfill",
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anyhow"
|
name = "anyhow"
|
||||||
version = "1.0.100"
|
version = "1.0.100"
|
||||||
@@ -303,12 +353,58 @@ dependencies = [
|
|||||||
"windows-link",
|
"windows-link",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap"
|
||||||
|
version = "4.5.54"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394"
|
||||||
|
dependencies = [
|
||||||
|
"clap_builder",
|
||||||
|
"clap_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap_builder"
|
||||||
|
version = "4.5.54"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fa42cf4d2b7a41bc8f663a7cab4031ebafa1bf3875705bfaf8466dc60ab52c00"
|
||||||
|
dependencies = [
|
||||||
|
"anstream",
|
||||||
|
"anstyle",
|
||||||
|
"clap_lex",
|
||||||
|
"strsim",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap_derive"
|
||||||
|
version = "4.5.49"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671"
|
||||||
|
dependencies = [
|
||||||
|
"heck",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap_lex"
|
||||||
|
version = "0.7.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "color_quant"
|
name = "color_quant"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
|
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "colorchoice"
|
||||||
|
version = "1.0.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "core-foundation-sys"
|
name = "core-foundation-sys"
|
||||||
version = "0.8.7"
|
version = "0.8.7"
|
||||||
@@ -387,6 +483,7 @@ name = "cursebreaker-parser"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
|
"clap",
|
||||||
"diesel",
|
"diesel",
|
||||||
"diesel_migrations",
|
"diesel_migrations",
|
||||||
"image",
|
"image",
|
||||||
@@ -957,6 +1054,12 @@ dependencies = [
|
|||||||
"rustversion",
|
"rustversion",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "is_terminal_polyfill"
|
||||||
|
version = "1.70.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itertools"
|
name = "itertools"
|
||||||
version = "0.14.0"
|
version = "0.14.0"
|
||||||
@@ -1256,6 +1359,12 @@ version = "1.21.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "once_cell_polyfill"
|
||||||
|
version = "1.70.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "parking_lot"
|
name = "parking_lot"
|
||||||
version = "0.12.5"
|
version = "0.12.5"
|
||||||
@@ -2138,6 +2247,12 @@ version = "0.2.11"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
|
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "utf8parse"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "v_frame"
|
name = "v_frame"
|
||||||
version = "0.3.9"
|
version = "0.3.9"
|
||||||
|
|||||||
@@ -49,6 +49,67 @@ struct Position {
|
|||||||
y: f32,
|
y: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Labels response (world_map_icons with icon_type == 16)
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct LabelsResponse {
|
||||||
|
labels: Vec<Label>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct Label {
|
||||||
|
x: f32,
|
||||||
|
y: f32,
|
||||||
|
text: String,
|
||||||
|
font_size: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Entrances response (world_teleporters)
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct EntrancesResponse {
|
||||||
|
icon_base64: String,
|
||||||
|
entrances: Vec<Entrance>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct Entrance {
|
||||||
|
pos_x: f32,
|
||||||
|
pos_y: f32,
|
||||||
|
tp_x: Option<f32>,
|
||||||
|
tp_y: Option<f32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ground Items response (world_loot)
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct GroundItemsResponse {
|
||||||
|
icon_base64: String,
|
||||||
|
items: Vec<GroundItem>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct GroundItem {
|
||||||
|
x: f32,
|
||||||
|
y: f32,
|
||||||
|
name: String,
|
||||||
|
amount: i32,
|
||||||
|
respawn_time: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Houses response (player_houses)
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct HousesResponse {
|
||||||
|
icon_base64: String,
|
||||||
|
houses: Vec<House>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct House {
|
||||||
|
x: f32,
|
||||||
|
y: f32,
|
||||||
|
name: String,
|
||||||
|
description: String,
|
||||||
|
price: i32,
|
||||||
|
}
|
||||||
|
|
||||||
// Establish database connection
|
// Establish database connection
|
||||||
fn establish_connection(database_url: &str) -> Result<DbConnection, diesel::ConnectionError> {
|
fn establish_connection(database_url: &str) -> Result<DbConnection, diesel::ConnectionError> {
|
||||||
SqliteConnection::establish(database_url)
|
SqliteConnection::establish(database_url)
|
||||||
@@ -201,6 +262,212 @@ async fn get_resources(
|
|||||||
Ok(Json(ResourceResponse { resources }))
|
Ok(Json(ResourceResponse { resources }))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get labels from world_map_icons where icon_type == 16
|
||||||
|
async fn get_labels(State(state): State<Arc<AppState>>) -> Result<Json<LabelsResponse>, StatusCode> {
|
||||||
|
use cursebreaker_parser::schema::world_map_icons;
|
||||||
|
|
||||||
|
let mut conn = establish_connection(&state.database_url).map_err(|e| {
|
||||||
|
tracing::error!("Database connection error: {}", e);
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let results = world_map_icons::table
|
||||||
|
.filter(world_map_icons::icon_type.eq(16))
|
||||||
|
.select((
|
||||||
|
world_map_icons::pos_x,
|
||||||
|
world_map_icons::pos_y,
|
||||||
|
world_map_icons::text,
|
||||||
|
world_map_icons::font_size,
|
||||||
|
))
|
||||||
|
.load::<(f32, f32, String, i32)>(&mut conn)
|
||||||
|
.map_err(|e| {
|
||||||
|
tracing::error!("Error querying labels: {}", e);
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let labels: Vec<Label> = results
|
||||||
|
.into_iter()
|
||||||
|
.map(|(pos_x, pos_y, text, font_size)| Label {
|
||||||
|
x: pos_x * 5.12,
|
||||||
|
y: pos_y * 5.12,
|
||||||
|
text,
|
||||||
|
font_size,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
info!("Returning {} labels", labels.len());
|
||||||
|
|
||||||
|
Ok(Json(LabelsResponse { labels }))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get entrances from world_teleporters
|
||||||
|
async fn get_entrances(
|
||||||
|
State(state): State<Arc<AppState>>,
|
||||||
|
) -> Result<Json<EntrancesResponse>, StatusCode> {
|
||||||
|
use cursebreaker_parser::schema::{general_icons, world_teleporters};
|
||||||
|
|
||||||
|
let mut conn = establish_connection(&state.database_url).map_err(|e| {
|
||||||
|
tracing::error!("Database connection error: {}", e);
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Get the Entrance icon
|
||||||
|
let icon_bytes: Vec<u8> = general_icons::table
|
||||||
|
.filter(general_icons::name.eq("Entrance"))
|
||||||
|
.select(general_icons::icon_32)
|
||||||
|
.first::<Option<Vec<u8>>>(&mut conn)
|
||||||
|
.map_err(|e| {
|
||||||
|
tracing::error!("Error querying entrance icon: {}", e);
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR
|
||||||
|
})?
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let icon_base64 = base64::engine::general_purpose::STANDARD.encode(&icon_bytes);
|
||||||
|
|
||||||
|
// Get teleporter positions
|
||||||
|
let results = world_teleporters::table
|
||||||
|
.select((
|
||||||
|
world_teleporters::pos_x,
|
||||||
|
world_teleporters::pos_y,
|
||||||
|
world_teleporters::tp_x,
|
||||||
|
world_teleporters::tp_y,
|
||||||
|
))
|
||||||
|
.load::<(f32, f32, Option<f32>, Option<f32>)>(&mut conn)
|
||||||
|
.map_err(|e| {
|
||||||
|
tracing::error!("Error querying teleporters: {}", e);
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let entrances: Vec<Entrance> = results
|
||||||
|
.into_iter()
|
||||||
|
.map(|(pos_x, pos_y, tp_x, tp_y)| Entrance {
|
||||||
|
pos_x: pos_x * 5.12,
|
||||||
|
pos_y: pos_y * 5.12,
|
||||||
|
tp_x: tp_x.map(|x| x * 5.12),
|
||||||
|
tp_y: tp_y.map(|y| y * 5.12),
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
info!("Returning {} entrances", entrances.len());
|
||||||
|
|
||||||
|
Ok(Json(EntrancesResponse {
|
||||||
|
icon_base64,
|
||||||
|
entrances,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get ground items from world_loot
|
||||||
|
async fn get_ground_items(
|
||||||
|
State(state): State<Arc<AppState>>,
|
||||||
|
) -> Result<Json<GroundItemsResponse>, StatusCode> {
|
||||||
|
use cursebreaker_parser::schema::{general_icons, items, world_loot};
|
||||||
|
|
||||||
|
let mut conn = establish_connection(&state.database_url).map_err(|e| {
|
||||||
|
tracing::error!("Database connection error: {}", e);
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Get the Common_tick icon
|
||||||
|
let icon_bytes: Vec<u8> = general_icons::table
|
||||||
|
.filter(general_icons::name.eq("Common_tick"))
|
||||||
|
.select(general_icons::icon_32)
|
||||||
|
.first::<Option<Vec<u8>>>(&mut conn)
|
||||||
|
.map_err(|e| {
|
||||||
|
tracing::error!("Error querying common_tick icon: {}", e);
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR
|
||||||
|
})?
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let icon_base64 = base64::engine::general_purpose::STANDARD.encode(&icon_bytes);
|
||||||
|
|
||||||
|
// Get world loot with item names
|
||||||
|
let results = world_loot::table
|
||||||
|
.inner_join(items::table.on(world_loot::item_id.eq(items::id.assume_not_null())))
|
||||||
|
.select((
|
||||||
|
world_loot::pos_x,
|
||||||
|
world_loot::pos_y,
|
||||||
|
items::name,
|
||||||
|
world_loot::amount,
|
||||||
|
world_loot::respawn_time,
|
||||||
|
))
|
||||||
|
.load::<(f32, f32, String, i32, i32)>(&mut conn)
|
||||||
|
.map_err(|e| {
|
||||||
|
tracing::error!("Error querying ground items: {}", e);
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let ground_items: Vec<GroundItem> = results
|
||||||
|
.into_iter()
|
||||||
|
.map(|(pos_x, pos_y, name, amount, respawn_time)| GroundItem {
|
||||||
|
x: pos_x * 5.12,
|
||||||
|
y: pos_y * 5.12,
|
||||||
|
name,
|
||||||
|
amount,
|
||||||
|
respawn_time,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
info!("Returning {} ground items", ground_items.len());
|
||||||
|
|
||||||
|
Ok(Json(GroundItemsResponse {
|
||||||
|
icon_base64,
|
||||||
|
items: ground_items,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get player houses
|
||||||
|
async fn get_houses(State(state): State<Arc<AppState>>) -> Result<Json<HousesResponse>, StatusCode> {
|
||||||
|
use cursebreaker_parser::schema::{general_icons, player_houses};
|
||||||
|
|
||||||
|
let mut conn = establish_connection(&state.database_url).map_err(|e| {
|
||||||
|
tracing::error!("Database connection error: {}", e);
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Get the Notifications_House icon (64px)
|
||||||
|
let icon_bytes: Vec<u8> = general_icons::table
|
||||||
|
.filter(general_icons::name.eq("Notifications_House"))
|
||||||
|
.select(general_icons::icon_64)
|
||||||
|
.first::<Option<Vec<u8>>>(&mut conn)
|
||||||
|
.map_err(|e| {
|
||||||
|
tracing::error!("Error querying house icon: {}", e);
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR
|
||||||
|
})?
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let icon_base64 = base64::engine::general_purpose::STANDARD.encode(&icon_bytes);
|
||||||
|
|
||||||
|
// Get player houses
|
||||||
|
let results = player_houses::table
|
||||||
|
.select((
|
||||||
|
player_houses::pos_x,
|
||||||
|
player_houses::pos_z,
|
||||||
|
player_houses::name,
|
||||||
|
player_houses::description,
|
||||||
|
player_houses::price,
|
||||||
|
))
|
||||||
|
.load::<(f32, f32, String, String, i32)>(&mut conn)
|
||||||
|
.map_err(|e| {
|
||||||
|
tracing::error!("Error querying player houses: {}", e);
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let houses: Vec<House> = results
|
||||||
|
.into_iter()
|
||||||
|
.map(|(pos_x, pos_z, name, description, price)| House {
|
||||||
|
x: pos_x * 5.12,
|
||||||
|
y: pos_z * 5.12,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
price,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
info!("Returning {} houses", houses.len());
|
||||||
|
|
||||||
|
Ok(Json(HousesResponse { icon_base64, houses }))
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
// Initialize tracing
|
// Initialize tracing
|
||||||
@@ -219,6 +486,10 @@ async fn main() {
|
|||||||
.route("/api/bounds", get(get_bounds))
|
.route("/api/bounds", get(get_bounds))
|
||||||
.route("/api/tiles/:z/:x/:y", get(get_tile))
|
.route("/api/tiles/:z/:x/:y", get(get_tile))
|
||||||
.route("/api/resources", get(get_resources))
|
.route("/api/resources", get(get_resources))
|
||||||
|
.route("/api/labels", get(get_labels))
|
||||||
|
.route("/api/entrances", get(get_entrances))
|
||||||
|
.route("/api/ground-items", get(get_ground_items))
|
||||||
|
.route("/api/houses", get(get_houses))
|
||||||
.nest_service("/", ServeDir::new("static"))
|
.nest_service("/", ServeDir::new("static"))
|
||||||
.layer(CorsLayer::permissive())
|
.layer(CorsLayer::permissive())
|
||||||
.with_state(state);
|
.with_state(state);
|
||||||
|
|||||||
@@ -21,6 +21,46 @@
|
|||||||
<p class="subtitle">The Black Grimoire: Cursebreaker</p>
|
<p class="subtitle">The Black Grimoire: Cursebreaker</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="filters-section">
|
||||||
|
<h3>Labels</h3>
|
||||||
|
<div class="filter-controls">
|
||||||
|
<label class="filter-label master-toggle">
|
||||||
|
<input type="checkbox" id="labels-toggle" checked>
|
||||||
|
<span>Show Labels</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="filters-section">
|
||||||
|
<h3>Entrances</h3>
|
||||||
|
<div class="filter-controls">
|
||||||
|
<label class="filter-label master-toggle">
|
||||||
|
<input type="checkbox" id="entrances-toggle" checked>
|
||||||
|
<span>Show Entrances</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="filters-section">
|
||||||
|
<h3>Ground Items</h3>
|
||||||
|
<div class="filter-controls">
|
||||||
|
<label class="filter-label master-toggle">
|
||||||
|
<input type="checkbox" id="ground-items-toggle" checked>
|
||||||
|
<span>Show Ground Items</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="filters-section">
|
||||||
|
<h3>Houses</h3>
|
||||||
|
<div class="filter-controls">
|
||||||
|
<label class="filter-label master-toggle">
|
||||||
|
<input type="checkbox" id="houses-toggle" checked>
|
||||||
|
<span>Show Houses</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="filters-section">
|
<div class="filters-section">
|
||||||
<h3>Resources</h3>
|
<h3>Resources</h3>
|
||||||
<div class="filter-controls">
|
<div class="filter-controls">
|
||||||
@@ -59,5 +99,6 @@
|
|||||||
<!-- Custom JS -->
|
<!-- Custom JS -->
|
||||||
<script src="map.js"></script>
|
<script src="map.js"></script>
|
||||||
<script src="resources.js"></script>
|
<script src="resources.js"></script>
|
||||||
|
<script src="markers.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -120,6 +120,9 @@ async function initMap() {
|
|||||||
console.error('Failed to load resources:', error);
|
console.error('Failed to load resources:', error);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Load markers (labels, entrances, ground items, houses)
|
||||||
|
initMarkers();
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error initializing map:', error);
|
console.error('Error initializing map:', error);
|
||||||
document.getElementById('map-stats').innerHTML =
|
document.getElementById('map-stats').innerHTML =
|
||||||
|
|||||||
377
cursebreaker-map/static/markers.js
Normal file
377
cursebreaker-map/static/markers.js
Normal file
@@ -0,0 +1,377 @@
|
|||||||
|
// Markers management for Cursebreaker map (Labels, Entrances, Ground Items, Houses)
|
||||||
|
|
||||||
|
// Layer groups for each marker type
|
||||||
|
let labelsLayerGroup = null;
|
||||||
|
let entrancesLayerGroup = null;
|
||||||
|
let groundItemsLayerGroup = null;
|
||||||
|
let housesLayerGroup = null;
|
||||||
|
|
||||||
|
// Store active teleport lines for entrances
|
||||||
|
let activeTeleportLine = null;
|
||||||
|
|
||||||
|
// Initialize all markers when map is ready
|
||||||
|
function initMarkers() {
|
||||||
|
// Load all marker types in parallel
|
||||||
|
Promise.all([
|
||||||
|
loadLabels(),
|
||||||
|
loadEntrances(),
|
||||||
|
loadGroundItems(),
|
||||||
|
loadHouses(),
|
||||||
|
]).catch(error => {
|
||||||
|
console.error('Error loading markers:', error);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set up toggle handlers
|
||||||
|
setupMarkerToggles();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up toggle event handlers
|
||||||
|
function setupMarkerToggles() {
|
||||||
|
const labelsToggle = document.getElementById('labels-toggle');
|
||||||
|
const entrancesToggle = document.getElementById('entrances-toggle');
|
||||||
|
const groundItemsToggle = document.getElementById('ground-items-toggle');
|
||||||
|
const housesToggle = document.getElementById('houses-toggle');
|
||||||
|
|
||||||
|
if (labelsToggle) {
|
||||||
|
labelsToggle.addEventListener('change', (e) => {
|
||||||
|
toggleLayer(labelsLayerGroup, e.target.checked);
|
||||||
|
saveMarkerState('labels', e.target.checked);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entrancesToggle) {
|
||||||
|
entrancesToggle.addEventListener('change', (e) => {
|
||||||
|
toggleLayer(entrancesLayerGroup, e.target.checked);
|
||||||
|
saveMarkerState('entrances', e.target.checked);
|
||||||
|
// Remove active teleport line when hiding entrances
|
||||||
|
if (!e.target.checked && activeTeleportLine) {
|
||||||
|
map.removeLayer(activeTeleportLine);
|
||||||
|
activeTeleportLine = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (groundItemsToggle) {
|
||||||
|
groundItemsToggle.addEventListener('change', (e) => {
|
||||||
|
toggleLayer(groundItemsLayerGroup, e.target.checked);
|
||||||
|
saveMarkerState('groundItems', e.target.checked);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (housesToggle) {
|
||||||
|
housesToggle.addEventListener('change', (e) => {
|
||||||
|
toggleLayer(housesLayerGroup, e.target.checked);
|
||||||
|
saveMarkerState('houses', e.target.checked);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore saved state
|
||||||
|
restoreMarkerState();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle layer visibility
|
||||||
|
function toggleLayer(layerGroup, visible) {
|
||||||
|
if (!layerGroup) return;
|
||||||
|
|
||||||
|
if (visible) {
|
||||||
|
layerGroup.addTo(map);
|
||||||
|
} else {
|
||||||
|
map.removeLayer(layerGroup);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save marker visibility state
|
||||||
|
function saveMarkerState(type, visible) {
|
||||||
|
try {
|
||||||
|
const state = JSON.parse(localStorage.getItem('cursebreaker_marker_state') || '{}');
|
||||||
|
state[type] = visible;
|
||||||
|
localStorage.setItem('cursebreaker_marker_state', JSON.stringify(state));
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to save marker state:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore marker visibility state
|
||||||
|
function restoreMarkerState() {
|
||||||
|
try {
|
||||||
|
const state = JSON.parse(localStorage.getItem('cursebreaker_marker_state') || '{}');
|
||||||
|
|
||||||
|
// Update checkboxes and layers based on saved state
|
||||||
|
setTimeout(() => {
|
||||||
|
if (state.labels === false) {
|
||||||
|
const toggle = document.getElementById('labels-toggle');
|
||||||
|
if (toggle) {
|
||||||
|
toggle.checked = false;
|
||||||
|
toggleLayer(labelsLayerGroup, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (state.entrances === false) {
|
||||||
|
const toggle = document.getElementById('entrances-toggle');
|
||||||
|
if (toggle) {
|
||||||
|
toggle.checked = false;
|
||||||
|
toggleLayer(entrancesLayerGroup, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (state.groundItems === false) {
|
||||||
|
const toggle = document.getElementById('ground-items-toggle');
|
||||||
|
if (toggle) {
|
||||||
|
toggle.checked = false;
|
||||||
|
toggleLayer(groundItemsLayerGroup, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (state.houses === false) {
|
||||||
|
const toggle = document.getElementById('houses-toggle');
|
||||||
|
if (toggle) {
|
||||||
|
toggle.checked = false;
|
||||||
|
toggleLayer(housesLayerGroup, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 200);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to restore marker state:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load labels (text markers on the map)
|
||||||
|
async function loadLabels() {
|
||||||
|
try {
|
||||||
|
console.log('Loading labels...');
|
||||||
|
const response = await fetch('/api/labels');
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
console.log(`Received ${data.labels.length} labels`);
|
||||||
|
|
||||||
|
labelsLayerGroup = L.layerGroup();
|
||||||
|
|
||||||
|
for (const label of data.labels) {
|
||||||
|
// Create a divIcon with the label text
|
||||||
|
const labelIcon = L.divIcon({
|
||||||
|
className: 'map-label',
|
||||||
|
html: `<div class="label-text" style="font-size: ${label.font_size}px;">${label.text}</div>`,
|
||||||
|
iconSize: null, // Let CSS handle sizing
|
||||||
|
iconAnchor: [0, 0],
|
||||||
|
});
|
||||||
|
|
||||||
|
const marker = L.marker([label.y, label.x], {
|
||||||
|
icon: labelIcon,
|
||||||
|
interactive: false, // Labels shouldn't be clickable
|
||||||
|
});
|
||||||
|
|
||||||
|
marker.addTo(labelsLayerGroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
labelsLayerGroup.addTo(map);
|
||||||
|
console.log('Labels loaded successfully');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading labels:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load entrances (teleporters with lines)
|
||||||
|
async function loadEntrances() {
|
||||||
|
try {
|
||||||
|
console.log('Loading entrances...');
|
||||||
|
const response = await fetch('/api/entrances');
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
console.log(`Received ${data.entrances.length} entrances`);
|
||||||
|
|
||||||
|
entrancesLayerGroup = L.layerGroup();
|
||||||
|
|
||||||
|
// Create icon from base64
|
||||||
|
const iconUrl = `data:image/webp;base64,${data.icon_base64}`;
|
||||||
|
const entranceIcon = L.icon({
|
||||||
|
iconUrl: iconUrl,
|
||||||
|
iconSize: [32, 32],
|
||||||
|
iconAnchor: [16, 16],
|
||||||
|
popupAnchor: [0, -16],
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const entrance of data.entrances) {
|
||||||
|
const marker = L.marker([entrance.pos_y, entrance.pos_x], {
|
||||||
|
icon: entranceIcon,
|
||||||
|
title: 'Entrance',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Store teleport destination on the marker
|
||||||
|
marker.teleportDest = {
|
||||||
|
x: entrance.tp_x,
|
||||||
|
y: entrance.tp_y,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle click to show teleport line
|
||||||
|
marker.on('click', function(e) {
|
||||||
|
// Remove existing line if any
|
||||||
|
if (activeTeleportLine) {
|
||||||
|
map.removeLayer(activeTeleportLine);
|
||||||
|
activeTeleportLine = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dest = this.teleportDest;
|
||||||
|
if (dest.x !== null && dest.y !== null) {
|
||||||
|
// Create a line from entrance to destination
|
||||||
|
activeTeleportLine = L.polyline(
|
||||||
|
[
|
||||||
|
[entrance.pos_y, entrance.pos_x],
|
||||||
|
[dest.y, dest.x]
|
||||||
|
],
|
||||||
|
{
|
||||||
|
color: '#00ffff',
|
||||||
|
weight: 3,
|
||||||
|
opacity: 0.8,
|
||||||
|
dashArray: '10, 10',
|
||||||
|
}
|
||||||
|
).addTo(map);
|
||||||
|
|
||||||
|
// Add a destination marker
|
||||||
|
const destMarker = L.circleMarker([dest.y, dest.x], {
|
||||||
|
radius: 8,
|
||||||
|
color: '#00ffff',
|
||||||
|
fillColor: '#00ffff',
|
||||||
|
fillOpacity: 0.5,
|
||||||
|
}).addTo(map);
|
||||||
|
|
||||||
|
// Remove line and destination marker after 5 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
if (activeTeleportLine) {
|
||||||
|
map.removeLayer(activeTeleportLine);
|
||||||
|
activeTeleportLine = null;
|
||||||
|
}
|
||||||
|
map.removeLayer(destMarker);
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
marker.addTo(entrancesLayerGroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
entrancesLayerGroup.addTo(map);
|
||||||
|
console.log('Entrances loaded successfully');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading entrances:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format respawn time as "XXM XXS"
|
||||||
|
function formatRespawnTime(seconds) {
|
||||||
|
const minutes = Math.floor(seconds / 60);
|
||||||
|
const secs = seconds % 60;
|
||||||
|
|
||||||
|
if (minutes > 0 && secs > 0) {
|
||||||
|
return `${minutes}M ${secs}S`;
|
||||||
|
} else if (minutes > 0) {
|
||||||
|
return `${minutes}M`;
|
||||||
|
} else {
|
||||||
|
return `${secs}S`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load ground items
|
||||||
|
async function loadGroundItems() {
|
||||||
|
try {
|
||||||
|
console.log('Loading ground items...');
|
||||||
|
const response = await fetch('/api/ground-items');
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
console.log(`Received ${data.items.length} ground items`);
|
||||||
|
|
||||||
|
groundItemsLayerGroup = L.layerGroup();
|
||||||
|
|
||||||
|
// Create icon from base64
|
||||||
|
const iconUrl = `data:image/webp;base64,${data.icon_base64}`;
|
||||||
|
const itemIcon = L.icon({
|
||||||
|
iconUrl: iconUrl,
|
||||||
|
iconSize: [24, 24],
|
||||||
|
iconAnchor: [12, 12],
|
||||||
|
popupAnchor: [0, -12],
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const item of data.items) {
|
||||||
|
const marker = L.marker([item.y, item.x], {
|
||||||
|
icon: itemIcon,
|
||||||
|
title: item.name,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Build popup content
|
||||||
|
let popupContent = `<strong>${item.name}</strong>`;
|
||||||
|
if (item.amount > 1) {
|
||||||
|
popupContent += `<br/>Amount: ${item.amount}`;
|
||||||
|
}
|
||||||
|
popupContent += `<br/>Respawn: ${formatRespawnTime(item.respawn_time)}`;
|
||||||
|
|
||||||
|
marker.bindPopup(popupContent);
|
||||||
|
marker.addTo(groundItemsLayerGroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
groundItemsLayerGroup.addTo(map);
|
||||||
|
console.log('Ground items loaded successfully');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading ground items:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load player houses
|
||||||
|
async function loadHouses() {
|
||||||
|
try {
|
||||||
|
console.log('Loading houses...');
|
||||||
|
const response = await fetch('/api/houses');
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
console.log(`Received ${data.houses.length} houses`);
|
||||||
|
|
||||||
|
housesLayerGroup = L.layerGroup();
|
||||||
|
|
||||||
|
// Create icon from base64
|
||||||
|
const iconUrl = `data:image/webp;base64,${data.icon_base64}`;
|
||||||
|
const houseIcon = L.icon({
|
||||||
|
iconUrl: iconUrl,
|
||||||
|
iconSize: [64, 64],
|
||||||
|
iconAnchor: [32, 32],
|
||||||
|
popupAnchor: [0, -32],
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const house of data.houses) {
|
||||||
|
const marker = L.marker([house.y, house.x], {
|
||||||
|
icon: houseIcon,
|
||||||
|
title: house.name,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Format price with commas
|
||||||
|
const formattedPrice = house.price.toLocaleString();
|
||||||
|
|
||||||
|
// Build popup content
|
||||||
|
const popupContent = `
|
||||||
|
<strong>${house.name}</strong><br/>
|
||||||
|
<em>${house.description}</em><br/>
|
||||||
|
<span class="house-price">Price: ${formattedPrice} gold</span>
|
||||||
|
`;
|
||||||
|
|
||||||
|
marker.bindPopup(popupContent);
|
||||||
|
marker.addTo(housesLayerGroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
housesLayerGroup.addTo(map);
|
||||||
|
console.log('Houses loaded successfully');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading houses:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call initMarkers after map is loaded
|
||||||
|
// This is called from map.js after resources are loaded
|
||||||
@@ -259,6 +259,53 @@ body {
|
|||||||
background: #2a2a2a;
|
background: #2a2a2a;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Master toggle for marker categories */
|
||||||
|
.master-toggle {
|
||||||
|
margin-left: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.master-toggle input[type="checkbox"] {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
accent-color: #8b5cf6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Map labels (text overlays) */
|
||||||
|
.map-label {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-text {
|
||||||
|
color: #e0e0e0;
|
||||||
|
text-shadow:
|
||||||
|
-1px -1px 2px #000,
|
||||||
|
1px -1px 2px #000,
|
||||||
|
-1px 1px 2px #000,
|
||||||
|
1px 1px 2px #000,
|
||||||
|
0 0 4px #000;
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
font-weight: bold;
|
||||||
|
white-space: nowrap;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* House price styling in popup */
|
||||||
|
.house-price {
|
||||||
|
color: #ffd700;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Popup styling for various marker types */
|
||||||
|
.leaflet-popup-content strong {
|
||||||
|
color: #8b5cf6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-popup-content em {
|
||||||
|
color: #a0a0a0;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
/* Responsive */
|
/* Responsive */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.sidebar {
|
.sidebar {
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ image = "0.25"
|
|||||||
webp = "0.3"
|
webp = "0.3"
|
||||||
thiserror = "1.0"
|
thiserror = "1.0"
|
||||||
chrono = "0.4"
|
chrono = "0.4"
|
||||||
|
clap = { version = "4.5", features = ["derive"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
diesel_migrations = "2.2"
|
diesel_migrations = "2.2"
|
||||||
|
|||||||
278
cursebreaker-parser/XML_PARSER.md
Normal file
278
cursebreaker-parser/XML_PARSER.md
Normal file
@@ -0,0 +1,278 @@
|
|||||||
|
# XML Parser Documentation
|
||||||
|
|
||||||
|
This document explains the XML parsing system used to load game data from Cursebreaker's XML files and populate the SQLite database.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The XML parser system is responsible for:
|
||||||
|
1. Reading game data from XML files (items, NPCs, quests, etc.)
|
||||||
|
2. Parsing the XML into Rust structs
|
||||||
|
3. Storing the parsed data in a SQLite database
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### File Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
cursebreaker-parser/src/
|
||||||
|
├── xml_parsers/ # XML parsing module
|
||||||
|
│ ├── mod.rs # Shared utilities and re-exports
|
||||||
|
│ ├── items.rs # Item parser
|
||||||
|
│ ├── npcs.rs # NPC parser
|
||||||
|
│ ├── quests.rs # Quest parser
|
||||||
|
│ ├── harvestables.rs # Harvestable resource parser
|
||||||
|
│ ├── loot.rs # Loot table parser
|
||||||
|
│ ├── maps.rs # Map/scene parser
|
||||||
|
│ ├── fast_travel.rs # Fast travel location parser
|
||||||
|
│ ├── player_houses.rs # Player house parser
|
||||||
|
│ ├── traits.rs # Character trait parser
|
||||||
|
│ └── shops.rs # Shop/vendor parser
|
||||||
|
├── databases/ # Database abstraction layer
|
||||||
|
│ ├── item_database.rs
|
||||||
|
│ ├── npc_database.rs
|
||||||
|
│ └── ...
|
||||||
|
├── types/ # Data structures
|
||||||
|
│ └── cursebreaker/
|
||||||
|
│ ├── item.rs
|
||||||
|
│ ├── npc.rs
|
||||||
|
│ └── ...
|
||||||
|
└── bin/
|
||||||
|
└── xml-parser.rs # CLI binary
|
||||||
|
```
|
||||||
|
|
||||||
|
### Data Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
XML Files (CBAssets/Data/XMLs/)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
XML Parsers (xml_parsers/*.rs)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Rust Structs (types/cursebreaker/*.rs)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Database Layer (databases/*.rs)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
SQLite Database (cursebreaker.db)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Parser Components
|
||||||
|
|
||||||
|
### Shared Utilities (`xml_parsers/mod.rs`)
|
||||||
|
|
||||||
|
The module provides common functionality used by all parsers:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
/// Error types for XML parsing
|
||||||
|
pub enum XmlParseError {
|
||||||
|
XmlError(quick_xml::Error), // XML syntax errors
|
||||||
|
IoError(std::io::Error), // File read errors
|
||||||
|
AttrError(AttrError), // Attribute parsing errors
|
||||||
|
MissingAttribute(String), // Required attribute not found
|
||||||
|
InvalidAttribute(String), // Attribute value invalid
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse XML element attributes into a HashMap
|
||||||
|
fn parse_attributes(element: &BytesStart) -> Result<HashMap<String, String>, XmlParseError>
|
||||||
|
|
||||||
|
/// Parse health range strings like "3-5" or "3" into (min, max)
|
||||||
|
fn parse_health_range(health_str: &str) -> (i32, i32)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Individual Parsers
|
||||||
|
|
||||||
|
Each parser follows a similar pattern:
|
||||||
|
|
||||||
|
1. **Open and read the XML file** using `quick_xml::Reader`
|
||||||
|
2. **Iterate through XML events** (Start, Empty, End, Text, Eof)
|
||||||
|
3. **Match element names** and extract attributes
|
||||||
|
4. **Build Rust structs** from the parsed data
|
||||||
|
5. **Return a Vec** of parsed objects
|
||||||
|
|
||||||
|
#### Example: Item Parser Flow
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub fn parse_items_xml<P: AsRef<Path>>(path: P) -> Result<Vec<Item>, XmlParseError> {
|
||||||
|
// 1. Open file and create reader
|
||||||
|
let file = File::open(path)?;
|
||||||
|
let mut reader = Reader::from_reader(BufReader::new(file));
|
||||||
|
|
||||||
|
// 2. Process XML events
|
||||||
|
loop {
|
||||||
|
match reader.read_event_into(&mut buf) {
|
||||||
|
Ok(Event::Start(e)) | Ok(Event::Empty(e)) => {
|
||||||
|
match e.name().as_ref() {
|
||||||
|
b"item" => {
|
||||||
|
// 3. Parse attributes
|
||||||
|
let attrs = parse_attributes(&e)?;
|
||||||
|
let id = attrs.get("id")...;
|
||||||
|
let name = attrs.get("name")...;
|
||||||
|
|
||||||
|
// 4. Create struct
|
||||||
|
let item = Item::new(id, name);
|
||||||
|
current_item = Some(item);
|
||||||
|
}
|
||||||
|
b"stat" => { /* Parse nested stat element */ }
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Event::End(e)) => {
|
||||||
|
if e.name().as_ref() == b"item" {
|
||||||
|
// 5. Add completed item to results
|
||||||
|
items.push(current_item.take().unwrap());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Event::Eof) => break,
|
||||||
|
Err(e) => return Err(XmlParseError::XmlError(e)),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(items)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Supported Data Types
|
||||||
|
|
||||||
|
| Parser | XML Source | Description |
|
||||||
|
|--------|-----------|-------------|
|
||||||
|
| `items` | `Items/Items.xml` | Game items (weapons, armor, consumables, etc.) |
|
||||||
|
| `npcs` | `Npcs/NPCInfo.xml` | Non-player characters (enemies, vendors, quest givers) |
|
||||||
|
| `quests` | `Quests/Quests.xml` | Quest definitions with phases and rewards |
|
||||||
|
| `harvestables` | `Harvestables/HarvestableInfo.xml` | Gatherable resources (trees, rocks, fishing spots) |
|
||||||
|
| `loot` | `Loot/Loot.xml` | NPC drop tables |
|
||||||
|
| `maps` | `Maps/Maps.xml` | Game scenes/areas with lighting and fog settings |
|
||||||
|
| `fast_travel` | `FastTravel*.xml` | Teleport locations, canoe routes, portals |
|
||||||
|
| `player_houses` | `PlayerHouses/PlayerHouses.xml` | Purchasable player housing |
|
||||||
|
| `traits` | `Traits/Traits.xml` | Character traits/perks |
|
||||||
|
| `shops` | `Shops/Shops.xml` | Vendor inventories and pricing |
|
||||||
|
|
||||||
|
## CLI Usage
|
||||||
|
|
||||||
|
The `xml-parser` binary provides command-line control over which parsers to run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Parse all data types
|
||||||
|
xml-parser --all
|
||||||
|
xml-parser -a
|
||||||
|
|
||||||
|
# Parse specific data types
|
||||||
|
xml-parser --items # or -i
|
||||||
|
xml-parser --npcs # or -n
|
||||||
|
xml-parser --quests # or -q
|
||||||
|
xml-parser --harvestables # or -r
|
||||||
|
xml-parser --loot # or -l
|
||||||
|
xml-parser --maps # or -m
|
||||||
|
xml-parser --fast-travel # or -f
|
||||||
|
xml-parser --houses # or -p
|
||||||
|
xml-parser --traits # or -t
|
||||||
|
xml-parser --shops # or -s
|
||||||
|
|
||||||
|
# Combine multiple parsers
|
||||||
|
xml-parser --items --npcs --quests
|
||||||
|
xml-parser -i -n -q
|
||||||
|
|
||||||
|
# View help
|
||||||
|
xml-parser --help
|
||||||
|
```
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
| Variable | Default | Description |
|
||||||
|
|----------|---------|-------------|
|
||||||
|
| `CB_ASSETS_PATH` | `/home/connor/repos/CBAssets` | Path to game assets directory |
|
||||||
|
| `DATABASE_URL` | `cursebreaker.db` | SQLite database file path |
|
||||||
|
|
||||||
|
## Database Integration
|
||||||
|
|
||||||
|
Each parser has a corresponding database module that handles:
|
||||||
|
|
||||||
|
1. **Loading from XML** - Wraps the parser and creates a queryable database
|
||||||
|
2. **Querying** - Methods like `get_by_id()`, `get_by_name()`, `get_all()`
|
||||||
|
3. **Saving to SQLite** - Serializes data and inserts into database tables
|
||||||
|
|
||||||
|
### Example: ItemDatabase
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// Load items from XML
|
||||||
|
let item_db = ItemDatabase::load_from_xml("path/to/Items.xml")?;
|
||||||
|
|
||||||
|
// Query items
|
||||||
|
let sword = item_db.get_by_id(150);
|
||||||
|
let bows = item_db.get_by_category("bow");
|
||||||
|
|
||||||
|
// Save to database (includes icon processing)
|
||||||
|
item_db.save_to_db_with_images(&mut conn, "path/to/icons")?;
|
||||||
|
```
|
||||||
|
|
||||||
|
## XML Format Examples
|
||||||
|
|
||||||
|
### Item XML
|
||||||
|
```xml
|
||||||
|
<item id="150" name="Iron Sword" level="10" price="500" maxstack="1">
|
||||||
|
<stat damagephysical="25" accuracyphysical="5"/>
|
||||||
|
<anim idle="1" walk="2" run="3" weaponattack="4"/>
|
||||||
|
</item>
|
||||||
|
```
|
||||||
|
|
||||||
|
### NPC XML
|
||||||
|
```xml
|
||||||
|
<npc id="45" name="Goblin" level="5" health="100" aggressive="1">
|
||||||
|
<stat damagephysical="10" resistancephysical="5"/>
|
||||||
|
<level swordsmanship="3" defence="2"/>
|
||||||
|
</npc>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Quest XML
|
||||||
|
```xml
|
||||||
|
<quest id="1" name="First Steps" mainquest="1">
|
||||||
|
<phase id="1" trackerdescription="Talk to the Elder"/>
|
||||||
|
<phase id="2" trackerdescription="Collect 5 herbs"/>
|
||||||
|
<rewards>
|
||||||
|
<reward item="100" amount="1"/>
|
||||||
|
<reward skill="swordsmanship" xp="50"/>
|
||||||
|
</rewards>
|
||||||
|
</quest>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
The parser uses a custom `XmlParseError` enum to handle various failure modes:
|
||||||
|
|
||||||
|
- **MissingAttribute**: Required XML attribute not found (e.g., missing `id`)
|
||||||
|
- **InvalidAttribute**: Attribute value cannot be parsed (e.g., non-numeric ID)
|
||||||
|
- **XmlError**: Malformed XML syntax
|
||||||
|
- **IoError**: File not found or permission denied
|
||||||
|
|
||||||
|
Parsers fail fast on required attributes but use defaults for optional ones:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// Required - returns error if missing
|
||||||
|
let id = attrs.get("id")
|
||||||
|
.ok_or_else(|| XmlParseError::MissingAttribute("id".to_string()))?;
|
||||||
|
|
||||||
|
// Optional - uses default if missing
|
||||||
|
let level = attrs.get("level")
|
||||||
|
.and_then(|v| v.parse().ok())
|
||||||
|
.unwrap_or(1);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Performance Considerations
|
||||||
|
|
||||||
|
- **Streaming parser**: Uses `quick_xml` which processes XML as a stream, keeping memory usage low
|
||||||
|
- **Single-pass parsing**: Each file is read once and parsed in a single pass
|
||||||
|
- **Batch database inserts**: Data is collected into vectors before database insertion
|
||||||
|
- **Selective parsing**: CLI allows parsing only needed data types, reducing processing time
|
||||||
|
|
||||||
|
## Adding a New Parser
|
||||||
|
|
||||||
|
To add support for a new XML data type:
|
||||||
|
|
||||||
|
1. **Create the type** in `types/cursebreaker/new_type.rs`
|
||||||
|
2. **Create the parser** in `xml_parsers/new_type.rs`
|
||||||
|
3. **Export from mod.rs**: Add `mod new_type;` and `pub use new_type::parse_new_type_xml;`
|
||||||
|
4. **Create database module** in `databases/new_type_database.rs`
|
||||||
|
5. **Add CLI flag** in `bin/xml-parser.rs`
|
||||||
|
6. **Update this documentation**
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
DROP TABLE IF EXISTS icons;
|
||||||
|
DROP TABLE IF EXISTS achievement_icons;
|
||||||
|
DROP TABLE IF EXISTS general_icons;
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
-- Simple icons table (abilities, buffs, traits, player houses, stat icons)
|
||||||
|
CREATE TABLE IF NOT EXISTS icons (
|
||||||
|
category TEXT NOT NULL,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
icon BLOB NOT NULL,
|
||||||
|
PRIMARY KEY (category, name)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Achievement icons table (filtered, no _0 suffix)
|
||||||
|
CREATE TABLE IF NOT EXISTS achievement_icons (
|
||||||
|
name TEXT PRIMARY KEY NOT NULL,
|
||||||
|
icon BLOB NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- General icons table (multiple sizes)
|
||||||
|
CREATE TABLE IF NOT EXISTS general_icons (
|
||||||
|
name TEXT PRIMARY KEY NOT NULL,
|
||||||
|
original_width INTEGER NOT NULL,
|
||||||
|
original_height INTEGER NOT NULL,
|
||||||
|
icon_original BLOB,
|
||||||
|
icon_256 BLOB,
|
||||||
|
icon_64 BLOB,
|
||||||
|
icon_32 BLOB
|
||||||
|
);
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
-- Drop the separate icon tables
|
||||||
|
DROP TABLE IF EXISTS ability_icons;
|
||||||
|
DROP TABLE IF EXISTS buff_icons;
|
||||||
|
DROP TABLE IF EXISTS trait_icons;
|
||||||
|
DROP TABLE IF EXISTS player_house_icons;
|
||||||
|
DROP TABLE IF EXISTS stat_icons;
|
||||||
|
|
||||||
|
-- Recreate the combined icons table
|
||||||
|
CREATE TABLE IF NOT EXISTS icons (
|
||||||
|
category TEXT NOT NULL,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
icon BLOB NOT NULL,
|
||||||
|
PRIMARY KEY (category, name)
|
||||||
|
);
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
-- Drop the combined icons table
|
||||||
|
DROP TABLE IF EXISTS icons;
|
||||||
|
|
||||||
|
-- Ability icons table
|
||||||
|
CREATE TABLE IF NOT EXISTS ability_icons (
|
||||||
|
name TEXT PRIMARY KEY NOT NULL,
|
||||||
|
icon BLOB NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Buff icons table
|
||||||
|
CREATE TABLE IF NOT EXISTS buff_icons (
|
||||||
|
name TEXT PRIMARY KEY NOT NULL,
|
||||||
|
icon BLOB NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Trait icons table
|
||||||
|
CREATE TABLE IF NOT EXISTS trait_icons (
|
||||||
|
name TEXT PRIMARY KEY NOT NULL,
|
||||||
|
icon BLOB NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Player house icons table
|
||||||
|
CREATE TABLE IF NOT EXISTS player_house_icons (
|
||||||
|
name TEXT PRIMARY KEY NOT NULL,
|
||||||
|
icon BLOB NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Stat icons table
|
||||||
|
CREATE TABLE IF NOT EXISTS stat_icons (
|
||||||
|
name TEXT PRIMARY KEY NOT NULL,
|
||||||
|
icon BLOB NOT NULL
|
||||||
|
);
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
-- Revert to original schema with JSON data field
|
||||||
|
DROP TABLE IF EXISTS player_houses;
|
||||||
|
|
||||||
|
CREATE TABLE player_houses (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
map_id INTEGER NOT NULL,
|
||||||
|
data TEXT NOT NULL
|
||||||
|
);
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
-- Drop the old table and recreate with direct fields instead of JSON data
|
||||||
|
DROP TABLE IF EXISTS player_houses;
|
||||||
|
|
||||||
|
CREATE TABLE player_houses (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
description TEXT NOT NULL,
|
||||||
|
pos_x REAL NOT NULL,
|
||||||
|
pos_z REAL NOT NULL,
|
||||||
|
price INTEGER NOT NULL
|
||||||
|
);
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
-- Restore old table schema
|
||||||
|
DROP TABLE IF EXISTS fast_travel_locations;
|
||||||
|
|
||||||
|
CREATE TABLE fast_travel_locations (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
map_name TEXT NOT NULL,
|
||||||
|
data TEXT NOT NULL
|
||||||
|
);
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
-- Drop the old table and create with new schema
|
||||||
|
DROP TABLE IF EXISTS fast_travel_locations;
|
||||||
|
|
||||||
|
CREATE TABLE fast_travel_locations (
|
||||||
|
name TEXT PRIMARY KEY,
|
||||||
|
pos_x REAL NOT NULL,
|
||||||
|
pos_z REAL NOT NULL,
|
||||||
|
travel_type TEXT NOT NULL,
|
||||||
|
unlocked INTEGER NOT NULL DEFAULT 0,
|
||||||
|
connections TEXT,
|
||||||
|
checks TEXT
|
||||||
|
);
|
||||||
@@ -8,65 +8,132 @@
|
|||||||
//! - Storing all tiles in the SQLite database
|
//! - Storing all tiles in the SQLite database
|
||||||
//! - Generating statistics about storage and compression
|
//! - Generating statistics about storage and compression
|
||||||
|
|
||||||
use cursebreaker_parser::MinimapDatabase;
|
use clap::Parser;
|
||||||
use log::{info, error, LevelFilter};
|
use cursebreaker_parser::{IconDatabase, MinimapDatabase};
|
||||||
use unity_parser::log::DedupLogger;
|
use log::{error, info, LevelFilter};
|
||||||
use std::env;
|
use std::env;
|
||||||
|
use unity_parser::log::DedupLogger;
|
||||||
|
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
#[command(name = "image-parser")]
|
||||||
|
#[command(about = "Processes minimap tiles and game icons")]
|
||||||
|
struct Args {
|
||||||
|
/// Process minimap tiles
|
||||||
|
#[arg(long)]
|
||||||
|
minimap: bool,
|
||||||
|
|
||||||
|
/// Process game icons
|
||||||
|
#[arg(long)]
|
||||||
|
icons: bool,
|
||||||
|
|
||||||
|
/// Process everything (minimap and icons)
|
||||||
|
#[arg(long)]
|
||||||
|
all: bool,
|
||||||
|
}
|
||||||
|
|
||||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let args = Args::parse();
|
||||||
|
|
||||||
|
// Validate that at least one option is specified
|
||||||
|
if !args.minimap && !args.icons && !args.all {
|
||||||
|
eprintln!("Error: At least one option must be specified.\n");
|
||||||
|
eprintln!("Usage: image-parser [OPTIONS]\n");
|
||||||
|
eprintln!("Options:");
|
||||||
|
eprintln!(" --minimap Process minimap tiles");
|
||||||
|
eprintln!(" --icons Process game icons");
|
||||||
|
eprintln!(" --all Process everything");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
let process_minimap = args.minimap || args.all;
|
||||||
|
let process_icons = args.icons || args.all;
|
||||||
|
|
||||||
let logger = DedupLogger::new();
|
let logger = DedupLogger::new();
|
||||||
log::set_boxed_logger(Box::new(logger))
|
log::set_boxed_logger(Box::new(logger))
|
||||||
.map(|()| log::set_max_level(LevelFilter::Trace))
|
.map(|()| log::set_max_level(LevelFilter::Trace))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
info!("🎮 Cursebreaker - Image Parser");
|
info!("Image Parser");
|
||||||
info!("Generates all zoom levels (0, 1, 2) with merged tiles");
|
info!("Generates all zoom levels (0, 1, 2) with merged tiles");
|
||||||
info!("⚠️ Will override existing database entries\n");
|
info!("Will override existing database entries\n");
|
||||||
|
|
||||||
|
let database_url = env::var("DATABASE_URL").unwrap_or_else(|_| "cursebreaker.db".to_string());
|
||||||
|
let cb_assets_path =
|
||||||
|
env::var("CB_ASSETS_PATH").unwrap_or_else(|_| "/home/connor/repos/CBAssets".to_string());
|
||||||
|
|
||||||
// Process minimap tiles
|
// Process minimap tiles
|
||||||
info!("🗺️ Processing minimap tiles...");
|
if process_minimap {
|
||||||
let database_url = env::var("DATABASE_URL").unwrap_or_else(|_| "cursebreaker.db".to_string());
|
info!("Processing minimap tiles...");
|
||||||
let minimap_db = MinimapDatabase::new(database_url);
|
let minimap_db = MinimapDatabase::new(database_url.clone());
|
||||||
|
let minimap_path = format!("{}/Data/Textures/MinimapSquares", cb_assets_path);
|
||||||
|
|
||||||
let cb_assets_path = env::var("CB_ASSETS_PATH")
|
match minimap_db.load_from_directory(&minimap_path, &cb_assets_path) {
|
||||||
.unwrap_or_else(|_| "/home/connor/repos/CBAssets".to_string());
|
Ok(total_count) => {
|
||||||
let minimap_path = format!("{}/Data/Textures/MinimapSquares", cb_assets_path);
|
info!("\nProcessed {} total tiles (all zoom levels)", total_count);
|
||||||
|
|
||||||
match minimap_db.load_from_directory(&minimap_path, &cb_assets_path) {
|
// Get statistics
|
||||||
Ok(total_count) => {
|
if let Ok(stats) = minimap_db.get_storage_stats() {
|
||||||
info!("\n✅ Processed {} total tiles (all zoom levels)", total_count);
|
info!("\n=== Storage Statistics ===");
|
||||||
|
info!(
|
||||||
|
"Original PNG total: {} MB",
|
||||||
|
stats.total_original_size / 1_048_576
|
||||||
|
);
|
||||||
|
info!("WebP total: {} MB", stats.total_webp_size() / 1_048_576);
|
||||||
|
info!("Compression ratio: {:.2}%\n", stats.compression_ratio());
|
||||||
|
|
||||||
// Get statistics
|
info!("=== Tiles Per Zoom Level ===");
|
||||||
if let Ok(stats) = minimap_db.get_storage_stats() {
|
info!(
|
||||||
info!("\n=== Storage Statistics ===");
|
"Zoom 2 (original): {} tiles ({} MB)",
|
||||||
info!("Original PNG total: {} MB", stats.total_original_size / 1_048_576);
|
stats.zoom2_count,
|
||||||
info!("WebP total: {} MB", stats.total_webp_size() / 1_048_576);
|
stats.zoom2_size / 1_048_576
|
||||||
info!("Compression ratio: {:.2}%\n", stats.compression_ratio());
|
);
|
||||||
|
info!(
|
||||||
|
"Zoom 1 (2x2 merged): {} tiles ({} MB)",
|
||||||
|
stats.zoom1_count,
|
||||||
|
stats.zoom1_size / 1_048_576
|
||||||
|
);
|
||||||
|
info!(
|
||||||
|
"Zoom 0 (4x4 merged): {} tiles ({} MB)",
|
||||||
|
stats.zoom0_count,
|
||||||
|
stats.zoom0_size / 1_048_576
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
info!("=== Tiles Per Zoom Level ===");
|
if let Ok(bounds) = minimap_db.get_map_bounds() {
|
||||||
info!("Zoom 2 (original): {} tiles ({} MB)",
|
info!("\n=== Map Bounds ===");
|
||||||
stats.zoom2_count,
|
info!("Min (x,y): {:?}", bounds.0);
|
||||||
stats.zoom2_size / 1_048_576
|
info!("Max (x,y): {:?}", bounds.1);
|
||||||
);
|
}
|
||||||
info!("Zoom 1 (2x2 merged): {} tiles ({} MB)",
|
|
||||||
stats.zoom1_count,
|
|
||||||
stats.zoom1_size / 1_048_576
|
|
||||||
);
|
|
||||||
info!("Zoom 0 (4x4 merged): {} tiles ({} MB)",
|
|
||||||
stats.zoom0_count,
|
|
||||||
stats.zoom0_size / 1_048_576
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
Err(e) => {
|
||||||
if let Ok(bounds) = minimap_db.get_map_bounds() {
|
error!("Failed to process minimap tiles: {}", e);
|
||||||
info!("\n=== Map Bounds ===");
|
return Err(Box::new(e));
|
||||||
info!("Min (x,y): {:?}", bounds.0);
|
|
||||||
info!("Max (x,y): {:?}", bounds.1);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => {
|
}
|
||||||
error!("Failed to process minimap tiles: {}", e);
|
|
||||||
return Err(Box::new(e));
|
// Process game icons
|
||||||
|
if process_icons {
|
||||||
|
info!("\n=== Processing Game Icons ===");
|
||||||
|
let icon_db = IconDatabase::new(database_url);
|
||||||
|
|
||||||
|
match icon_db.load_all_icons(&cb_assets_path) {
|
||||||
|
Ok(stats) => {
|
||||||
|
info!("\n=== Icon Statistics ===");
|
||||||
|
info!("Ability icons: {}", stats.abilities);
|
||||||
|
info!("Buff icons: {}", stats.buffs);
|
||||||
|
info!("Trait icons: {}", stats.traits);
|
||||||
|
info!("Player house icons: {}", stats.player_houses);
|
||||||
|
info!("Stat icons: {}", stats.stat_icons);
|
||||||
|
info!("Achievement icons: {}", stats.achievement_icons);
|
||||||
|
info!("General icons: {}", stats.general_icons);
|
||||||
|
info!("Total icons: {}", stats.total_icons());
|
||||||
|
info!("Total size: {} KB", stats.total_bytes / 1024);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to process icons: {}", e);
|
||||||
|
return Err(Box::new(e));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,13 @@
|
|||||||
//! - Computing world transforms
|
//! - Computing world transforms
|
||||||
//! - Saving resource locations to the database
|
//! - Saving resource locations to the database
|
||||||
//! - Processing and saving item icons for resources
|
//! - Processing and saving item icons for resources
|
||||||
|
//!
|
||||||
|
//! Usage:
|
||||||
|
//! scene-parser [min_x max_x min_y max_y]
|
||||||
|
//!
|
||||||
|
//! Examples:
|
||||||
|
//! scene-parser # Parse all scenes
|
||||||
|
//! scene-parser 0 10 0 10 # Parse scenes from (0,0) to (10,10)
|
||||||
|
|
||||||
use cursebreaker_parser::{
|
use cursebreaker_parser::{
|
||||||
InteractableResource, InteractableTeleporter, InteractableWorkbench,
|
InteractableResource, InteractableTeleporter, InteractableWorkbench,
|
||||||
@@ -20,15 +27,111 @@ use std::env;
|
|||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
use diesel::sqlite::SqliteConnection;
|
use diesel::sqlite::SqliteConnection;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::fs;
|
||||||
|
|
||||||
|
/// Bounds for filtering which scene tiles to parse
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct Bounds {
|
||||||
|
min_x: i32,
|
||||||
|
max_x: i32,
|
||||||
|
min_y: i32,
|
||||||
|
max_y: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Bounds {
|
||||||
|
fn contains(&self, x: i32, y: i32) -> bool {
|
||||||
|
x >= self.min_x && x <= self.max_x && y >= self.min_y && y <= self.max_y
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse scene filename to extract tile coordinates (e.g., "10_3.unity" -> (10, 3))
|
||||||
|
fn parse_scene_coords(filename: &str) -> Option<(i32, i32)> {
|
||||||
|
let stem = filename.strip_suffix(".unity")?;
|
||||||
|
let parts: Vec<&str> = stem.split('_').collect();
|
||||||
|
if parts.len() == 2 {
|
||||||
|
let x = parts[0].parse().ok()?;
|
||||||
|
let y = parts[1].parse().ok()?;
|
||||||
|
Some((x, y))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find all scene files matching the *_*.unity pattern
|
||||||
|
fn find_scene_files(scenes_dir: &Path, bounds: Option<&Bounds>) -> Vec<PathBuf> {
|
||||||
|
let mut scenes = Vec::new();
|
||||||
|
|
||||||
|
if let Ok(entries) = fs::read_dir(scenes_dir) {
|
||||||
|
for entry in entries.flatten() {
|
||||||
|
let path = entry.path();
|
||||||
|
if let Some(filename) = path.file_name().and_then(|n| n.to_str()) {
|
||||||
|
if filename.ends_with(".unity") {
|
||||||
|
if let Some((x, y)) = parse_scene_coords(filename) {
|
||||||
|
// Check bounds if specified
|
||||||
|
if let Some(b) = bounds {
|
||||||
|
if !b.contains(x, y) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
scenes.push(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by coordinates for consistent ordering
|
||||||
|
scenes.sort_by(|a, b| {
|
||||||
|
let a_coords = a.file_name()
|
||||||
|
.and_then(|n| n.to_str())
|
||||||
|
.and_then(parse_scene_coords)
|
||||||
|
.unwrap_or((0, 0));
|
||||||
|
let b_coords = b.file_name()
|
||||||
|
.and_then(|n| n.to_str())
|
||||||
|
.and_then(parse_scene_coords)
|
||||||
|
.unwrap_or((0, 0));
|
||||||
|
a_coords.cmp(&b_coords)
|
||||||
|
});
|
||||||
|
|
||||||
|
scenes
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse command line arguments for bounds
|
||||||
|
fn parse_bounds_args() -> Option<Bounds> {
|
||||||
|
let args: Vec<String> = env::args().collect();
|
||||||
|
|
||||||
|
if args.len() == 5 {
|
||||||
|
let min_x = args[1].parse().ok()?;
|
||||||
|
let max_x = args[2].parse().ok()?;
|
||||||
|
let min_y = args[3].parse().ok()?;
|
||||||
|
let max_y = args[4].parse().ok()?;
|
||||||
|
Some(Bounds { min_x, max_x, min_y, max_y })
|
||||||
|
} else if args.len() == 1 {
|
||||||
|
None // No bounds specified, parse all
|
||||||
|
} else {
|
||||||
|
eprintln!("Usage: {} [min_x max_x min_y max_y]", args[0]);
|
||||||
|
eprintln!(" No arguments: parse all scenes");
|
||||||
|
eprintln!(" 4 arguments: parse scenes within bounds (inclusive)");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let logger = DedupLogger::new();
|
let logger = DedupLogger::new();
|
||||||
log::set_boxed_logger(Box::new(logger))
|
log::set_boxed_logger(Box::new(logger))
|
||||||
.map(|()| log::set_max_level(LevelFilter::Trace))
|
.map(|()| log::set_max_level(LevelFilter::Warn))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
info!("🎮 Cursebreaker - Scene Parser");
|
info!("🎮 Cursebreaker - Scene Parser");
|
||||||
|
|
||||||
|
// Parse bounds from command line
|
||||||
|
let bounds = parse_bounds_args();
|
||||||
|
if let Some(ref b) = bounds {
|
||||||
|
info!("📐 Bounds: x=[{}, {}], y=[{}, {}]", b.min_x, b.max_x, b.min_y, b.max_y);
|
||||||
|
} else {
|
||||||
|
info!("📐 Bounds: none (parsing all scenes)");
|
||||||
|
}
|
||||||
|
|
||||||
let cb_assets_path = env::var("CB_ASSETS_PATH").unwrap_or_else(|_| "/home/connor/repos/CBAssets".to_string());
|
let cb_assets_path = env::var("CB_ASSETS_PATH").unwrap_or_else(|_| "/home/connor/repos/CBAssets".to_string());
|
||||||
|
|
||||||
// Initialize Unity project once - scans entire project for GUID mappings
|
// Initialize Unity project once - scans entire project for GUID mappings
|
||||||
@@ -37,6 +140,16 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
|
|
||||||
let project = UnityProject::from_path(project_root)?;
|
let project = UnityProject::from_path(project_root)?;
|
||||||
|
|
||||||
|
// Find all scene files
|
||||||
|
let scenes_dir = project_root.join("_GameAssets/Scenes/Tiles");
|
||||||
|
let scene_files = find_scene_files(&scenes_dir, bounds.as_ref());
|
||||||
|
info!("🔍 Found {} scene files to parse", scene_files.len());
|
||||||
|
|
||||||
|
if scene_files.is_empty() {
|
||||||
|
warn!("No scene files found matching criteria");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
// Create type filter to only parse GameObject, Transform, and InteractableResource MonoBehaviour
|
// Create type filter to only parse GameObject, Transform, and InteractableResource MonoBehaviour
|
||||||
info!("🔍 Setting up type filter:");
|
info!("🔍 Setting up type filter:");
|
||||||
info!(" • Unity types: GameObject, Transform");
|
info!(" • Unity types: GameObject, Transform");
|
||||||
@@ -46,117 +159,145 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
vec!["InteractableResource", "InteractableTeleporter", "InteractableWorkbench", "LootSpawner", "MapIcon", "MapNameChanger"]
|
vec!["InteractableResource", "InteractableTeleporter", "InteractableWorkbench", "LootSpawner", "MapIcon", "MapNameChanger"]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Now parse the scene using the pre-built GUID resolvers with filtering
|
// Setup database connection
|
||||||
let scene_path = "_GameAssets/Scenes/Tiles/10_3.unity";
|
let database_url = env::var("DATABASE_URL").unwrap_or_else(|_| "cursebreaker.db".to_string());
|
||||||
info!("📁 Parsing scene: {}", scene_path);
|
let mut conn = SqliteConnection::establish(&database_url)?;
|
||||||
|
|
||||||
|
// Clear all tables before processing (they're regenerated each run)
|
||||||
|
{
|
||||||
|
use cursebreaker_parser::schema::{
|
||||||
|
world_resources, world_teleporters, world_workbenches,
|
||||||
|
world_loot, world_map_icons, world_map_name_changers, resource_icons
|
||||||
|
};
|
||||||
|
diesel::delete(world_resources::table).execute(&mut conn)?;
|
||||||
|
diesel::delete(world_teleporters::table).execute(&mut conn)?;
|
||||||
|
diesel::delete(world_workbenches::table).execute(&mut conn)?;
|
||||||
|
diesel::delete(world_loot::table).execute(&mut conn)?;
|
||||||
|
diesel::delete(world_map_icons::table).execute(&mut conn)?;
|
||||||
|
diesel::delete(world_map_name_changers::table).execute(&mut conn)?;
|
||||||
|
diesel::delete(resource_icons::table).execute(&mut conn)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect unique harvestables across all scenes for icon processing
|
||||||
|
let mut all_unique_harvestables: HashMap<i32, String> = HashMap::new();
|
||||||
|
|
||||||
|
// Track totals
|
||||||
|
let mut total_resources = 0;
|
||||||
|
let mut total_teleporters = 0;
|
||||||
|
let mut total_workbenches = 0;
|
||||||
|
let mut total_loot = 0;
|
||||||
|
let mut total_map_icons = 0;
|
||||||
|
let mut total_map_name_changers = 0;
|
||||||
|
let mut scenes_processed = 0;
|
||||||
|
let mut scenes_failed = 0;
|
||||||
|
|
||||||
|
// Process each scene
|
||||||
|
for (idx, scene_path) in scene_files.iter().enumerate() {
|
||||||
|
let relative_path = scene_path.strip_prefix(project_root)
|
||||||
|
.map(|p| p.to_string_lossy().to_string())
|
||||||
|
.unwrap_or_else(|_| scene_path.to_string_lossy().to_string());
|
||||||
|
|
||||||
|
print!("\n📁 [{}/{}] Parsing scene: {}", idx + 1, scene_files.len(), relative_path);
|
||||||
|
|
||||||
|
match project.parse_scene_filtered(&relative_path, Some(&type_filter)) {
|
||||||
|
Ok(mut scene) => {
|
||||||
|
info!(" ✓ Parsed ({} entities)", scene.entity_map.len());
|
||||||
|
|
||||||
|
// Post-processing: Compute world transforms
|
||||||
|
unity_parser::compute_world_transforms(&mut scene.world, &scene.entity_map);
|
||||||
|
|
||||||
|
// Save resources
|
||||||
|
let resource_count = save_resources(&mut conn, &scene)?;
|
||||||
|
total_resources += resource_count;
|
||||||
|
|
||||||
|
// Collect unique harvestables for icon processing later
|
||||||
|
scene.world
|
||||||
|
.query_all::<(&InteractableResource, &unity_parser::GameObject)>()
|
||||||
|
.for_each(|(resource, object)| {
|
||||||
|
all_unique_harvestables.entry(resource.type_id as i32)
|
||||||
|
.or_insert_with(|| object.name.to_string());
|
||||||
|
});
|
||||||
|
|
||||||
|
// Save other world objects (append mode - tables already cleared)
|
||||||
|
total_teleporters += save_teleporters_append(&mut conn, &scene)?;
|
||||||
|
total_workbenches += save_workbenches_append(&mut conn, &scene)?;
|
||||||
|
total_loot += save_loot_spawners_append(&mut conn, &scene)?;
|
||||||
|
total_map_icons += save_map_icons_append(&mut conn, &scene)?;
|
||||||
|
total_map_name_changers += save_map_name_changers_append(&mut conn, &scene)?;
|
||||||
|
|
||||||
|
scenes_processed += 1;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!(" ✗ Parse error: {}", e);
|
||||||
|
scenes_failed += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
log::logger().flush();
|
log::logger().flush();
|
||||||
|
|
||||||
// Parse the scene using the project with type filtering
|
// Process icons for all unique harvestables
|
||||||
match project.parse_scene_filtered(scene_path, Some(&type_filter)) {
|
info!("\n🎨 Processing item icons for {} unique harvestable types...", all_unique_harvestables.len());
|
||||||
Ok(mut scene) => {
|
process_item_icons_from_map(&cb_assets_path, &mut conn, &all_unique_harvestables)?;
|
||||||
info!("✅ Scene parsed successfully!");
|
|
||||||
info!(" Total entities: {}", scene.entity_map.len());
|
|
||||||
|
|
||||||
// Post-processing: Compute world transforms
|
// Print summary
|
||||||
info!("🔄 Computing world transforms...");
|
println!("\n==================================================");
|
||||||
unity_parser::compute_world_transforms(&mut scene.world, &scene.entity_map);
|
println!("📊 SUMMARY");
|
||||||
info!(" ✓ World transforms computed");
|
println!("==================================================");
|
||||||
|
println!(" Scenes processed: {} ({} failed)", scenes_processed, scenes_failed);
|
||||||
// Save resources to database
|
println!(" Resources: {}", total_resources);
|
||||||
info!("💾 Saving resources to database...");
|
println!(" Teleporters: {}", total_teleporters);
|
||||||
let database_url = env::var("DATABASE_URL").unwrap_or_else(|_| "../cursebreaker.db".to_string());
|
println!(" Workbenches: {}", total_workbenches);
|
||||||
let mut conn = SqliteConnection::establish(&database_url)?;
|
println!(" Loot spawners: {}", total_loot);
|
||||||
|
println!(" Map icons: {}", total_map_icons);
|
||||||
// Use diesel schema
|
println!(" Map name changers:{}", total_map_name_changers);
|
||||||
use cursebreaker_parser::schema::world_resources;
|
println!("==================================================");
|
||||||
|
|
||||||
// Clear the entire table (it's regenerated each run)
|
|
||||||
diesel::delete(world_resources::table).execute(&mut conn)?;
|
|
||||||
|
|
||||||
let mut resource_count = 0;
|
|
||||||
|
|
||||||
// Insert all resources in a transaction
|
|
||||||
conn.transaction::<_, diesel::result::Error, _>(|conn| {
|
|
||||||
scene.world
|
|
||||||
.query_all::<(&InteractableResource, &unity_parser::WorldTransform, &unity_parser::GameObject)>()
|
|
||||||
.for_each(|(resource, transform, object)| {
|
|
||||||
let world_pos = transform.position();
|
|
||||||
|
|
||||||
info!(" 📦 Resource: \"{}\"", object.name);
|
|
||||||
info!(" • typeId: {}", resource.type_id);
|
|
||||||
info!(" • Position: ({:.2}, {:.2})", world_pos.x, world_pos.z);
|
|
||||||
|
|
||||||
// Insert: store x and z (as y) coordinates only
|
|
||||||
let _ = diesel::insert_into(world_resources::table)
|
|
||||||
.values((
|
|
||||||
world_resources::item_id.eq(resource.type_id as i32),
|
|
||||||
world_resources::pos_x.eq(world_pos.x as f32),
|
|
||||||
world_resources::pos_y.eq(world_pos.z as f32),
|
|
||||||
))
|
|
||||||
.execute(conn);
|
|
||||||
|
|
||||||
resource_count += 1;
|
|
||||||
});
|
|
||||||
Ok(())
|
|
||||||
})?;
|
|
||||||
|
|
||||||
info!("✅ Saved {} resources to database", resource_count);
|
|
||||||
log::logger().flush();
|
|
||||||
|
|
||||||
// Process and save item icons
|
|
||||||
info!("🎨 Processing item icons...");
|
|
||||||
process_item_icons(&cb_assets_path, &mut conn, &scene)?;
|
|
||||||
|
|
||||||
// Save other world objects
|
|
||||||
info!("🗺️ Saving teleporters...");
|
|
||||||
save_teleporters(&mut conn, &scene)?;
|
|
||||||
|
|
||||||
info!("🔨 Saving workbenches...");
|
|
||||||
save_workbenches(&mut conn, &scene)?;
|
|
||||||
|
|
||||||
info!("💰 Saving loot spawners...");
|
|
||||||
save_loot_spawners(&mut conn, &scene)?;
|
|
||||||
|
|
||||||
info!("📍 Saving map icons...");
|
|
||||||
save_map_icons(&mut conn, &scene)?;
|
|
||||||
|
|
||||||
info!("🏷️ Saving map name changers...");
|
|
||||||
save_map_name_changers(&mut conn, &scene)?;
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
error!("Parse error: {}", e);
|
|
||||||
return Err(Box::new(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log::logger().flush();
|
log::logger().flush();
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Process item icons for all resources in the scene
|
/// Save resources from a scene (append mode)
|
||||||
fn process_item_icons(
|
fn save_resources(
|
||||||
cb_assets_path: &str,
|
|
||||||
conn: &mut SqliteConnection,
|
conn: &mut SqliteConnection,
|
||||||
scene: &unity_parser::UnityScene,
|
scene: &unity_parser::UnityScene,
|
||||||
|
) -> Result<usize, Box<dyn std::error::Error>> {
|
||||||
|
use cursebreaker_parser::schema::world_resources;
|
||||||
|
|
||||||
|
let mut count = 0;
|
||||||
|
|
||||||
|
conn.transaction::<_, diesel::result::Error, _>(|conn| {
|
||||||
|
scene.world
|
||||||
|
.query_all::<(&InteractableResource, &unity_parser::WorldTransform, &unity_parser::GameObject)>()
|
||||||
|
.for_each(|(resource, transform, _object)| {
|
||||||
|
let world_pos = transform.position();
|
||||||
|
|
||||||
|
let _ = diesel::insert_into(world_resources::table)
|
||||||
|
.values((
|
||||||
|
world_resources::item_id.eq(resource.type_id as i32),
|
||||||
|
world_resources::pos_x.eq(world_pos.x as f32),
|
||||||
|
world_resources::pos_y.eq(world_pos.z as f32),
|
||||||
|
))
|
||||||
|
.execute(conn);
|
||||||
|
|
||||||
|
count += 1;
|
||||||
|
});
|
||||||
|
Ok(())
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(count)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Process item icons from a pre-collected map of harvestables
|
||||||
|
fn process_item_icons_from_map(
|
||||||
|
cb_assets_path: &str,
|
||||||
|
conn: &mut SqliteConnection,
|
||||||
|
unique_harvestables: &HashMap<i32, String>,
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
use cursebreaker_parser::schema::{resource_icons, items, harvestables, harvestable_drops};
|
use cursebreaker_parser::schema::{resource_icons, items, harvestables, harvestable_drops};
|
||||||
|
|
||||||
// Collect unique harvestable IDs from resources
|
info!(" Processing {} unique harvestable types", unique_harvestables.len());
|
||||||
let mut unique_harvestables: HashMap<i32, String> = HashMap::new();
|
|
||||||
|
|
||||||
scene.world
|
|
||||||
.query_all::<(&InteractableResource, &unity_parser::GameObject)>()
|
|
||||||
.for_each(|(resource, object)| {
|
|
||||||
unique_harvestables.entry(resource.type_id as i32)
|
|
||||||
.or_insert_with(|| object.name.to_string());
|
|
||||||
});
|
|
||||||
|
|
||||||
info!(" Found {} unique harvestable types", unique_harvestables.len());
|
|
||||||
|
|
||||||
// Clear existing resource icons (regenerated each run)
|
|
||||||
diesel::delete(resource_icons::table).execute(conn)?;
|
|
||||||
|
|
||||||
// Create image processor with white outline
|
// Create image processor with white outline
|
||||||
let processor = ImageProcessor::default();
|
let processor = ImageProcessor::default();
|
||||||
@@ -249,22 +390,19 @@ fn process_item_icons(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Save teleporter data to database
|
/// Save teleporter data to database (append mode - doesn't clear table)
|
||||||
fn save_teleporters(
|
fn save_teleporters_append(
|
||||||
conn: &mut SqliteConnection,
|
conn: &mut SqliteConnection,
|
||||||
scene: &unity_parser::UnityScene,
|
scene: &unity_parser::UnityScene,
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<usize, Box<dyn std::error::Error>> {
|
||||||
use cursebreaker_parser::schema::world_teleporters;
|
use cursebreaker_parser::schema::world_teleporters;
|
||||||
|
|
||||||
// Clear existing teleporters
|
|
||||||
diesel::delete(world_teleporters::table).execute(conn)?;
|
|
||||||
|
|
||||||
let mut count = 0;
|
let mut count = 0;
|
||||||
|
|
||||||
// Query all teleporters
|
// Query all teleporters
|
||||||
scene.world
|
scene.world
|
||||||
.query_all::<(&InteractableTeleporter, &unity_parser::WorldTransform, &unity_parser::GameObject)>()
|
.query_all::<(&InteractableTeleporter, &unity_parser::WorldTransform, &unity_parser::GameObject)>()
|
||||||
.for_each(|(teleporter, transform, object)| {
|
.for_each(|(teleporter, transform, _object)| {
|
||||||
let world_pos = transform.position();
|
let world_pos = transform.position();
|
||||||
|
|
||||||
// Get the tp_transform position if it exists
|
// Get the tp_transform position if it exists
|
||||||
@@ -279,9 +417,6 @@ fn save_teleporters(
|
|||||||
(None, None)
|
(None, None)
|
||||||
};
|
};
|
||||||
|
|
||||||
info!(" 🗺️ Teleporter: \"{}\" at ({:.2}, {:.2}) -> ({:?}, {:?})",
|
|
||||||
object.name, world_pos.x, world_pos.z, tp_x, tp_y);
|
|
||||||
|
|
||||||
let _ = diesel::insert_into(world_teleporters::table)
|
let _ = diesel::insert_into(world_teleporters::table)
|
||||||
.values((
|
.values((
|
||||||
world_teleporters::pos_x.eq(world_pos.x as f32),
|
world_teleporters::pos_x.eq(world_pos.x as f32),
|
||||||
@@ -294,31 +429,24 @@ fn save_teleporters(
|
|||||||
count += 1;
|
count += 1;
|
||||||
});
|
});
|
||||||
|
|
||||||
info!("✅ Saved {} teleporters to database", count);
|
Ok(count)
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Save workbench data to database
|
/// Save workbench data to database (append mode - doesn't clear table)
|
||||||
fn save_workbenches(
|
fn save_workbenches_append(
|
||||||
conn: &mut SqliteConnection,
|
conn: &mut SqliteConnection,
|
||||||
scene: &unity_parser::UnityScene,
|
scene: &unity_parser::UnityScene,
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<usize, Box<dyn std::error::Error>> {
|
||||||
use cursebreaker_parser::schema::world_workbenches;
|
use cursebreaker_parser::schema::world_workbenches;
|
||||||
|
|
||||||
// Clear existing workbenches
|
|
||||||
diesel::delete(world_workbenches::table).execute(conn)?;
|
|
||||||
|
|
||||||
let mut count = 0;
|
let mut count = 0;
|
||||||
|
|
||||||
// Query all workbenches
|
// Query all workbenches
|
||||||
scene.world
|
scene.world
|
||||||
.query_all::<(&InteractableWorkbench, &unity_parser::WorldTransform, &unity_parser::GameObject)>()
|
.query_all::<(&InteractableWorkbench, &unity_parser::WorldTransform, &unity_parser::GameObject)>()
|
||||||
.for_each(|(workbench, transform, object)| {
|
.for_each(|(workbench, transform, _object)| {
|
||||||
let world_pos = transform.position();
|
let world_pos = transform.position();
|
||||||
|
|
||||||
info!(" 🔨 Workbench: \"{}\" (ID: {}) at ({:.2}, {:.2})",
|
|
||||||
object.name, workbench.workbench_id, world_pos.x, world_pos.z);
|
|
||||||
|
|
||||||
let _ = diesel::insert_into(world_workbenches::table)
|
let _ = diesel::insert_into(world_workbenches::table)
|
||||||
.values((
|
.values((
|
||||||
world_workbenches::pos_x.eq(world_pos.x as f32),
|
world_workbenches::pos_x.eq(world_pos.x as f32),
|
||||||
@@ -330,31 +458,24 @@ fn save_workbenches(
|
|||||||
count += 1;
|
count += 1;
|
||||||
});
|
});
|
||||||
|
|
||||||
info!("✅ Saved {} workbenches to database", count);
|
Ok(count)
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Save loot spawner data to database
|
/// Save loot spawner data to database (append mode - doesn't clear table)
|
||||||
fn save_loot_spawners(
|
fn save_loot_spawners_append(
|
||||||
conn: &mut SqliteConnection,
|
conn: &mut SqliteConnection,
|
||||||
scene: &unity_parser::UnityScene,
|
scene: &unity_parser::UnityScene,
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<usize, Box<dyn std::error::Error>> {
|
||||||
use cursebreaker_parser::schema::world_loot;
|
use cursebreaker_parser::schema::world_loot;
|
||||||
|
|
||||||
// Clear existing loot spawners
|
|
||||||
diesel::delete(world_loot::table).execute(conn)?;
|
|
||||||
|
|
||||||
let mut count = 0;
|
let mut count = 0;
|
||||||
|
|
||||||
// Query all loot spawners
|
// Query all loot spawners
|
||||||
scene.world
|
scene.world
|
||||||
.query_all::<(&LootSpawner, &unity_parser::WorldTransform, &unity_parser::GameObject)>()
|
.query_all::<(&LootSpawner, &unity_parser::WorldTransform, &unity_parser::GameObject)>()
|
||||||
.for_each(|(loot, transform, object)| {
|
.for_each(|(loot, transform, _object)| {
|
||||||
let world_pos = transform.position();
|
let world_pos = transform.position();
|
||||||
|
|
||||||
info!(" 💰 Loot: \"{}\" (Item: {}, Amount: {}, Respawn: {}s) at ({:.2}, {:.2})",
|
|
||||||
object.name, loot.item_id, loot.amount, loot.respawn_time, world_pos.x, world_pos.z);
|
|
||||||
|
|
||||||
let _ = diesel::insert_into(world_loot::table)
|
let _ = diesel::insert_into(world_loot::table)
|
||||||
.values((
|
.values((
|
||||||
world_loot::pos_x.eq(world_pos.x as f32),
|
world_loot::pos_x.eq(world_pos.x as f32),
|
||||||
@@ -369,31 +490,24 @@ fn save_loot_spawners(
|
|||||||
count += 1;
|
count += 1;
|
||||||
});
|
});
|
||||||
|
|
||||||
info!("✅ Saved {} loot spawners to database", count);
|
Ok(count)
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Save map icon data to database
|
/// Save map icon data to database (append mode - doesn't clear table)
|
||||||
fn save_map_icons(
|
fn save_map_icons_append(
|
||||||
conn: &mut SqliteConnection,
|
conn: &mut SqliteConnection,
|
||||||
scene: &unity_parser::UnityScene,
|
scene: &unity_parser::UnityScene,
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<usize, Box<dyn std::error::Error>> {
|
||||||
use cursebreaker_parser::schema::world_map_icons;
|
use cursebreaker_parser::schema::world_map_icons;
|
||||||
|
|
||||||
// Clear existing map icons
|
|
||||||
diesel::delete(world_map_icons::table).execute(conn)?;
|
|
||||||
|
|
||||||
let mut count = 0;
|
let mut count = 0;
|
||||||
|
|
||||||
// Query all map icons
|
// Query all map icons
|
||||||
scene.world
|
scene.world
|
||||||
.query_all::<(&MapIcon, &unity_parser::WorldTransform, &unity_parser::GameObject)>()
|
.query_all::<(&MapIcon, &unity_parser::WorldTransform, &unity_parser::GameObject)>()
|
||||||
.for_each(|(icon, transform, object)| {
|
.for_each(|(icon, transform, _object)| {
|
||||||
let world_pos = transform.position();
|
let world_pos = transform.position();
|
||||||
|
|
||||||
info!(" 📍 MapIcon: \"{}\" (Type: {:?}, Text: \"{}\") at ({:.2}, {:.2})",
|
|
||||||
object.name, icon.icon_type, icon.text, world_pos.x, world_pos.z);
|
|
||||||
|
|
||||||
let _ = diesel::insert_into(world_map_icons::table)
|
let _ = diesel::insert_into(world_map_icons::table)
|
||||||
.values((
|
.values((
|
||||||
world_map_icons::pos_x.eq(world_pos.x as f32),
|
world_map_icons::pos_x.eq(world_pos.x as f32),
|
||||||
@@ -410,31 +524,24 @@ fn save_map_icons(
|
|||||||
count += 1;
|
count += 1;
|
||||||
});
|
});
|
||||||
|
|
||||||
info!("✅ Saved {} map icons to database", count);
|
Ok(count)
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Save map name changer data to database
|
/// Save map name changer data to database (append mode - doesn't clear table)
|
||||||
fn save_map_name_changers(
|
fn save_map_name_changers_append(
|
||||||
conn: &mut SqliteConnection,
|
conn: &mut SqliteConnection,
|
||||||
scene: &unity_parser::UnityScene,
|
scene: &unity_parser::UnityScene,
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<usize, Box<dyn std::error::Error>> {
|
||||||
use cursebreaker_parser::schema::world_map_name_changers;
|
use cursebreaker_parser::schema::world_map_name_changers;
|
||||||
|
|
||||||
// Clear existing map name changers
|
|
||||||
diesel::delete(world_map_name_changers::table).execute(conn)?;
|
|
||||||
|
|
||||||
let mut count = 0;
|
let mut count = 0;
|
||||||
|
|
||||||
// Query all map name changers
|
// Query all map name changers
|
||||||
scene.world
|
scene.world
|
||||||
.query_all::<(&MapNameChanger, &unity_parser::WorldTransform, &unity_parser::GameObject)>()
|
.query_all::<(&MapNameChanger, &unity_parser::WorldTransform, &unity_parser::GameObject)>()
|
||||||
.for_each(|(changer, transform, object)| {
|
.for_each(|(changer, transform, _object)| {
|
||||||
let world_pos = transform.position();
|
let world_pos = transform.position();
|
||||||
|
|
||||||
info!(" 🏷️ MapNameChanger: \"{}\" -> \"{}\" at ({:.2}, {:.2})",
|
|
||||||
object.name, changer.map_name, world_pos.x, world_pos.z);
|
|
||||||
|
|
||||||
let _ = diesel::insert_into(world_map_name_changers::table)
|
let _ = diesel::insert_into(world_map_name_changers::table)
|
||||||
.values((
|
.values((
|
||||||
world_map_name_changers::pos_x.eq(world_pos.x as f32),
|
world_map_name_changers::pos_x.eq(world_pos.x as f32),
|
||||||
@@ -446,6 +553,5 @@ fn save_map_name_changers(
|
|||||||
count += 1;
|
count += 1;
|
||||||
});
|
});
|
||||||
|
|
||||||
info!("✅ Saved {} map name changers to database", count);
|
Ok(count)
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,150 @@
|
|||||||
//! XML Parser - Loads game data from XML files and populates the SQLite database
|
//! XML Parser - Loads game data from XML files and populates the SQLite database
|
||||||
//!
|
//!
|
||||||
//! This binary handles:
|
//! Usage:
|
||||||
//! - Loading all game data from XML files
|
//! xml-parser --all Parse all data types
|
||||||
//! - Populating the SQLite database with the parsed data
|
//! xml-parser --items Parse items only
|
||||||
//! - Generating statistics about the loaded data
|
//! xml-parser --npcs Parse NPCs only
|
||||||
|
//! xml-parser --quests Parse quests only
|
||||||
|
//! xml-parser --harvestables Parse harvestables only
|
||||||
|
//! xml-parser --loot Parse loot tables only
|
||||||
|
//! xml-parser --maps Parse maps only
|
||||||
|
//! xml-parser --fast-travel Parse fast travel locations only
|
||||||
|
//! xml-parser --houses Parse player houses only
|
||||||
|
//! xml-parser --traits Parse traits only
|
||||||
|
//! xml-parser --shops Parse shops only
|
||||||
|
//!
|
||||||
|
//! Multiple flags can be combined:
|
||||||
|
//! xml-parser --items --npcs --quests
|
||||||
|
|
||||||
use cursebreaker_parser::{ItemDatabase, HarvestableDatabase};
|
use clap::Parser;
|
||||||
use log::{info, warn, LevelFilter};
|
use cursebreaker_parser::{
|
||||||
use unity_parser::log::DedupLogger;
|
ItemDatabase, NpcDatabase, QuestDatabase, HarvestableDatabase,
|
||||||
|
LootDatabase, MapDatabase, FastTravelDatabase, PlayerHouseDatabase,
|
||||||
|
TraitDatabase, ShopDatabase,
|
||||||
|
};
|
||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
use diesel::sqlite::SqliteConnection;
|
use diesel::sqlite::SqliteConnection;
|
||||||
|
use log::{info, warn, LevelFilter};
|
||||||
use std::env;
|
use std::env;
|
||||||
|
use unity_parser::log::DedupLogger;
|
||||||
|
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
#[command(name = "xml-parser")]
|
||||||
|
#[command(author = "Cursebreaker Team")]
|
||||||
|
#[command(version = "1.0")]
|
||||||
|
#[command(about = "Parses game XML data and populates the SQLite database")]
|
||||||
|
struct Args {
|
||||||
|
/// Parse all data types
|
||||||
|
#[arg(long, short = 'a')]
|
||||||
|
all: bool,
|
||||||
|
|
||||||
|
/// Parse items
|
||||||
|
#[arg(long, short = 'i')]
|
||||||
|
items: bool,
|
||||||
|
|
||||||
|
/// Parse NPCs
|
||||||
|
#[arg(long, short = 'n')]
|
||||||
|
npcs: bool,
|
||||||
|
|
||||||
|
/// Parse quests
|
||||||
|
#[arg(long, short = 'q')]
|
||||||
|
quests: bool,
|
||||||
|
|
||||||
|
/// Parse harvestables
|
||||||
|
#[arg(long, short = 'r')]
|
||||||
|
harvestables: bool,
|
||||||
|
|
||||||
|
/// Parse loot tables
|
||||||
|
#[arg(long, short = 'l')]
|
||||||
|
loot: bool,
|
||||||
|
|
||||||
|
/// Parse maps
|
||||||
|
#[arg(long, short = 'm')]
|
||||||
|
maps: bool,
|
||||||
|
|
||||||
|
/// Parse fast travel locations
|
||||||
|
#[arg(long, short = 'f')]
|
||||||
|
fast_travel: bool,
|
||||||
|
|
||||||
|
/// Parse player houses
|
||||||
|
#[arg(long, short = 'p')]
|
||||||
|
houses: bool,
|
||||||
|
|
||||||
|
/// Parse traits
|
||||||
|
#[arg(long, short = 't')]
|
||||||
|
traits: bool,
|
||||||
|
|
||||||
|
/// Parse shops
|
||||||
|
#[arg(long, short = 's')]
|
||||||
|
shops: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Args {
|
||||||
|
/// Returns true if no specific parsers were selected
|
||||||
|
fn none_selected(&self) -> bool {
|
||||||
|
!self.all
|
||||||
|
&& !self.items
|
||||||
|
&& !self.npcs
|
||||||
|
&& !self.quests
|
||||||
|
&& !self.harvestables
|
||||||
|
&& !self.loot
|
||||||
|
&& !self.maps
|
||||||
|
&& !self.fast_travel
|
||||||
|
&& !self.houses
|
||||||
|
&& !self.traits
|
||||||
|
&& !self.shops
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if items should be parsed
|
||||||
|
fn should_parse_items(&self) -> bool {
|
||||||
|
self.all || self.items
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if NPCs should be parsed
|
||||||
|
fn should_parse_npcs(&self) -> bool {
|
||||||
|
self.all || self.npcs
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if quests should be parsed
|
||||||
|
fn should_parse_quests(&self) -> bool {
|
||||||
|
self.all || self.quests
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if harvestables should be parsed
|
||||||
|
fn should_parse_harvestables(&self) -> bool {
|
||||||
|
self.all || self.harvestables
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if loot should be parsed
|
||||||
|
fn should_parse_loot(&self) -> bool {
|
||||||
|
self.all || self.loot
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if maps should be parsed
|
||||||
|
fn should_parse_maps(&self) -> bool {
|
||||||
|
self.all || self.maps
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if fast travel should be parsed
|
||||||
|
fn should_parse_fast_travel(&self) -> bool {
|
||||||
|
self.all || self.fast_travel
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if houses should be parsed
|
||||||
|
fn should_parse_houses(&self) -> bool {
|
||||||
|
self.all || self.houses
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if traits should be parsed
|
||||||
|
fn should_parse_traits(&self) -> bool {
|
||||||
|
self.all || self.traits
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if shops should be parsed
|
||||||
|
fn should_parse_shops(&self) -> bool {
|
||||||
|
self.all || self.shops
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let logger = DedupLogger::new();
|
let logger = DedupLogger::new();
|
||||||
@@ -18,114 +152,190 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
.map(|()| log::set_max_level(LevelFilter::Trace))
|
.map(|()| log::set_max_level(LevelFilter::Trace))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
info!("🎮 Cursebreaker - XML Parser");
|
let args = Args::parse();
|
||||||
info!("📚 Loading game data from XML...");
|
|
||||||
|
|
||||||
let cb_assets_path = env::var("CB_ASSETS_PATH").unwrap_or_else(|_| "/home/connor/repos/CBAssets".to_string());
|
// If no parsers selected, show help
|
||||||
|
if args.none_selected() {
|
||||||
|
eprintln!("No parsers selected. Use --all to parse everything, or specify individual parsers.");
|
||||||
|
eprintln!("Run with --help for usage information.");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
// Load items from XML
|
info!("Cursebreaker - XML Parser");
|
||||||
let items_path = format!("{}/Data/XMLs/Items/Items.xml", cb_assets_path);
|
info!("Loading game data from XML...");
|
||||||
let item_db = ItemDatabase::load_from_xml(items_path)?;
|
|
||||||
info!("✅ Loaded {} items", item_db.len());
|
|
||||||
|
|
||||||
// let npcs_path = format!("{}/Data/XMLs/Npcs/NPCInfo.xml", cb_assets_path);
|
let cb_assets_path = env::var("CB_ASSETS_PATH")
|
||||||
// let npc_db = NpcDatabase::load_from_xml(npcs_path)?;
|
.unwrap_or_else(|_| "/home/connor/repos/CBAssets".to_string());
|
||||||
// info!("✅ Loaded {} NPCs", npc_db.len());
|
let database_url = env::var("DATABASE_URL")
|
||||||
|
.unwrap_or_else(|_| "cursebreaker.db".to_string());
|
||||||
|
|
||||||
// let quests_path = format!("{}/Data/XMLs/Quests/Quests.xml", cb_assets_path);
|
|
||||||
// let quest_db = QuestDatabase::load_from_xml(quests_path)?;
|
|
||||||
// info!("✅ Loaded {} quests", quest_db.len());
|
|
||||||
|
|
||||||
let harvestables_path = format!("{}/Data/XMLs/Harvestables/HarvestableInfo.xml", cb_assets_path);
|
|
||||||
let harvestable_db = HarvestableDatabase::load_from_xml(harvestables_path)?;
|
|
||||||
info!("✅ Loaded {} harvestables", harvestable_db.len());
|
|
||||||
|
|
||||||
// let loot_path = format!("{}/Data/XMLs/Loot/Loot.xml", cb_assets_path);
|
|
||||||
// let loot_db = LootDatabase::load_from_xml(loot_path)?;
|
|
||||||
// info!("✅ Loaded {} loot tables", loot_db.len());
|
|
||||||
|
|
||||||
// let maps_path = format!("{}/Data/XMLs/Maps/Maps.xml", cb_assets_path);
|
|
||||||
// let map_db = MapDatabase::load_from_xml(maps_path)?;
|
|
||||||
// info!("✅ Loaded {} maps", map_db.len());
|
|
||||||
|
|
||||||
// let fast_travel_dir = format!("{}/Data/XMLs", cb_assets_path);
|
|
||||||
// let fast_travel_db = FastTravelDatabase::load_from_directory(fast_travel_dir)?;
|
|
||||||
// info!("✅ Loaded {} fast travel locations", fast_travel_db.len());
|
|
||||||
|
|
||||||
// let player_houses_path = format!("{}/Data/XMLs/PlayerHouses/PlayerHouses.xml", cb_assets_path);
|
|
||||||
// let player_house_db = PlayerHouseDatabase::load_from_xml(player_houses_path)?;
|
|
||||||
// info!("✅ Loaded {} player houses", player_house_db.len());
|
|
||||||
|
|
||||||
// let traits_path = format!("{}/Data/XMLs/Traits/Traits.xml", cb_assets_path);
|
|
||||||
// let trait_db = TraitDatabase::load_from_xml(traits_path)?;
|
|
||||||
// info!("✅ Loaded {} traits", trait_db.len());
|
|
||||||
|
|
||||||
// let shops_path = format!("{}/Data/XMLs/Shops/Shops.xml", cb_assets_path);
|
|
||||||
// let shop_db = ShopDatabase::load_from_xml(shops_path)?;
|
|
||||||
// info!("✅ Loaded {} shops", shop_db.len());
|
|
||||||
|
|
||||||
// Save to SQLite database
|
|
||||||
info!("\n💾 Saving game data to SQLite database...");
|
|
||||||
let database_url = env::var("DATABASE_URL").unwrap_or_else(|_| "../cursebreaker.db".to_string());
|
|
||||||
let mut conn = SqliteConnection::establish(&database_url)?;
|
let mut conn = SqliteConnection::establish(&database_url)?;
|
||||||
|
|
||||||
// Process and save items with icons
|
// Parse Items
|
||||||
let icon_path = format!("{}/Data/Textures/ItemIcons", cb_assets_path);
|
if args.should_parse_items() {
|
||||||
info!("📸 Processing item icons from: {}", icon_path);
|
info!("Parsing items...");
|
||||||
|
let items_path = format!("{}/Data/XMLs/Items/Items.xml", cb_assets_path);
|
||||||
match item_db.save_to_db_with_images(&mut conn, &icon_path) {
|
match ItemDatabase::load_from_xml(&items_path) {
|
||||||
Ok((items_count, images_count)) => {
|
Ok(item_db) => {
|
||||||
info!("✅ Saved {} items to database", items_count);
|
info!("Loaded {} items", item_db.len());
|
||||||
info!("✅ Processed {} item icons (256px, 64px, 16px)", images_count);
|
let icon_path = format!("{}/Data/Textures/ItemIcons", cb_assets_path);
|
||||||
|
match item_db.save_to_db_with_images(&mut conn, &icon_path) {
|
||||||
|
Ok((items_count, images_count)) => {
|
||||||
|
info!("Saved {} items to database", items_count);
|
||||||
|
info!("Processed {} item icons", images_count);
|
||||||
|
}
|
||||||
|
Err(e) => warn!("Failed to save items: {}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => warn!("Failed to load items: {}", e),
|
||||||
}
|
}
|
||||||
Err(e) => warn!("⚠️ Failed to save items: {}", e),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// match npc_db.save_to_db(&mut conn) {
|
// Parse NPCs
|
||||||
// Ok(count) => info!("✅ Saved {} NPCs to database", count),
|
if args.should_parse_npcs() {
|
||||||
// Err(e) => warn!("⚠️ Failed to save NPCs: {}", e),
|
info!("Parsing NPCs...");
|
||||||
// }
|
let npcs_path = format!("{}/Data/XMLs/Npcs/NPCInfo.xml", cb_assets_path);
|
||||||
|
match NpcDatabase::load_from_xml(&npcs_path) {
|
||||||
// match quest_db.save_to_db(&mut conn) {
|
Ok(npc_db) => {
|
||||||
// Ok(count) => info!("✅ Saved {} quests to database", count),
|
info!("Loaded {} NPCs", npc_db.len());
|
||||||
// Err(e) => warn!("⚠️ Failed to save quests: {}", e),
|
match npc_db.save_to_db(&mut conn) {
|
||||||
// }
|
Ok(count) => info!("Saved {} NPCs to database", count),
|
||||||
|
Err(e) => warn!("Failed to save NPCs: {}", e),
|
||||||
match harvestable_db.save_to_db(&mut conn) {
|
}
|
||||||
Ok(count) => info!("✅ Saved {} harvestables to database", count),
|
}
|
||||||
Err(e) => warn!("⚠️ Failed to save harvestables: {}", e),
|
Err(e) => warn!("Failed to load NPCs: {}", e),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// match loot_db.save_to_db(&mut conn) {
|
// Parse Quests
|
||||||
// Ok(count) => info!("✅ Saved {} loot tables to database", count),
|
if args.should_parse_quests() {
|
||||||
// Err(e) => warn!("⚠️ Failed to save loot tables: {}", e),
|
info!("Parsing quests...");
|
||||||
// }
|
let quests_path = format!("{}/Data/XMLs/Quests/Quests.xml", cb_assets_path);
|
||||||
|
match QuestDatabase::load_from_xml(&quests_path) {
|
||||||
|
Ok(quest_db) => {
|
||||||
|
info!("Loaded {} quests", quest_db.len());
|
||||||
|
match quest_db.save_to_db(&mut conn) {
|
||||||
|
Ok(count) => info!("Saved {} quests to database", count),
|
||||||
|
Err(e) => warn!("Failed to save quests: {}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => warn!("Failed to load quests: {}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// match map_db.save_to_db(&mut conn) {
|
// Parse Harvestables
|
||||||
// Ok(count) => info!("✅ Saved {} maps to database", count),
|
if args.should_parse_harvestables() {
|
||||||
// Err(e) => warn!("⚠️ Failed to save maps: {}", e),
|
info!("Parsing harvestables...");
|
||||||
// }
|
let harvestables_path = format!("{}/Data/XMLs/Harvestables/HarvestableInfo.xml", cb_assets_path);
|
||||||
|
match HarvestableDatabase::load_from_xml(&harvestables_path) {
|
||||||
|
Ok(harvestable_db) => {
|
||||||
|
info!("Loaded {} harvestables", harvestable_db.len());
|
||||||
|
match harvestable_db.save_to_db(&mut conn) {
|
||||||
|
Ok(count) => info!("Saved {} harvestables to database", count),
|
||||||
|
Err(e) => warn!("Failed to save harvestables: {}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => warn!("Failed to load harvestables: {}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// match fast_travel_db.save_to_db(&mut conn) {
|
// Parse Loot
|
||||||
// Ok(count) => info!("✅ Saved {} fast travel locations to database", count),
|
if args.should_parse_loot() {
|
||||||
// Err(e) => warn!("⚠️ Failed to save fast travel locations: {}", e),
|
info!("Parsing loot tables...");
|
||||||
// }
|
let loot_path = format!("{}/Data/XMLs/Loot/Loot.xml", cb_assets_path);
|
||||||
|
match LootDatabase::load_from_xml(&loot_path) {
|
||||||
|
Ok(loot_db) => {
|
||||||
|
info!("Loaded {} loot tables", loot_db.len());
|
||||||
|
match loot_db.save_to_db(&mut conn) {
|
||||||
|
Ok(count) => info!("Saved {} loot tables to database", count),
|
||||||
|
Err(e) => warn!("Failed to save loot tables: {}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => warn!("Failed to load loot tables: {}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// match player_house_db.save_to_db(&mut conn) {
|
// Parse Maps
|
||||||
// Ok(count) => info!("✅ Saved {} player houses to database", count),
|
if args.should_parse_maps() {
|
||||||
// Err(e) => warn!("⚠️ Failed to save player houses: {}", e),
|
info!("Parsing maps...");
|
||||||
// }
|
let maps_path = format!("{}/Data/XMLs/Maps/Maps.xml", cb_assets_path);
|
||||||
|
match MapDatabase::load_from_xml(&maps_path) {
|
||||||
|
Ok(map_db) => {
|
||||||
|
info!("Loaded {} maps", map_db.len());
|
||||||
|
match map_db.save_to_db(&mut conn) {
|
||||||
|
Ok(count) => info!("Saved {} maps to database", count),
|
||||||
|
Err(e) => warn!("Failed to save maps: {}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => warn!("Failed to load maps: {}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// match trait_db.save_to_db(&mut conn) {
|
// Parse Fast Travel
|
||||||
// Ok(count) => info!("✅ Saved {} traits to database", count),
|
if args.should_parse_fast_travel() {
|
||||||
// Err(e) => warn!("⚠️ Failed to save traits: {}", e),
|
info!("Parsing fast travel locations...");
|
||||||
// }
|
let fast_travel_dir = format!("{}/Data/XMLs", cb_assets_path);
|
||||||
|
match FastTravelDatabase::load_from_directory(&fast_travel_dir) {
|
||||||
|
Ok(fast_travel_db) => {
|
||||||
|
info!("Loaded {} fast travel locations", fast_travel_db.len());
|
||||||
|
match fast_travel_db.save_to_db(&mut conn) {
|
||||||
|
Ok(count) => info!("Saved {} fast travel locations to database", count),
|
||||||
|
Err(e) => warn!("Failed to save fast travel locations: {}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => warn!("Failed to load fast travel locations: {}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// match shop_db.save_to_db(&mut conn) {
|
// Parse Player Houses
|
||||||
// Ok(count) => info!("✅ Saved {} shops to database", count),
|
if args.should_parse_houses() {
|
||||||
// Err(e) => warn!("⚠️ Failed to save shops: {}", e),
|
info!("Parsing player houses...");
|
||||||
// }
|
let player_houses_path = format!("{}/Data/XMLs/PlayerHouses/PlayerHouses.xml", cb_assets_path);
|
||||||
|
match PlayerHouseDatabase::load_from_xml(&player_houses_path) {
|
||||||
|
Ok(player_house_db) => {
|
||||||
|
info!("Loaded {} player houses", player_house_db.len());
|
||||||
|
match player_house_db.save_to_db(&mut conn) {
|
||||||
|
Ok(count) => info!("Saved {} player houses to database", count),
|
||||||
|
Err(e) => warn!("Failed to save player houses: {}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => warn!("Failed to load player houses: {}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse Traits
|
||||||
|
if args.should_parse_traits() {
|
||||||
|
info!("Parsing traits...");
|
||||||
|
let traits_path = format!("{}/Data/XMLs/Traits/Traits.xml", cb_assets_path);
|
||||||
|
match TraitDatabase::load_from_xml(&traits_path) {
|
||||||
|
Ok(trait_db) => {
|
||||||
|
info!("Loaded {} traits", trait_db.len());
|
||||||
|
match trait_db.save_to_db(&mut conn) {
|
||||||
|
Ok(count) => info!("Saved {} traits to database", count),
|
||||||
|
Err(e) => warn!("Failed to save traits: {}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => warn!("Failed to load traits: {}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse Shops
|
||||||
|
if args.should_parse_shops() {
|
||||||
|
info!("Parsing shops...");
|
||||||
|
let shops_path = format!("{}/Data/XMLs/Shops/Shops.xml", cb_assets_path);
|
||||||
|
match ShopDatabase::load_from_xml(&shops_path) {
|
||||||
|
Ok(shop_db) => {
|
||||||
|
info!("Loaded {} shops", shop_db.len());
|
||||||
|
match shop_db.save_to_db(&mut conn) {
|
||||||
|
Ok(count) => info!("Saved {} shops to database", count),
|
||||||
|
Err(e) => warn!("Failed to save shops: {}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => warn!("Failed to load shops: {}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("XML parsing complete!");
|
||||||
log::logger().flush();
|
log::logger().flush();
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use crate::types::{FastTravelLocation, FastTravelType};
|
use crate::types::{FastTravelLocation, FastTravelType};
|
||||||
use crate::xml_parser::{
|
use crate::xml_parsers::{
|
||||||
parse_fast_travel_canoe_xml, parse_fast_travel_locations_xml, parse_fast_travel_portals_xml,
|
parse_fast_travel_canoe_xml, parse_fast_travel_locations_xml, parse_fast_travel_portals_xml,
|
||||||
XmlParseError,
|
XmlParseError,
|
||||||
};
|
};
|
||||||
@@ -236,44 +236,25 @@ impl FastTravelDatabase {
|
|||||||
self.locations.is_empty()
|
self.locations.is_empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Prepare fast travel locations for SQL insertion (deprecated - use save_to_db instead)
|
|
||||||
#[deprecated(note = "Use save_to_db() to save directly to SQLite database")]
|
|
||||||
pub fn prepare_for_sql(&self) -> Vec<(i32, String, String, String)> {
|
|
||||||
self.locations
|
|
||||||
.iter()
|
|
||||||
.map(|location| {
|
|
||||||
let json =
|
|
||||||
serde_json::to_string(location).unwrap_or_else(|_| "{}".to_string());
|
|
||||||
(
|
|
||||||
location.id,
|
|
||||||
location.name.clone(),
|
|
||||||
location.travel_type.to_string(),
|
|
||||||
json,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Save all fast travel locations to SQLite database
|
/// Save all fast travel locations to SQLite database
|
||||||
pub fn save_to_db(&self, conn: &mut SqliteConnection) -> Result<usize, diesel::result::Error> {
|
pub fn save_to_db(&self, conn: &mut SqliteConnection) -> Result<usize, diesel::result::Error> {
|
||||||
use crate::schema::fast_travel_locations;
|
use crate::schema::fast_travel_locations;
|
||||||
|
|
||||||
let records: Vec<_> = self
|
// Clear existing entries
|
||||||
.locations
|
diesel::delete(fast_travel_locations::table).execute(conn)?;
|
||||||
.iter()
|
|
||||||
.map(|location| {
|
|
||||||
let json = serde_json::to_string(location).unwrap_or_else(|_| "{}".to_string());
|
|
||||||
(
|
|
||||||
fast_travel_locations::id.eq(location.id),
|
|
||||||
fast_travel_locations::name.eq(&location.name),
|
|
||||||
fast_travel_locations::map_name.eq(""), // TODO: determine actual map name
|
|
||||||
fast_travel_locations::data.eq(json),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let mut count = 0;
|
let mut count = 0;
|
||||||
for record in records {
|
for location in &self.locations {
|
||||||
|
let record = (
|
||||||
|
fast_travel_locations::name.eq(&location.name),
|
||||||
|
fast_travel_locations::pos_x.eq(location.pos_x),
|
||||||
|
fast_travel_locations::pos_z.eq(location.pos_z),
|
||||||
|
fast_travel_locations::travel_type.eq(location.travel_type.to_string()),
|
||||||
|
fast_travel_locations::unlocked.eq(if location.unlocked { 1 } else { 0 }),
|
||||||
|
fast_travel_locations::connections.eq(&location.connections),
|
||||||
|
fast_travel_locations::checks.eq(&location.checks),
|
||||||
|
);
|
||||||
|
|
||||||
diesel::insert_into(fast_travel_locations::table)
|
diesel::insert_into(fast_travel_locations::table)
|
||||||
.values(&record)
|
.values(&record)
|
||||||
.execute(conn)?;
|
.execute(conn)?;
|
||||||
@@ -288,20 +269,40 @@ impl FastTravelDatabase {
|
|||||||
use crate::schema::fast_travel_locations::dsl::*;
|
use crate::schema::fast_travel_locations::dsl::*;
|
||||||
|
|
||||||
#[derive(Queryable)]
|
#[derive(Queryable)]
|
||||||
|
#[allow(dead_code)]
|
||||||
struct FastTravelLocationRecord {
|
struct FastTravelLocationRecord {
|
||||||
id: Option<i32>,
|
name: Option<String>,
|
||||||
name: String,
|
pos_x: f32,
|
||||||
map_name: String,
|
pos_z: f32,
|
||||||
data: String,
|
travel_type: String,
|
||||||
|
unlocked: i32,
|
||||||
|
connections: Option<String>,
|
||||||
|
checks: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
let records = fast_travel_locations.load::<FastTravelLocationRecord>(conn)?;
|
let records = fast_travel_locations.load::<FastTravelLocationRecord>(conn)?;
|
||||||
|
|
||||||
let mut loaded_locations = Vec::new();
|
let mut loaded_locations = Vec::new();
|
||||||
for record in records {
|
for record in records {
|
||||||
if let Ok(location) = serde_json::from_str::<FastTravelLocation>(&record.data) {
|
let travel_type_enum = match record.travel_type.as_str() {
|
||||||
loaded_locations.push(location);
|
"Location" => FastTravelType::Location,
|
||||||
}
|
"Canoe" => FastTravelType::Canoe,
|
||||||
|
"Portal" => FastTravelType::Portal,
|
||||||
|
_ => FastTravelType::Location, // Default fallback
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut location = FastTravelLocation::new(
|
||||||
|
0, // id not stored in DB
|
||||||
|
record.name.unwrap_or_default(),
|
||||||
|
record.pos_x,
|
||||||
|
record.pos_z,
|
||||||
|
travel_type_enum,
|
||||||
|
);
|
||||||
|
location.unlocked = record.unlocked != 0;
|
||||||
|
location.connections = record.connections;
|
||||||
|
location.checks = record.checks;
|
||||||
|
|
||||||
|
loaded_locations.push(location);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut db = Self::new();
|
let mut db = Self::new();
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use crate::types::Harvestable;
|
use crate::types::Harvestable;
|
||||||
use crate::xml_parser::{parse_harvestables_xml, XmlParseError};
|
use crate::xml_parsers::{parse_harvestables_xml, XmlParseError};
|
||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
use diesel::sqlite::SqliteConnection;
|
use diesel::sqlite::SqliteConnection;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|||||||
655
cursebreaker-parser/src/databases/icon_database.rs
Normal file
655
cursebreaker-parser/src/databases/icon_database.rs
Normal file
@@ -0,0 +1,655 @@
|
|||||||
|
use crate::types::{
|
||||||
|
NewAbilityIcon, NewBuffIcon, NewTraitIcon, NewPlayerHouseIcon, NewStatIcon,
|
||||||
|
NewAchievementIcon, NewGeneralIcon
|
||||||
|
};
|
||||||
|
use crate::image_processor::ImageProcessor;
|
||||||
|
use diesel::prelude::*;
|
||||||
|
use diesel::sqlite::SqliteConnection;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::fs;
|
||||||
|
use thiserror::Error;
|
||||||
|
use log::{info, warn};
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum IconDatabaseError {
|
||||||
|
#[error("Database error: {0}")]
|
||||||
|
DatabaseError(#[from] diesel::result::Error),
|
||||||
|
|
||||||
|
#[error("Image load error: {0}")]
|
||||||
|
ImageLoadError(#[from] image::ImageError),
|
||||||
|
|
||||||
|
#[error("IO error: {0}")]
|
||||||
|
IoError(#[from] std::io::Error),
|
||||||
|
|
||||||
|
#[error("Connection pool error: {0}")]
|
||||||
|
ConnectionError(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Statistics for icon loading
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct IconStats {
|
||||||
|
pub abilities: usize,
|
||||||
|
pub buffs: usize,
|
||||||
|
pub traits: usize,
|
||||||
|
pub player_houses: usize,
|
||||||
|
pub stat_icons: usize,
|
||||||
|
pub achievement_icons: usize,
|
||||||
|
pub general_icons: usize,
|
||||||
|
pub total_bytes: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IconStats {
|
||||||
|
pub fn total_icons(&self) -> usize {
|
||||||
|
self.abilities + self.buffs + self.traits + self.player_houses
|
||||||
|
+ self.stat_icons + self.achievement_icons + self.general_icons
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Database for managing game icons
|
||||||
|
pub struct IconDatabase {
|
||||||
|
database_url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IconDatabase {
|
||||||
|
/// Create new database connection
|
||||||
|
pub fn new(database_url: String) -> Self {
|
||||||
|
Self { database_url }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Establish database connection
|
||||||
|
fn establish_connection(&self) -> Result<SqliteConnection, IconDatabaseError> {
|
||||||
|
SqliteConnection::establish(&self.database_url)
|
||||||
|
.map_err(|e| IconDatabaseError::ConnectionError(e.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load all icons from the CBAssets directory
|
||||||
|
pub fn load_all_icons<P: AsRef<Path>>(
|
||||||
|
&self,
|
||||||
|
cb_assets_path: P,
|
||||||
|
) -> Result<IconStats, IconDatabaseError> {
|
||||||
|
let base = cb_assets_path.as_ref();
|
||||||
|
let textures = base.join("Data/Textures");
|
||||||
|
let mut stats = IconStats::default();
|
||||||
|
|
||||||
|
info!("Loading ability icons...");
|
||||||
|
stats.abilities = self.load_ability_icons(
|
||||||
|
&textures.join("Abilities"),
|
||||||
|
&mut stats.total_bytes,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
info!("Loading buff icons...");
|
||||||
|
stats.buffs = self.load_buff_icons(
|
||||||
|
&textures.join("Buffs"),
|
||||||
|
&mut stats.total_bytes,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
info!("Loading trait icons...");
|
||||||
|
stats.traits = self.load_trait_icons(
|
||||||
|
&textures.join("Traits"),
|
||||||
|
&mut stats.total_bytes,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
info!("Loading player house icons...");
|
||||||
|
stats.player_houses = self.load_player_house_icons(
|
||||||
|
&textures.join("PlayerHouses/Houses"),
|
||||||
|
&mut stats.total_bytes,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
info!("Loading stat icons...");
|
||||||
|
stats.stat_icons = self.load_stat_icons(
|
||||||
|
&textures.join("StatIcons"),
|
||||||
|
&mut stats.total_bytes,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
info!("Loading achievement icons...");
|
||||||
|
stats.achievement_icons = self.load_achievement_icons(
|
||||||
|
&textures.join("Achievements/Icons"),
|
||||||
|
&mut stats.total_bytes,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
info!("Loading general icons...");
|
||||||
|
stats.general_icons = self.load_general_icons(&textures, &mut stats.total_bytes)?;
|
||||||
|
|
||||||
|
Ok(stats)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load ability icons from a directory
|
||||||
|
fn load_ability_icons<P: AsRef<Path>>(
|
||||||
|
&self,
|
||||||
|
dir: P,
|
||||||
|
total_bytes: &mut usize,
|
||||||
|
) -> Result<usize, IconDatabaseError> {
|
||||||
|
use crate::schema::ability_icons;
|
||||||
|
|
||||||
|
let dir_path = dir.as_ref();
|
||||||
|
if !dir_path.exists() {
|
||||||
|
warn!("Directory does not exist: {}", dir_path.display());
|
||||||
|
return Ok(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut conn = self.establish_connection()?;
|
||||||
|
let mut count = 0;
|
||||||
|
|
||||||
|
let image_files = self.find_image_files(dir_path)?;
|
||||||
|
|
||||||
|
for path in image_files {
|
||||||
|
let name = path.file_stem()
|
||||||
|
.and_then(|s| s.to_str())
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
if name.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let img = image::open(&path)?;
|
||||||
|
let rgba = img.to_rgba8();
|
||||||
|
let webp_data = ImageProcessor::encode_webp_lossless(&rgba)
|
||||||
|
.map_err(|e| IconDatabaseError::IoError(std::io::Error::other(e.to_string())))?;
|
||||||
|
|
||||||
|
*total_bytes += webp_data.len();
|
||||||
|
|
||||||
|
let new_icon = NewAbilityIcon {
|
||||||
|
name: &name,
|
||||||
|
icon: &webp_data,
|
||||||
|
};
|
||||||
|
|
||||||
|
diesel::replace_into(ability_icons::table)
|
||||||
|
.values(&new_icon)
|
||||||
|
.execute(&mut conn)?;
|
||||||
|
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
info!(" Loaded {} ability icons", count);
|
||||||
|
Ok(count)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load buff icons from a directory
|
||||||
|
fn load_buff_icons<P: AsRef<Path>>(
|
||||||
|
&self,
|
||||||
|
dir: P,
|
||||||
|
total_bytes: &mut usize,
|
||||||
|
) -> Result<usize, IconDatabaseError> {
|
||||||
|
use crate::schema::buff_icons;
|
||||||
|
|
||||||
|
let dir_path = dir.as_ref();
|
||||||
|
if !dir_path.exists() {
|
||||||
|
warn!("Directory does not exist: {}", dir_path.display());
|
||||||
|
return Ok(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut conn = self.establish_connection()?;
|
||||||
|
let mut count = 0;
|
||||||
|
|
||||||
|
let image_files = self.find_image_files(dir_path)?;
|
||||||
|
|
||||||
|
for path in image_files {
|
||||||
|
let name = path.file_stem()
|
||||||
|
.and_then(|s| s.to_str())
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
if name.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let img = image::open(&path)?;
|
||||||
|
let rgba = img.to_rgba8();
|
||||||
|
let webp_data = ImageProcessor::encode_webp_lossless(&rgba)
|
||||||
|
.map_err(|e| IconDatabaseError::IoError(std::io::Error::other(e.to_string())))?;
|
||||||
|
|
||||||
|
*total_bytes += webp_data.len();
|
||||||
|
|
||||||
|
let new_icon = NewBuffIcon {
|
||||||
|
name: &name,
|
||||||
|
icon: &webp_data,
|
||||||
|
};
|
||||||
|
|
||||||
|
diesel::replace_into(buff_icons::table)
|
||||||
|
.values(&new_icon)
|
||||||
|
.execute(&mut conn)?;
|
||||||
|
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
info!(" Loaded {} buff icons", count);
|
||||||
|
Ok(count)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load trait icons from a directory
|
||||||
|
fn load_trait_icons<P: AsRef<Path>>(
|
||||||
|
&self,
|
||||||
|
dir: P,
|
||||||
|
total_bytes: &mut usize,
|
||||||
|
) -> Result<usize, IconDatabaseError> {
|
||||||
|
use crate::schema::trait_icons;
|
||||||
|
|
||||||
|
let dir_path = dir.as_ref();
|
||||||
|
if !dir_path.exists() {
|
||||||
|
warn!("Directory does not exist: {}", dir_path.display());
|
||||||
|
return Ok(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut conn = self.establish_connection()?;
|
||||||
|
let mut count = 0;
|
||||||
|
|
||||||
|
let image_files = self.find_image_files(dir_path)?;
|
||||||
|
|
||||||
|
for path in image_files {
|
||||||
|
let name = path.file_stem()
|
||||||
|
.and_then(|s| s.to_str())
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
if name.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let img = image::open(&path)?;
|
||||||
|
let rgba = img.to_rgba8();
|
||||||
|
let webp_data = ImageProcessor::encode_webp_lossless(&rgba)
|
||||||
|
.map_err(|e| IconDatabaseError::IoError(std::io::Error::other(e.to_string())))?;
|
||||||
|
|
||||||
|
*total_bytes += webp_data.len();
|
||||||
|
|
||||||
|
let new_icon = NewTraitIcon {
|
||||||
|
name: &name,
|
||||||
|
icon: &webp_data,
|
||||||
|
};
|
||||||
|
|
||||||
|
diesel::replace_into(trait_icons::table)
|
||||||
|
.values(&new_icon)
|
||||||
|
.execute(&mut conn)?;
|
||||||
|
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
info!(" Loaded {} trait icons", count);
|
||||||
|
Ok(count)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load player house icons from a directory
|
||||||
|
fn load_player_house_icons<P: AsRef<Path>>(
|
||||||
|
&self,
|
||||||
|
dir: P,
|
||||||
|
total_bytes: &mut usize,
|
||||||
|
) -> Result<usize, IconDatabaseError> {
|
||||||
|
use crate::schema::player_house_icons;
|
||||||
|
|
||||||
|
let dir_path = dir.as_ref();
|
||||||
|
if !dir_path.exists() {
|
||||||
|
warn!("Directory does not exist: {}", dir_path.display());
|
||||||
|
return Ok(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut conn = self.establish_connection()?;
|
||||||
|
let mut count = 0;
|
||||||
|
|
||||||
|
let image_files = self.find_image_files(dir_path)?;
|
||||||
|
|
||||||
|
for path in image_files {
|
||||||
|
let name = path.file_stem()
|
||||||
|
.and_then(|s| s.to_str())
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
if name.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let img = image::open(&path)?;
|
||||||
|
let rgba = img.to_rgba8();
|
||||||
|
let webp_data = ImageProcessor::encode_webp_lossless(&rgba)
|
||||||
|
.map_err(|e| IconDatabaseError::IoError(std::io::Error::other(e.to_string())))?;
|
||||||
|
|
||||||
|
*total_bytes += webp_data.len();
|
||||||
|
|
||||||
|
let new_icon = NewPlayerHouseIcon {
|
||||||
|
name: &name,
|
||||||
|
icon: &webp_data,
|
||||||
|
};
|
||||||
|
|
||||||
|
diesel::replace_into(player_house_icons::table)
|
||||||
|
.values(&new_icon)
|
||||||
|
.execute(&mut conn)?;
|
||||||
|
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
info!(" Loaded {} player house icons", count);
|
||||||
|
Ok(count)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load stat icons from a directory
|
||||||
|
fn load_stat_icons<P: AsRef<Path>>(
|
||||||
|
&self,
|
||||||
|
dir: P,
|
||||||
|
total_bytes: &mut usize,
|
||||||
|
) -> Result<usize, IconDatabaseError> {
|
||||||
|
use crate::schema::stat_icons;
|
||||||
|
|
||||||
|
let dir_path = dir.as_ref();
|
||||||
|
if !dir_path.exists() {
|
||||||
|
warn!("Directory does not exist: {}", dir_path.display());
|
||||||
|
return Ok(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut conn = self.establish_connection()?;
|
||||||
|
let mut count = 0;
|
||||||
|
|
||||||
|
let image_files = self.find_image_files(dir_path)?;
|
||||||
|
|
||||||
|
for path in image_files {
|
||||||
|
let name = path.file_stem()
|
||||||
|
.and_then(|s| s.to_str())
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
if name.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let img = image::open(&path)?;
|
||||||
|
let rgba = img.to_rgba8();
|
||||||
|
let webp_data = ImageProcessor::encode_webp_lossless(&rgba)
|
||||||
|
.map_err(|e| IconDatabaseError::IoError(std::io::Error::other(e.to_string())))?;
|
||||||
|
|
||||||
|
*total_bytes += webp_data.len();
|
||||||
|
|
||||||
|
let new_icon = NewStatIcon {
|
||||||
|
name: &name,
|
||||||
|
icon: &webp_data,
|
||||||
|
};
|
||||||
|
|
||||||
|
diesel::replace_into(stat_icons::table)
|
||||||
|
.values(&new_icon)
|
||||||
|
.execute(&mut conn)?;
|
||||||
|
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
info!(" Loaded {} stat icons", count);
|
||||||
|
Ok(count)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load achievement icons, filtering out files ending with _0
|
||||||
|
fn load_achievement_icons<P: AsRef<Path>>(
|
||||||
|
&self,
|
||||||
|
dir: P,
|
||||||
|
total_bytes: &mut usize,
|
||||||
|
) -> Result<usize, IconDatabaseError> {
|
||||||
|
use crate::schema::achievement_icons;
|
||||||
|
|
||||||
|
let dir_path = dir.as_ref();
|
||||||
|
if !dir_path.exists() {
|
||||||
|
warn!("Directory does not exist: {}", dir_path.display());
|
||||||
|
return Ok(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut conn = self.establish_connection()?;
|
||||||
|
let mut count = 0;
|
||||||
|
|
||||||
|
let image_files = self.find_image_files(dir_path)?;
|
||||||
|
|
||||||
|
for path in image_files {
|
||||||
|
let name = path.file_stem()
|
||||||
|
.and_then(|s| s.to_str())
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
if name.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip files ending with _0
|
||||||
|
if name.ends_with("_0") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load and encode as lossless WebP
|
||||||
|
let img = image::open(&path)?;
|
||||||
|
let rgba = img.to_rgba8();
|
||||||
|
let webp_data = ImageProcessor::encode_webp_lossless(&rgba)
|
||||||
|
.map_err(|e| IconDatabaseError::IoError(std::io::Error::other(e.to_string())))?;
|
||||||
|
|
||||||
|
*total_bytes += webp_data.len();
|
||||||
|
|
||||||
|
let new_icon = NewAchievementIcon {
|
||||||
|
name: &name,
|
||||||
|
icon: &webp_data,
|
||||||
|
};
|
||||||
|
|
||||||
|
diesel::replace_into(achievement_icons::table)
|
||||||
|
.values(&new_icon)
|
||||||
|
.execute(&mut conn)?;
|
||||||
|
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
info!(" Loaded {} achievement icons", count);
|
||||||
|
Ok(count)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load general icons with multiple sizes
|
||||||
|
fn load_general_icons<P: AsRef<Path>>(
|
||||||
|
&self,
|
||||||
|
textures_dir: P,
|
||||||
|
total_bytes: &mut usize,
|
||||||
|
) -> Result<usize, IconDatabaseError> {
|
||||||
|
let textures = textures_dir.as_ref();
|
||||||
|
let mut count = 0;
|
||||||
|
|
||||||
|
// Collect all general icon paths
|
||||||
|
let mut icon_paths: Vec<(String, PathBuf)> = Vec::new();
|
||||||
|
|
||||||
|
// Directory-based icons
|
||||||
|
let directories = [
|
||||||
|
("Achievements/Trophies", true), // PNG only
|
||||||
|
("BottomRightTabs", false),
|
||||||
|
("MinimapIcons", false),
|
||||||
|
("Notifications", false),
|
||||||
|
("OverheadIcons", false),
|
||||||
|
("Skills", false),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (subdir, png_only) in directories {
|
||||||
|
let dir = textures.join(subdir);
|
||||||
|
if dir.exists() {
|
||||||
|
let files = if png_only {
|
||||||
|
self.find_png_files(&dir)?
|
||||||
|
} else {
|
||||||
|
self.find_image_files(&dir)?
|
||||||
|
};
|
||||||
|
|
||||||
|
for path in files {
|
||||||
|
let name = path.file_stem()
|
||||||
|
.and_then(|s| s.to_str())
|
||||||
|
.map(|s| format!("{}_{}", subdir.replace('/', "_"), s))
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
if !name.is_empty() {
|
||||||
|
icon_paths.push((name, path));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Individual file icons
|
||||||
|
let individual_files = [
|
||||||
|
("Common/Book.png", "Common_Book"),
|
||||||
|
("Common/Hourglass.png", "Common_Hourglass"),
|
||||||
|
("Common/Mana.png", "Common_Mana"),
|
||||||
|
("Common/QuestCompleteTrophy.png", "Common_QuestCompleteTrophy"),
|
||||||
|
("Common/Tick.png", "Common_Tick"),
|
||||||
|
("Common/TutorialTip.png", "Common_TutorialTip"),
|
||||||
|
("Common/Zoom_Minus.png", "Common_Zoom_Minus"),
|
||||||
|
("Common/Zoom_Plus.png", "Common_Zoom_Plus"),
|
||||||
|
("Inventory/Banknote.png", "Inventory_Banknote"),
|
||||||
|
("Minimap/ShowCoordinates.png", "Minimap_ShowCoordinates"),
|
||||||
|
("SplashScreens/Olipa.png", "SplashScreens_Olipa"),
|
||||||
|
("ItemIcons/131.png", "Coins"),
|
||||||
|
("118.png", "Map"),
|
||||||
|
("124.png", "Entrance"),
|
||||||
|
("Bug.png", "Bug"),
|
||||||
|
("Checkmark.png", "Checkmark"),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (file, name) in individual_files {
|
||||||
|
let path = textures.join(file);
|
||||||
|
if path.exists() {
|
||||||
|
icon_paths.push((name.to_string(), path));
|
||||||
|
} else {
|
||||||
|
warn!("File not found: {}", path.display());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process all collected icons
|
||||||
|
let mut conn = self.establish_connection()?;
|
||||||
|
|
||||||
|
for (name, path) in icon_paths {
|
||||||
|
if let Ok(bytes) = self.process_general_icon(&path, &name, &mut conn) {
|
||||||
|
*total_bytes += bytes;
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
info!(" Loaded {} general icons", count);
|
||||||
|
Ok(count)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Process a single general icon at multiple sizes
|
||||||
|
fn process_general_icon(
|
||||||
|
&self,
|
||||||
|
path: &Path,
|
||||||
|
name: &str,
|
||||||
|
conn: &mut SqliteConnection,
|
||||||
|
) -> Result<usize, IconDatabaseError> {
|
||||||
|
use crate::schema::general_icons;
|
||||||
|
|
||||||
|
// Load image
|
||||||
|
let img = image::open(path)?;
|
||||||
|
let (width, height) = (img.width(), img.height());
|
||||||
|
let rgba = img.to_rgba8();
|
||||||
|
|
||||||
|
let mut total_bytes = 0;
|
||||||
|
|
||||||
|
// Original size (lossless)
|
||||||
|
let icon_original = ImageProcessor::encode_webp_lossless(&rgba)
|
||||||
|
.map_err(|e| IconDatabaseError::IoError(std::io::Error::other(e.to_string())))?;
|
||||||
|
total_bytes += icon_original.len();
|
||||||
|
|
||||||
|
// Generate smaller sizes only if image is large enough (no upscaling)
|
||||||
|
let processor = ImageProcessor::new(90.0);
|
||||||
|
|
||||||
|
let icon_256 = if width >= 256 && height >= 256 {
|
||||||
|
Some(self.resize_and_encode(&img, 256, &processor)?)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
if let Some(ref data) = icon_256 {
|
||||||
|
total_bytes += data.len();
|
||||||
|
}
|
||||||
|
|
||||||
|
let icon_64 = if width >= 64 && height >= 64 {
|
||||||
|
Some(self.resize_and_encode(&img, 64, &processor)?)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
if let Some(ref data) = icon_64 {
|
||||||
|
total_bytes += data.len();
|
||||||
|
}
|
||||||
|
|
||||||
|
let icon_32 = if width >= 32 && height >= 32 {
|
||||||
|
Some(self.resize_and_encode(&img, 32, &processor)?)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
if let Some(ref data) = icon_32 {
|
||||||
|
total_bytes += data.len();
|
||||||
|
}
|
||||||
|
|
||||||
|
let new_icon = NewGeneralIcon {
|
||||||
|
name,
|
||||||
|
original_width: width as i32,
|
||||||
|
original_height: height as i32,
|
||||||
|
icon_original: Some(&icon_original),
|
||||||
|
icon_256: icon_256.as_deref(),
|
||||||
|
icon_64: icon_64.as_deref(),
|
||||||
|
icon_32: icon_32.as_deref(),
|
||||||
|
};
|
||||||
|
|
||||||
|
diesel::replace_into(general_icons::table)
|
||||||
|
.values(&new_icon)
|
||||||
|
.execute(conn)?;
|
||||||
|
|
||||||
|
Ok(total_bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resize image and encode to WebP
|
||||||
|
fn resize_and_encode(
|
||||||
|
&self,
|
||||||
|
img: &image::DynamicImage,
|
||||||
|
size: u32,
|
||||||
|
_processor: &ImageProcessor,
|
||||||
|
) -> Result<Vec<u8>, IconDatabaseError> {
|
||||||
|
let resized = img.resize_exact(size, size, image::imageops::FilterType::Lanczos3);
|
||||||
|
let rgba = resized.to_rgba8();
|
||||||
|
|
||||||
|
// Use lossy encoding for smaller sizes
|
||||||
|
let encoder = webp::Encoder::from_rgba(rgba.as_raw(), size, size);
|
||||||
|
let webp_data = encoder.encode(90.0);
|
||||||
|
|
||||||
|
Ok(webp_data.to_vec())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find all image files (PNG, JPG, etc.) in a directory
|
||||||
|
fn find_image_files<P: AsRef<Path>>(
|
||||||
|
&self,
|
||||||
|
dir: P,
|
||||||
|
) -> Result<Vec<PathBuf>, IconDatabaseError> {
|
||||||
|
let mut files = Vec::new();
|
||||||
|
|
||||||
|
for entry in fs::read_dir(dir)? {
|
||||||
|
let entry = entry?;
|
||||||
|
let path = entry.path();
|
||||||
|
|
||||||
|
if path.is_file() {
|
||||||
|
if let Some(ext) = path.extension().and_then(|s| s.to_str()) {
|
||||||
|
let ext_lower = ext.to_lowercase();
|
||||||
|
if ext_lower == "png" || ext_lower == "jpg" || ext_lower == "jpeg" {
|
||||||
|
files.push(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
files.sort();
|
||||||
|
Ok(files)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find only PNG files in a directory
|
||||||
|
fn find_png_files<P: AsRef<Path>>(
|
||||||
|
&self,
|
||||||
|
dir: P,
|
||||||
|
) -> Result<Vec<PathBuf>, IconDatabaseError> {
|
||||||
|
let mut files = Vec::new();
|
||||||
|
|
||||||
|
for entry in fs::read_dir(dir)? {
|
||||||
|
let entry = entry?;
|
||||||
|
let path = entry.path();
|
||||||
|
|
||||||
|
if path.is_file() {
|
||||||
|
if let Some(ext) = path.extension().and_then(|s| s.to_str()) {
|
||||||
|
let ext_lower = ext.to_lowercase();
|
||||||
|
if ext_lower == "png" {
|
||||||
|
files.push(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
files.sort();
|
||||||
|
Ok(files)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@ use crate::item_loader::{
|
|||||||
calculate_prices, generate_banknotes, generate_exceptional_items, load_items_from_directory,
|
calculate_prices, generate_banknotes, generate_exceptional_items, load_items_from_directory,
|
||||||
};
|
};
|
||||||
use crate::types::Item;
|
use crate::types::Item;
|
||||||
use crate::xml_parser::{parse_items_xml, XmlParseError};
|
use crate::xml_parsers::{parse_items_xml, XmlParseError};
|
||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
use diesel::sqlite::SqliteConnection;
|
use diesel::sqlite::SqliteConnection;
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
@@ -516,11 +516,17 @@ impl ItemDatabase {
|
|||||||
icon_base_path: &Path,
|
icon_base_path: &Path,
|
||||||
item_id: i32,
|
item_id: i32,
|
||||||
) -> (Option<Vec<u8>>, Option<Vec<u8>>, Option<Vec<u8>>) {
|
) -> (Option<Vec<u8>>, Option<Vec<u8>>, Option<Vec<u8>>) {
|
||||||
let icon_file = icon_base_path.join(format!("{}.png", item_id));
|
// Try both lowercase and uppercase extensions (Linux is case-sensitive)
|
||||||
|
let lowercase = icon_base_path.join(format!("{}.png", item_id));
|
||||||
|
let uppercase = icon_base_path.join(format!("{}.PNG", item_id));
|
||||||
|
|
||||||
if !icon_file.exists() {
|
let icon_file = if lowercase.exists() {
|
||||||
|
lowercase
|
||||||
|
} else if uppercase.exists() {
|
||||||
|
uppercase
|
||||||
|
} else {
|
||||||
return (None, None, None);
|
return (None, None, None);
|
||||||
}
|
};
|
||||||
|
|
||||||
// Process image at 3 sizes: 256, 64, 16
|
// Process image at 3 sizes: 256, 64, 16
|
||||||
match processor.process_image(&icon_file, &[256, 64, 16], None, None) {
|
match processor.process_image(&icon_file, &[256, 64, 16], None, None) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use crate::types::{LootTable, LootDrop};
|
use crate::types::{LootTable, LootDrop};
|
||||||
use crate::xml_parser::{parse_loot_xml, XmlParseError};
|
use crate::xml_parsers::{parse_loot_xml, XmlParseError};
|
||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
use diesel::sqlite::SqliteConnection;
|
use diesel::sqlite::SqliteConnection;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use crate::types::Map;
|
use crate::types::Map;
|
||||||
use crate::xml_parser::{parse_maps_xml, XmlParseError};
|
use crate::xml_parsers::{parse_maps_xml, XmlParseError};
|
||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
use diesel::sqlite::SqliteConnection;
|
use diesel::sqlite::SqliteConnection;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ mod player_house_database;
|
|||||||
mod trait_database;
|
mod trait_database;
|
||||||
mod shop_database;
|
mod shop_database;
|
||||||
mod minimap_database;
|
mod minimap_database;
|
||||||
|
mod icon_database;
|
||||||
|
|
||||||
pub use item_database::ItemDatabase;
|
pub use item_database::ItemDatabase;
|
||||||
pub use npc_database::NpcDatabase;
|
pub use npc_database::NpcDatabase;
|
||||||
@@ -21,3 +22,4 @@ pub use player_house_database::PlayerHouseDatabase;
|
|||||||
pub use trait_database::TraitDatabase;
|
pub use trait_database::TraitDatabase;
|
||||||
pub use shop_database::ShopDatabase;
|
pub use shop_database::ShopDatabase;
|
||||||
pub use minimap_database::{MinimapDatabase, MinimapDatabaseError, StorageStats};
|
pub use minimap_database::{MinimapDatabase, MinimapDatabaseError, StorageStats};
|
||||||
|
pub use icon_database::{IconDatabase, IconDatabaseError, IconStats};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use crate::types::Npc;
|
use crate::types::Npc;
|
||||||
use crate::xml_parser::{parse_npcs_xml, XmlParseError};
|
use crate::xml_parsers::{parse_npcs_xml, XmlParseError};
|
||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
use diesel::sqlite::SqliteConnection;
|
use diesel::sqlite::SqliteConnection;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use crate::types::PlayerHouse;
|
use crate::types::PlayerHouse;
|
||||||
use crate::xml_parser::{parse_player_houses_xml, XmlParseError};
|
use crate::xml_parsers::{parse_player_houses_xml, XmlParseError};
|
||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
use diesel::sqlite::SqliteConnection;
|
use diesel::sqlite::SqliteConnection;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
@@ -76,16 +76,6 @@ impl PlayerHouseDatabase {
|
|||||||
&self.houses
|
&self.houses
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get all visible houses (not hidden)
|
|
||||||
pub fn get_visible_houses(&self) -> Vec<&PlayerHouse> {
|
|
||||||
self.houses.iter().filter(|h| h.is_visible()).collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get all hidden houses
|
|
||||||
pub fn get_hidden_houses(&self) -> Vec<&PlayerHouse> {
|
|
||||||
self.houses.iter().filter(|h| h.hidden).collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get all free houses (price is 0)
|
/// Get all free houses (price is 0)
|
||||||
pub fn get_free_houses(&self) -> Vec<&PlayerHouse> {
|
pub fn get_free_houses(&self) -> Vec<&PlayerHouse> {
|
||||||
self.houses.iter().filter(|h| h.is_free()).collect()
|
self.houses.iter().filter(|h| h.is_free()).collect()
|
||||||
@@ -152,32 +142,24 @@ impl PlayerHouseDatabase {
|
|||||||
self.houses.is_empty()
|
self.houses.is_empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Prepare player houses for SQL insertion (deprecated - use save_to_db instead)
|
/// Save all player houses to SQLite database (clears existing entries first)
|
||||||
#[deprecated(note = "Use save_to_db() to save directly to SQLite database")]
|
|
||||||
pub fn prepare_for_sql(&self) -> Vec<(i32, String, i32, String)> {
|
|
||||||
self.houses
|
|
||||||
.iter()
|
|
||||||
.map(|house| {
|
|
||||||
let json = serde_json::to_string(house).unwrap_or_else(|_| "{}".to_string());
|
|
||||||
(house.id, house.name.clone(), house.price, json)
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Save all player houses to SQLite database
|
|
||||||
pub fn save_to_db(&self, conn: &mut SqliteConnection) -> Result<usize, diesel::result::Error> {
|
pub fn save_to_db(&self, conn: &mut SqliteConnection) -> Result<usize, diesel::result::Error> {
|
||||||
use crate::schema::player_houses;
|
use crate::schema::player_houses;
|
||||||
|
|
||||||
|
// Clear existing entries
|
||||||
|
diesel::delete(player_houses::table).execute(conn)?;
|
||||||
|
|
||||||
let records: Vec<_> = self
|
let records: Vec<_> = self
|
||||||
.houses
|
.houses
|
||||||
.iter()
|
.iter()
|
||||||
.map(|house| {
|
.map(|house| {
|
||||||
let json = serde_json::to_string(house).unwrap_or_else(|_| "{}".to_string());
|
|
||||||
(
|
(
|
||||||
player_houses::id.eq(house.id),
|
player_houses::id.eq(house.id),
|
||||||
player_houses::name.eq(&house.name),
|
player_houses::name.eq(&house.name),
|
||||||
player_houses::map_id.eq(0), // TODO: determine actual map ID
|
player_houses::description.eq(&house.description),
|
||||||
player_houses::data.eq(json),
|
player_houses::pos_x.eq(house.pos_x),
|
||||||
|
player_houses::pos_z.eq(house.pos_z),
|
||||||
|
player_houses::price.eq(house.price),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
@@ -199,20 +181,31 @@ impl PlayerHouseDatabase {
|
|||||||
|
|
||||||
#[derive(Queryable)]
|
#[derive(Queryable)]
|
||||||
struct PlayerHouseRecord {
|
struct PlayerHouseRecord {
|
||||||
id: Option<i32>,
|
record_id: Option<i32>,
|
||||||
name: String,
|
name: String,
|
||||||
map_id: i32,
|
description: String,
|
||||||
data: String,
|
pos_x: f32,
|
||||||
|
pos_z: f32,
|
||||||
|
price: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
let records = player_houses.load::<PlayerHouseRecord>(conn)?;
|
let records = player_houses.load::<PlayerHouseRecord>(conn)?;
|
||||||
|
|
||||||
let mut loaded_houses = Vec::new();
|
let loaded_houses: Vec<PlayerHouse> = records
|
||||||
for record in records {
|
.into_iter()
|
||||||
if let Ok(house) = serde_json::from_str::<PlayerHouse>(&record.data) {
|
.filter_map(|record| {
|
||||||
loaded_houses.push(house);
|
record.record_id.map(|house_id| {
|
||||||
}
|
PlayerHouse::new(
|
||||||
}
|
house_id,
|
||||||
|
record.name,
|
||||||
|
record.description,
|
||||||
|
record.pos_x,
|
||||||
|
record.pos_z,
|
||||||
|
record.price,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
let mut db = Self::new();
|
let mut db = Self::new();
|
||||||
db.add_houses(loaded_houses);
|
db.add_houses(loaded_houses);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use crate::types::Quest;
|
use crate::types::Quest;
|
||||||
use crate::xml_parser::{parse_quests_xml, XmlParseError};
|
use crate::xml_parsers::{parse_quests_xml, XmlParseError};
|
||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
use diesel::sqlite::SqliteConnection;
|
use diesel::sqlite::SqliteConnection;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use crate::types::Shop;
|
use crate::types::Shop;
|
||||||
use crate::xml_parser::{parse_shops_xml, XmlParseError};
|
use crate::xml_parsers::{parse_shops_xml, XmlParseError};
|
||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
use diesel::sqlite::SqliteConnection;
|
use diesel::sqlite::SqliteConnection;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use crate::types::Trait;
|
use crate::types::Trait;
|
||||||
use crate::xml_parser::{parse_traits_xml, XmlParseError};
|
use crate::xml_parsers::{parse_traits_xml, XmlParseError};
|
||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
use diesel::sqlite::SqliteConnection;
|
use diesel::sqlite::SqliteConnection;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|||||||
@@ -132,16 +132,6 @@ impl ImageProcessor {
|
|||||||
Ok(ProcessedImages { images })
|
Ok(ProcessedImages { images })
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load PNG and generate 4 WebP sizes specifically for minimap tiles (512x512 source)
|
|
||||||
///
|
|
||||||
/// Convenience method that generates 512, 256, 128, and 64 pixel versions
|
|
||||||
pub fn process_minimap_png<P: AsRef<Path>>(
|
|
||||||
&self,
|
|
||||||
png_path: P,
|
|
||||||
) -> Result<ProcessedImages, ImageProcessingError> {
|
|
||||||
self.process_image(png_path, &[512, 256, 128, 64], Some((512, 512)), None)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Apply outline effect to image based on alpha channel edges
|
/// Apply outline effect to image based on alpha channel edges
|
||||||
fn apply_outline(&self, img: RgbaImage, config: &OutlineConfig) -> RgbaImage {
|
fn apply_outline(&self, img: RgbaImage, config: &OutlineConfig) -> RgbaImage {
|
||||||
let (width, height) = img.dimensions();
|
let (width, height) = img.dimensions();
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use crate::types::{
|
|||||||
ItemCategory, ItemType, ItemXpBoost, PermanentStatBoost, SkillType, Stat, StatType,
|
ItemCategory, ItemType, ItemXpBoost, PermanentStatBoost, SkillType, Stat, StatType,
|
||||||
Tool, MAX_STACK,
|
Tool, MAX_STACK,
|
||||||
};
|
};
|
||||||
use crate::xml_parser::XmlParseError;
|
use crate::xml_parsers::XmlParseError;
|
||||||
use quick_xml::events::Event;
|
use quick_xml::events::Event;
|
||||||
use quick_xml::reader::Reader;
|
use quick_xml::reader::Reader;
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
|
|||||||
@@ -52,7 +52,7 @@
|
|||||||
pub mod types;
|
pub mod types;
|
||||||
pub mod databases;
|
pub mod databases;
|
||||||
pub mod schema;
|
pub mod schema;
|
||||||
mod xml_parser;
|
mod xml_parsers;
|
||||||
mod item_loader;
|
mod item_loader;
|
||||||
mod image_processor;
|
mod image_processor;
|
||||||
|
|
||||||
@@ -70,6 +70,9 @@ pub use databases::{
|
|||||||
MinimapDatabase,
|
MinimapDatabase,
|
||||||
MinimapDatabaseError,
|
MinimapDatabaseError,
|
||||||
StorageStats,
|
StorageStats,
|
||||||
|
IconDatabase,
|
||||||
|
IconDatabaseError,
|
||||||
|
IconStats,
|
||||||
};
|
};
|
||||||
pub use types::{
|
pub use types::{
|
||||||
// Items
|
// Items
|
||||||
@@ -124,6 +127,21 @@ pub use types::{
|
|||||||
MinimapTile,
|
MinimapTile,
|
||||||
MinimapTileRecord,
|
MinimapTileRecord,
|
||||||
NewMinimapTile,
|
NewMinimapTile,
|
||||||
|
// Icons
|
||||||
|
AbilityIconRecord,
|
||||||
|
NewAbilityIcon,
|
||||||
|
BuffIconRecord,
|
||||||
|
NewBuffIcon,
|
||||||
|
TraitIconRecord,
|
||||||
|
NewTraitIcon,
|
||||||
|
PlayerHouseIconRecord,
|
||||||
|
NewPlayerHouseIcon,
|
||||||
|
StatIconRecord,
|
||||||
|
NewStatIcon,
|
||||||
|
AchievementIconRecord,
|
||||||
|
NewAchievementIcon,
|
||||||
|
GeneralIconRecord,
|
||||||
|
NewGeneralIcon,
|
||||||
};
|
};
|
||||||
pub use xml_parser::XmlParseError;
|
pub use xml_parsers::XmlParseError;
|
||||||
pub use image_processor::{ImageProcessor, ImageProcessingError, ProcessedImages, OutlineConfig};
|
pub use image_processor::{ImageProcessor, ImageProcessingError, ProcessedImages, OutlineConfig};
|
||||||
|
|||||||
@@ -1,5 +1,26 @@
|
|||||||
// @generated automatically by Diesel CLI.
|
// @generated automatically by Diesel CLI.
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
ability_icons (name) {
|
||||||
|
name -> Text,
|
||||||
|
icon -> Binary,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
achievement_icons (name) {
|
||||||
|
name -> Text,
|
||||||
|
icon -> Binary,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
buff_icons (name) {
|
||||||
|
name -> Text,
|
||||||
|
icon -> Binary,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
diesel::table! {
|
diesel::table! {
|
||||||
crafting_recipe_items (recipe_id, item_id) {
|
crafting_recipe_items (recipe_id, item_id) {
|
||||||
recipe_id -> Integer,
|
recipe_id -> Integer,
|
||||||
@@ -22,11 +43,26 @@ diesel::table! {
|
|||||||
}
|
}
|
||||||
|
|
||||||
diesel::table! {
|
diesel::table! {
|
||||||
fast_travel_locations (id) {
|
fast_travel_locations (name) {
|
||||||
id -> Nullable<Integer>,
|
name -> Nullable<Text>,
|
||||||
|
pos_x -> Float,
|
||||||
|
pos_z -> Float,
|
||||||
|
travel_type -> Text,
|
||||||
|
unlocked -> Integer,
|
||||||
|
connections -> Nullable<Text>,
|
||||||
|
checks -> Nullable<Text>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
general_icons (name) {
|
||||||
name -> Text,
|
name -> Text,
|
||||||
map_name -> Text,
|
original_width -> Integer,
|
||||||
data -> Text,
|
original_height -> Integer,
|
||||||
|
icon_original -> Nullable<Binary>,
|
||||||
|
icon_256 -> Nullable<Binary>,
|
||||||
|
icon_64 -> Nullable<Binary>,
|
||||||
|
icon_32 -> Nullable<Binary>,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,12 +177,21 @@ diesel::table! {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
player_house_icons (name) {
|
||||||
|
name -> Text,
|
||||||
|
icon -> Binary,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
diesel::table! {
|
diesel::table! {
|
||||||
player_houses (id) {
|
player_houses (id) {
|
||||||
id -> Nullable<Integer>,
|
id -> Nullable<Integer>,
|
||||||
name -> Text,
|
name -> Text,
|
||||||
map_id -> Integer,
|
description -> Text,
|
||||||
data -> Text,
|
pos_x -> Float,
|
||||||
|
pos_z -> Float,
|
||||||
|
price -> Integer,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,6 +221,20 @@ diesel::table! {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
stat_icons (name) {
|
||||||
|
name -> Text,
|
||||||
|
icon -> Binary,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
trait_icons (name) {
|
||||||
|
name -> Text,
|
||||||
|
icon -> Binary,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
diesel::table! {
|
diesel::table! {
|
||||||
traits (id) {
|
traits (id) {
|
||||||
id -> Nullable<Integer>,
|
id -> Nullable<Integer>,
|
||||||
@@ -251,9 +310,13 @@ diesel::joinable!(harvestable_drops -> items (item_id));
|
|||||||
diesel::joinable!(item_stats -> items (item_id));
|
diesel::joinable!(item_stats -> items (item_id));
|
||||||
|
|
||||||
diesel::allow_tables_to_appear_in_same_query!(
|
diesel::allow_tables_to_appear_in_same_query!(
|
||||||
|
ability_icons,
|
||||||
|
achievement_icons,
|
||||||
|
buff_icons,
|
||||||
crafting_recipe_items,
|
crafting_recipe_items,
|
||||||
crafting_recipes,
|
crafting_recipes,
|
||||||
fast_travel_locations,
|
fast_travel_locations,
|
||||||
|
general_icons,
|
||||||
harvestable_drops,
|
harvestable_drops,
|
||||||
harvestables,
|
harvestables,
|
||||||
item_stats,
|
item_stats,
|
||||||
@@ -262,10 +325,13 @@ diesel::allow_tables_to_appear_in_same_query!(
|
|||||||
maps,
|
maps,
|
||||||
minimap_tiles,
|
minimap_tiles,
|
||||||
npcs,
|
npcs,
|
||||||
|
player_house_icons,
|
||||||
player_houses,
|
player_houses,
|
||||||
quests,
|
quests,
|
||||||
resource_icons,
|
resource_icons,
|
||||||
shops,
|
shops,
|
||||||
|
stat_icons,
|
||||||
|
trait_icons,
|
||||||
traits,
|
traits,
|
||||||
world_loot,
|
world_loot,
|
||||||
world_map_icons,
|
world_map_icons,
|
||||||
|
|||||||
@@ -30,8 +30,11 @@ pub struct FastTravelLocation {
|
|||||||
/// Display name
|
/// Display name
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
|
||||||
/// 3D position in world space (x,y,z)
|
/// X position in world space
|
||||||
pub position: String,
|
pub pos_x: f32,
|
||||||
|
|
||||||
|
/// Z position in world space
|
||||||
|
pub pos_z: f32,
|
||||||
|
|
||||||
/// Type of fast travel
|
/// Type of fast travel
|
||||||
pub travel_type: FastTravelType,
|
pub travel_type: FastTravelType,
|
||||||
@@ -49,11 +52,12 @@ pub struct FastTravelLocation {
|
|||||||
|
|
||||||
impl FastTravelLocation {
|
impl FastTravelLocation {
|
||||||
/// Create a new FastTravelLocation with required fields
|
/// Create a new FastTravelLocation with required fields
|
||||||
pub fn new(id: i32, name: String, position: String, travel_type: FastTravelType) -> Self {
|
pub fn new(id: i32, name: String, pos_x: f32, pos_z: f32, travel_type: FastTravelType) -> Self {
|
||||||
Self {
|
Self {
|
||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
position,
|
pos_x,
|
||||||
|
pos_z,
|
||||||
travel_type,
|
travel_type,
|
||||||
unlocked: false,
|
unlocked: false,
|
||||||
connections: None,
|
connections: None,
|
||||||
@@ -61,19 +65,9 @@ impl FastTravelLocation {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse position into (x, y, z) coordinates
|
/// Get position as (x, z) tuple
|
||||||
pub fn get_position(&self) -> Option<(f32, f32, f32)> {
|
pub fn get_position(&self) -> (f32, f32) {
|
||||||
let parts: Vec<&str> = self.position.split(',').collect();
|
(self.pos_x, self.pos_z)
|
||||||
if parts.len() == 3 {
|
|
||||||
if let (Ok(x), Ok(y), Ok(z)) = (
|
|
||||||
parts[0].parse::<f32>(),
|
|
||||||
parts[1].parse::<f32>(),
|
|
||||||
parts[2].parse::<f32>(),
|
|
||||||
) {
|
|
||||||
return Some((x, y, z));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get list of connected location IDs
|
/// Get list of connected location IDs
|
||||||
|
|||||||
134
cursebreaker-parser/src/types/cursebreaker/icon_models.rs
Normal file
134
cursebreaker-parser/src/types/cursebreaker/icon_models.rs
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
use diesel::prelude::*;
|
||||||
|
use crate::schema::{
|
||||||
|
ability_icons, buff_icons, trait_icons, player_house_icons, stat_icons,
|
||||||
|
achievement_icons, general_icons
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Diesel queryable model for ability_icons table
|
||||||
|
#[derive(Queryable, Selectable, Debug, Clone)]
|
||||||
|
#[diesel(table_name = ability_icons)]
|
||||||
|
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
|
||||||
|
pub struct AbilityIconRecord {
|
||||||
|
pub name: String,
|
||||||
|
pub icon: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Diesel insertable model for ability_icons table
|
||||||
|
#[derive(Insertable, Debug)]
|
||||||
|
#[diesel(table_name = ability_icons)]
|
||||||
|
pub struct NewAbilityIcon<'a> {
|
||||||
|
pub name: &'a str,
|
||||||
|
pub icon: &'a [u8],
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Diesel queryable model for buff_icons table
|
||||||
|
#[derive(Queryable, Selectable, Debug, Clone)]
|
||||||
|
#[diesel(table_name = buff_icons)]
|
||||||
|
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
|
||||||
|
pub struct BuffIconRecord {
|
||||||
|
pub name: String,
|
||||||
|
pub icon: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Diesel insertable model for buff_icons table
|
||||||
|
#[derive(Insertable, Debug)]
|
||||||
|
#[diesel(table_name = buff_icons)]
|
||||||
|
pub struct NewBuffIcon<'a> {
|
||||||
|
pub name: &'a str,
|
||||||
|
pub icon: &'a [u8],
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Diesel queryable model for trait_icons table
|
||||||
|
#[derive(Queryable, Selectable, Debug, Clone)]
|
||||||
|
#[diesel(table_name = trait_icons)]
|
||||||
|
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
|
||||||
|
pub struct TraitIconRecord {
|
||||||
|
pub name: String,
|
||||||
|
pub icon: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Diesel insertable model for trait_icons table
|
||||||
|
#[derive(Insertable, Debug)]
|
||||||
|
#[diesel(table_name = trait_icons)]
|
||||||
|
pub struct NewTraitIcon<'a> {
|
||||||
|
pub name: &'a str,
|
||||||
|
pub icon: &'a [u8],
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Diesel queryable model for player_house_icons table
|
||||||
|
#[derive(Queryable, Selectable, Debug, Clone)]
|
||||||
|
#[diesel(table_name = player_house_icons)]
|
||||||
|
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
|
||||||
|
pub struct PlayerHouseIconRecord {
|
||||||
|
pub name: String,
|
||||||
|
pub icon: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Diesel insertable model for player_house_icons table
|
||||||
|
#[derive(Insertable, Debug)]
|
||||||
|
#[diesel(table_name = player_house_icons)]
|
||||||
|
pub struct NewPlayerHouseIcon<'a> {
|
||||||
|
pub name: &'a str,
|
||||||
|
pub icon: &'a [u8],
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Diesel queryable model for stat_icons table
|
||||||
|
#[derive(Queryable, Selectable, Debug, Clone)]
|
||||||
|
#[diesel(table_name = stat_icons)]
|
||||||
|
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
|
||||||
|
pub struct StatIconRecord {
|
||||||
|
pub name: String,
|
||||||
|
pub icon: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Diesel insertable model for stat_icons table
|
||||||
|
#[derive(Insertable, Debug)]
|
||||||
|
#[diesel(table_name = stat_icons)]
|
||||||
|
pub struct NewStatIcon<'a> {
|
||||||
|
pub name: &'a str,
|
||||||
|
pub icon: &'a [u8],
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Diesel queryable model for achievement_icons table
|
||||||
|
#[derive(Queryable, Selectable, Debug, Clone)]
|
||||||
|
#[diesel(table_name = achievement_icons)]
|
||||||
|
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
|
||||||
|
pub struct AchievementIconRecord {
|
||||||
|
pub name: String,
|
||||||
|
pub icon: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Diesel insertable model for achievement_icons table
|
||||||
|
#[derive(Insertable, Debug)]
|
||||||
|
#[diesel(table_name = achievement_icons)]
|
||||||
|
pub struct NewAchievementIcon<'a> {
|
||||||
|
pub name: &'a str,
|
||||||
|
pub icon: &'a [u8],
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Diesel queryable model for general_icons table
|
||||||
|
#[derive(Queryable, Selectable, Debug, Clone)]
|
||||||
|
#[diesel(table_name = general_icons)]
|
||||||
|
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
|
||||||
|
pub struct GeneralIconRecord {
|
||||||
|
pub name: String,
|
||||||
|
pub original_width: i32,
|
||||||
|
pub original_height: i32,
|
||||||
|
pub icon_original: Option<Vec<u8>>,
|
||||||
|
pub icon_256: Option<Vec<u8>>,
|
||||||
|
pub icon_64: Option<Vec<u8>>,
|
||||||
|
pub icon_32: Option<Vec<u8>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Diesel insertable model for general_icons table
|
||||||
|
#[derive(Insertable, Debug)]
|
||||||
|
#[diesel(table_name = general_icons)]
|
||||||
|
pub struct NewGeneralIcon<'a> {
|
||||||
|
pub name: &'a str,
|
||||||
|
pub original_width: i32,
|
||||||
|
pub original_height: i32,
|
||||||
|
pub icon_original: Option<&'a [u8]>,
|
||||||
|
pub icon_256: Option<&'a [u8]>,
|
||||||
|
pub icon_64: Option<&'a [u8]>,
|
||||||
|
pub icon_32: Option<&'a [u8]>,
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ mod r#trait;
|
|||||||
mod shop;
|
mod shop;
|
||||||
mod minimap_tile;
|
mod minimap_tile;
|
||||||
mod minimap_models;
|
mod minimap_models;
|
||||||
|
mod icon_models;
|
||||||
|
|
||||||
pub use item::{
|
pub use item::{
|
||||||
// Main types
|
// Main types
|
||||||
@@ -44,3 +45,19 @@ pub use r#trait::{Trait, TraitTrainer};
|
|||||||
pub use shop::{Shop, ShopItem};
|
pub use shop::{Shop, ShopItem};
|
||||||
pub use minimap_tile::MinimapTile;
|
pub use minimap_tile::MinimapTile;
|
||||||
pub use minimap_models::{MinimapTileRecord, NewMinimapTile};
|
pub use minimap_models::{MinimapTileRecord, NewMinimapTile};
|
||||||
|
pub use icon_models::{
|
||||||
|
AbilityIconRecord,
|
||||||
|
NewAbilityIcon,
|
||||||
|
BuffIconRecord,
|
||||||
|
NewBuffIcon,
|
||||||
|
TraitIconRecord,
|
||||||
|
NewTraitIcon,
|
||||||
|
PlayerHouseIconRecord,
|
||||||
|
NewPlayerHouseIcon,
|
||||||
|
StatIconRecord,
|
||||||
|
NewStatIcon,
|
||||||
|
AchievementIconRecord,
|
||||||
|
NewAchievementIcon,
|
||||||
|
GeneralIconRecord,
|
||||||
|
NewGeneralIcon,
|
||||||
|
};
|
||||||
|
|||||||
@@ -12,42 +12,32 @@ pub struct PlayerHouse {
|
|||||||
/// Description text
|
/// Description text
|
||||||
pub description: String,
|
pub description: String,
|
||||||
|
|
||||||
/// 3D position in world space (x,y,z)
|
/// X position in world space
|
||||||
pub position: String,
|
pub pos_x: f32,
|
||||||
|
|
||||||
|
/// Z position in world space
|
||||||
|
pub pos_z: f32,
|
||||||
|
|
||||||
/// Purchase price in gold
|
/// Purchase price in gold
|
||||||
pub price: i32,
|
pub price: i32,
|
||||||
|
|
||||||
/// Whether this house is hidden (not shown in normal lists)
|
|
||||||
pub hidden: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PlayerHouse {
|
impl PlayerHouse {
|
||||||
/// Create a new PlayerHouse with required fields
|
/// Create a new PlayerHouse with required fields
|
||||||
pub fn new(id: i32, name: String, description: String, position: String, price: i32) -> Self {
|
pub fn new(id: i32, name: String, description: String, pos_x: f32, pos_z: f32, price: i32) -> Self {
|
||||||
Self {
|
Self {
|
||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
position,
|
pos_x,
|
||||||
|
pos_z,
|
||||||
price,
|
price,
|
||||||
hidden: false,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse position into (x, y, z) coordinates
|
/// Get position as (x, z) tuple
|
||||||
pub fn get_position(&self) -> Option<(f32, f32, f32)> {
|
pub fn get_position(&self) -> (f32, f32) {
|
||||||
let parts: Vec<&str> = self.position.split(',').collect();
|
(self.pos_x, self.pos_z)
|
||||||
if parts.len() == 3 {
|
|
||||||
if let (Ok(x), Ok(y), Ok(z)) = (
|
|
||||||
parts[0].parse::<f32>(),
|
|
||||||
parts[1].parse::<f32>(),
|
|
||||||
parts[2].parse::<f32>(),
|
|
||||||
) {
|
|
||||||
return Some((x, y, z));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if this house is free (price is 0)
|
/// Check if this house is free (price is 0)
|
||||||
@@ -55,11 +45,6 @@ impl PlayerHouse {
|
|||||||
self.price == 0
|
self.price == 0
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if this house is visible (not hidden)
|
|
||||||
pub fn is_visible(&self) -> bool {
|
|
||||||
!self.hidden
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if this house is expensive (price >= 10000)
|
/// Check if this house is expensive (price >= 10000)
|
||||||
pub fn is_expensive(&self) -> bool {
|
pub fn is_expensive(&self) -> bool {
|
||||||
self.price >= 10000
|
self.price >= 10000
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
/// Interactable_TeleporterTeleporter component from Cursebreaker
|
/// Interactable_TeleporterDoor component from Cursebreaker
|
||||||
///
|
///
|
||||||
/// C# definition from Interactable_TeleporterTeleporter.cs:
|
/// C# definition from Interactable_TeleporterDoor.cs:
|
||||||
/// ```csharp
|
/// ```csharp
|
||||||
/// public class Interactable_TeleporterTeleporter : MonoBehaviour
|
/// public class Interactable_TeleporterDoor : MonoBehaviour
|
||||||
/// {
|
/// {
|
||||||
/// public Transform tpTransform;
|
/// public Transform tpTransform;
|
||||||
/// }
|
/// }
|
||||||
@@ -53,7 +53,7 @@ impl EcsInsertable for InteractableTeleporter {
|
|||||||
inventory::submit! {
|
inventory::submit! {
|
||||||
unity_parser::ComponentRegistration {
|
unity_parser::ComponentRegistration {
|
||||||
type_id: 114,
|
type_id: 114,
|
||||||
class_name: "Interactable_TeleporterTeleporter",
|
class_name: "Interactable_TeleporterDoorEditor",
|
||||||
parse_and_insert: |yaml, ctx, world, entity| {
|
parse_and_insert: |yaml, ctx, world, entity| {
|
||||||
<InteractableTeleporter as EcsInsertable>::parse_and_insert(yaml, ctx, world, entity)
|
<InteractableTeleporter as EcsInsertable>::parse_and_insert(yaml, ctx, world, entity)
|
||||||
},
|
},
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
127
cursebreaker-parser/src/xml_parsers/fast_travel.rs
Normal file
127
cursebreaker-parser/src/xml_parsers/fast_travel.rs
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
//! Fast Travel XML Parser
|
||||||
|
|
||||||
|
use crate::types::{FastTravelLocation, FastTravelType};
|
||||||
|
use super::{parse_attributes, XmlParseError};
|
||||||
|
use quick_xml::events::Event;
|
||||||
|
use quick_xml::reader::Reader;
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::BufReader;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
/// Parse FastTravelLocations.xml (regular fast travel locations)
|
||||||
|
pub fn parse_fast_travel_locations_xml<P: AsRef<Path>>(
|
||||||
|
path: P,
|
||||||
|
) -> Result<Vec<FastTravelLocation>, XmlParseError> {
|
||||||
|
parse_fast_travel_xml_internal(path, FastTravelType::Location)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse FastTravelCanoe.xml (canoe fast travel locations)
|
||||||
|
pub fn parse_fast_travel_canoe_xml<P: AsRef<Path>>(
|
||||||
|
path: P,
|
||||||
|
) -> Result<Vec<FastTravelLocation>, XmlParseError> {
|
||||||
|
parse_fast_travel_xml_internal(path, FastTravelType::Canoe)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse FastTravelPortals.xml (portal fast travel locations)
|
||||||
|
pub fn parse_fast_travel_portals_xml<P: AsRef<Path>>(
|
||||||
|
path: P,
|
||||||
|
) -> Result<Vec<FastTravelLocation>, XmlParseError> {
|
||||||
|
parse_fast_travel_xml_internal(path, FastTravelType::Portal)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Internal function to parse any fast travel XML file
|
||||||
|
fn parse_fast_travel_xml_internal<P: AsRef<Path>>(
|
||||||
|
path: P,
|
||||||
|
travel_type: FastTravelType,
|
||||||
|
) -> Result<Vec<FastTravelLocation>, XmlParseError> {
|
||||||
|
let file = File::open(path)?;
|
||||||
|
let buf_reader = BufReader::new(file);
|
||||||
|
let mut reader = Reader::from_reader(buf_reader);
|
||||||
|
reader.config_mut().trim_text(true);
|
||||||
|
|
||||||
|
let mut locations = Vec::new();
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
match reader.read_event_into(&mut buf) {
|
||||||
|
Ok(Event::Start(ref e)) | Ok(Event::Empty(ref e)) => {
|
||||||
|
match e.name().as_ref() {
|
||||||
|
b"location" => {
|
||||||
|
let attrs = parse_attributes(e)?;
|
||||||
|
|
||||||
|
// Get required attributes
|
||||||
|
let id = attrs
|
||||||
|
.get("id")
|
||||||
|
.ok_or_else(|| XmlParseError::MissingAttribute("id".to_string()))?
|
||||||
|
.parse::<i32>()
|
||||||
|
.map_err(|_| XmlParseError::InvalidAttribute("id".to_string()))?;
|
||||||
|
|
||||||
|
let name = attrs
|
||||||
|
.get("name")
|
||||||
|
.ok_or_else(|| XmlParseError::MissingAttribute("name".to_string()))?
|
||||||
|
.clone();
|
||||||
|
|
||||||
|
let position_str = attrs
|
||||||
|
.get("pos")
|
||||||
|
.ok_or_else(|| XmlParseError::MissingAttribute("pos".to_string()))?;
|
||||||
|
|
||||||
|
// Parse position "x,y,z" and extract x,z (discard y)
|
||||||
|
let (pos_x, pos_z) = parse_position(position_str)?;
|
||||||
|
|
||||||
|
let mut location = FastTravelLocation::new(id, name, pos_x, pos_z, travel_type);
|
||||||
|
|
||||||
|
// Parse optional attributes based on type
|
||||||
|
match travel_type {
|
||||||
|
FastTravelType::Location => {
|
||||||
|
// Regular locations have unlocked and connections
|
||||||
|
if attrs.get("unlocked").is_some() {
|
||||||
|
location.unlocked = true;
|
||||||
|
}
|
||||||
|
if let Some(v) = attrs.get("connections") {
|
||||||
|
location.connections = Some(v.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FastTravelType::Canoe => {
|
||||||
|
// Canoe locations have checks
|
||||||
|
if let Some(v) = attrs.get("checks") {
|
||||||
|
location.checks = Some(v.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FastTravelType::Portal => {
|
||||||
|
// Portals have no additional fields
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
locations.push(location);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Event::Eof) => break,
|
||||||
|
Err(e) => return Err(XmlParseError::XmlError(e)),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
buf.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(locations)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse position string "x,y,z" and return (x, z), discarding y
|
||||||
|
fn parse_position(pos: &str) -> Result<(f32, f32), XmlParseError> {
|
||||||
|
let parts: Vec<&str> = pos.split(',').collect();
|
||||||
|
if parts.len() != 3 {
|
||||||
|
return Err(XmlParseError::InvalidAttribute("pos".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let x = parts[0]
|
||||||
|
.trim()
|
||||||
|
.parse::<f32>()
|
||||||
|
.map_err(|_| XmlParseError::InvalidAttribute("pos.x".to_string()))?;
|
||||||
|
let z = parts[2]
|
||||||
|
.trim()
|
||||||
|
.parse::<f32>()
|
||||||
|
.map_err(|_| XmlParseError::InvalidAttribute("pos.z".to_string()))?;
|
||||||
|
|
||||||
|
Ok((x, z))
|
||||||
|
}
|
||||||
127
cursebreaker-parser/src/xml_parsers/harvestables.rs
Normal file
127
cursebreaker-parser/src/xml_parsers/harvestables.rs
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
//! Harvestable XML Parser
|
||||||
|
|
||||||
|
use crate::types::{Harvestable, HarvestableDrop, SkillType, Tool};
|
||||||
|
use super::{parse_attributes, parse_health_range, XmlParseError};
|
||||||
|
use quick_xml::events::Event;
|
||||||
|
use quick_xml::reader::Reader;
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::BufReader;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
pub fn parse_harvestables_xml<P: AsRef<Path>>(path: P) -> Result<Vec<Harvestable>, XmlParseError> {
|
||||||
|
let file = File::open(path)?;
|
||||||
|
let buf_reader = BufReader::new(file);
|
||||||
|
let mut reader = Reader::from_reader(buf_reader);
|
||||||
|
reader.config_mut().trim_text(true);
|
||||||
|
|
||||||
|
let mut harvestables = Vec::new();
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
let mut current_harvestable: Option<Harvestable> = None;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
match reader.read_event_into(&mut buf) {
|
||||||
|
Ok(Event::Start(ref e)) | Ok(Event::Empty(ref e)) => {
|
||||||
|
match e.name().as_ref() {
|
||||||
|
b"harvestable" => {
|
||||||
|
let attrs = parse_attributes(e)?;
|
||||||
|
let typeid = attrs.get("typeid")
|
||||||
|
.ok_or_else(|| XmlParseError::MissingAttribute("typeid".to_string()))?
|
||||||
|
.parse::<i32>()
|
||||||
|
.map_err(|_| XmlParseError::InvalidAttribute("typeid".to_string()))?;
|
||||||
|
|
||||||
|
let name = attrs.get("name")
|
||||||
|
.ok_or_else(|| XmlParseError::MissingAttribute("name".to_string()))?
|
||||||
|
.clone();
|
||||||
|
|
||||||
|
let mut harvestable = Harvestable::new(typeid, name);
|
||||||
|
|
||||||
|
// Parse optional attributes with defaults
|
||||||
|
if let Some(v) = attrs.get("actionname") { harvestable.actionname = v.clone(); }
|
||||||
|
if let Some(v) = attrs.get("desc") { harvestable.desc = v.clone(); }
|
||||||
|
if let Some(v) = attrs.get("comment") { harvestable.comment = v.clone(); }
|
||||||
|
if let Some(v) = attrs.get("level") { harvestable.level = v.parse().unwrap_or(0); }
|
||||||
|
if let Some(v) = attrs.get("skill") { harvestable.skill = v.parse().unwrap_or(SkillType::None); }
|
||||||
|
if let Some(v) = attrs.get("tool") { harvestable.tool = v.parse().unwrap_or(Tool::None); }
|
||||||
|
if let Some(v) = attrs.get("health") {
|
||||||
|
let (min, max) = parse_health_range(v);
|
||||||
|
harvestable.min_health = min;
|
||||||
|
harvestable.max_health = max;
|
||||||
|
}
|
||||||
|
if let Some(v) = attrs.get("harvesttime") { harvestable.harvesttime = v.parse().unwrap_or(0); }
|
||||||
|
if let Some(v) = attrs.get("hittime") { harvestable.hittime = v.parse().unwrap_or(0); }
|
||||||
|
if let Some(v) = attrs.get("respawntime") { harvestable.respawntime = v.parse().unwrap_or(0); }
|
||||||
|
|
||||||
|
// Audio (handle both cases: harvestSfx and harvestsfx)
|
||||||
|
if let Some(v) = attrs.get("harvestSfx").or_else(|| attrs.get("harvestsfx")) {
|
||||||
|
harvestable.harvestsfx = v.clone();
|
||||||
|
}
|
||||||
|
if let Some(v) = attrs.get("endSfx").or_else(|| attrs.get("endsfx")) {
|
||||||
|
harvestable.endsfx = v.clone();
|
||||||
|
}
|
||||||
|
if let Some(v) = attrs.get("receiveItemSfx").or_else(|| attrs.get("receiveitemsfx")) {
|
||||||
|
harvestable.receiveitemsfx = v.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(v) = attrs.get("animation") { harvestable.animation = v.clone(); }
|
||||||
|
if let Some(v) = attrs.get("takehitanimation") { harvestable.takehitanimation = v.clone(); }
|
||||||
|
if let Some(v) = attrs.get("endgfx") { harvestable.endgfx = v.clone(); }
|
||||||
|
if let Some(v) = attrs.get("tree") { harvestable.tree = v.parse().unwrap_or(0) == 1; }
|
||||||
|
if let Some(v) = attrs.get("hidemilestone") { harvestable.hidemilestone = v.parse().unwrap_or(0) == 1; }
|
||||||
|
if let Some(v) = attrs.get("nohighlight") { harvestable.nohighlight = v.parse().unwrap_or(0) == 1; }
|
||||||
|
|
||||||
|
// Handle both cases: hideMinimap and hideminimap
|
||||||
|
if let Some(v) = attrs.get("hideMinimap").or_else(|| attrs.get("hideminimap")) {
|
||||||
|
harvestable.hideminimap = v.parse().unwrap_or(0) == 1;
|
||||||
|
}
|
||||||
|
if let Some(v) = attrs.get("noLeftClickInteract").or_else(|| attrs.get("noleftclickinteract")) {
|
||||||
|
harvestable.noleftclickinteract = v.parse().unwrap_or(0) == 1;
|
||||||
|
}
|
||||||
|
if let Some(v) = attrs.get("interactDistance").or_else(|| attrs.get("interactdistance")) {
|
||||||
|
harvestable.interactdistance = v.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
current_harvestable = Some(harvestable);
|
||||||
|
}
|
||||||
|
b"item" if current_harvestable.is_some() => {
|
||||||
|
if let Some(ref mut harvestable) = current_harvestable {
|
||||||
|
let attrs = parse_attributes(e)?;
|
||||||
|
if let Some(id_str) = attrs.get("id") {
|
||||||
|
if let Ok(id) = id_str.parse::<i32>() {
|
||||||
|
let drop = HarvestableDrop {
|
||||||
|
id,
|
||||||
|
minamount: attrs.get("minamount").and_then(|v| v.parse().ok()).unwrap_or(0),
|
||||||
|
maxamount: attrs.get("maxamount").and_then(|v| v.parse().ok()).unwrap_or(0),
|
||||||
|
droprate: attrs.get("droprate").and_then(|v| v.parse().ok()).unwrap_or(0),
|
||||||
|
droprateboost: attrs.get("droprateboost").and_then(|v| v.parse().ok()).unwrap_or(0),
|
||||||
|
amountboost: attrs.get("amountboost").and_then(|v| v.parse().ok()).unwrap_or(0),
|
||||||
|
checks: attrs.get("checks").cloned().unwrap_or_default(),
|
||||||
|
comment: attrs.get("comment").cloned().unwrap_or_default(),
|
||||||
|
dontconsumehealth: attrs.get("dontconsumehealth").and_then(|v| v.parse().ok()).unwrap_or(0) == 1,
|
||||||
|
};
|
||||||
|
harvestable.drops.push(drop);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Event::End(ref e)) => {
|
||||||
|
match e.name().as_ref() {
|
||||||
|
b"harvestable" => {
|
||||||
|
if let Some(harvestable) = current_harvestable.take() {
|
||||||
|
harvestables.push(harvestable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Event::Eof) => break,
|
||||||
|
Err(e) => return Err(XmlParseError::XmlError(e)),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
buf.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(harvestables)
|
||||||
|
}
|
||||||
141
cursebreaker-parser/src/xml_parsers/items.rs
Normal file
141
cursebreaker-parser/src/xml_parsers/items.rs
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
//! Item XML Parser
|
||||||
|
|
||||||
|
use crate::types::{Item, ItemStat, AnimationSet};
|
||||||
|
use super::{parse_attributes, XmlParseError};
|
||||||
|
use quick_xml::events::Event;
|
||||||
|
use quick_xml::reader::Reader;
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::BufReader;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
pub fn parse_items_xml<P: AsRef<Path>>(path: P) -> Result<Vec<Item>, XmlParseError> {
|
||||||
|
let file = File::open(path)?;
|
||||||
|
let buf_reader = BufReader::new(file);
|
||||||
|
let mut reader = Reader::from_reader(buf_reader);
|
||||||
|
reader.config_mut().trim_text(true);
|
||||||
|
|
||||||
|
let mut items = Vec::new();
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
let mut current_item: Option<Item> = None;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
match reader.read_event_into(&mut buf) {
|
||||||
|
Ok(Event::Start(e)) | Ok(Event::Empty(e)) => {
|
||||||
|
match e.name().as_ref() {
|
||||||
|
b"item" => {
|
||||||
|
let attrs = parse_attributes(&e)?;
|
||||||
|
|
||||||
|
// Get required attributes
|
||||||
|
let id = attrs.get("id")
|
||||||
|
.ok_or_else(|| XmlParseError::MissingAttribute("id".to_string()))?
|
||||||
|
.parse::<i32>()
|
||||||
|
.map_err(|_| XmlParseError::InvalidAttribute("id".to_string()))?;
|
||||||
|
|
||||||
|
let name = attrs.get("name")
|
||||||
|
.ok_or_else(|| XmlParseError::MissingAttribute("name".to_string()))?
|
||||||
|
.clone();
|
||||||
|
|
||||||
|
let mut item = Item::new(id, name);
|
||||||
|
|
||||||
|
// Note: This is a legacy simple parser. For full functionality,
|
||||||
|
// use the item_loader module's load_items_from_directory function.
|
||||||
|
|
||||||
|
// Parse optional attributes using defaults for new structure
|
||||||
|
if let Some(v) = attrs.get("level") { item.level = v.parse().unwrap_or(1); }
|
||||||
|
if let Some(v) = attrs.get("description") { item.description = v.clone(); }
|
||||||
|
if let Some(v) = attrs.get("price") { item.price = v.parse().unwrap_or(0); }
|
||||||
|
if let Some(v) = attrs.get("maxstack") { item.max_stack = v.parse().unwrap_or(1); }
|
||||||
|
if let Some(v) = attrs.get("abilityid") { item.ability_id = v.parse().unwrap_or(0); }
|
||||||
|
if let Some(v) = attrs.get("swap") { item.swap_item = v.parse().unwrap_or(0); }
|
||||||
|
if attrs.get("twohanded").is_some() { item.two_handed = true; }
|
||||||
|
if let Some(v) = attrs.get("foodamount") { item.food_amount = v.parse().unwrap_or(0); }
|
||||||
|
if let Some(v) = attrs.get("foodfrequency") { item.food_frequency = v.parse().unwrap_or(0); }
|
||||||
|
if let Some(v) = attrs.get("foodtime") { item.food_time = v.parse().unwrap_or(0); }
|
||||||
|
if let Some(v) = attrs.get("foodlevel") { item.food_level = v.parse().unwrap_or(0); }
|
||||||
|
if let Some(v) = attrs.get("handmodel") { item.handmodel = v.clone(); }
|
||||||
|
if let Some(v) = attrs.get("groundmodel") {
|
||||||
|
item.ground_model = v.parse().unwrap_or(item.type_id);
|
||||||
|
}
|
||||||
|
if let Some(v) = attrs.get("usingitemmodel") { item.using_item_model = v.clone(); }
|
||||||
|
if let Some(v) = attrs.get("dropsfx") { item.drop_sfx = v.parse().unwrap_or(0); }
|
||||||
|
if let Some(v) = attrs.get("pickupsfx") { item.pickup_sfx = v.parse().unwrap_or(0); }
|
||||||
|
if let Some(v) = attrs.get("storagesize") { item.storage_size = v.parse().unwrap_or(0); }
|
||||||
|
if attrs.get("hidemilestone").is_some() { item.hide_milestone = true; }
|
||||||
|
if attrs.get("generateicon").is_some() { item.generate_icon = true; }
|
||||||
|
if let Some(v) = attrs.get("comment") { item.comment = v.clone(); }
|
||||||
|
|
||||||
|
current_item = Some(item);
|
||||||
|
}
|
||||||
|
b"stat" => {
|
||||||
|
if let Some(ref mut item) = current_item {
|
||||||
|
let attrs = parse_attributes(&e)?;
|
||||||
|
let stat = parse_stat(&attrs);
|
||||||
|
// Convert legacy ItemStat to new Stat vec
|
||||||
|
let stats = stat.to_stats();
|
||||||
|
item.stats.extend(stats);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b"crafting" => {
|
||||||
|
// Crafting recipes are now handled by item_loader for full functionality
|
||||||
|
// This is kept for backwards compatibility but doesn't fully populate the new structure
|
||||||
|
}
|
||||||
|
b"anim" => {
|
||||||
|
if let Some(ref mut item) = current_item {
|
||||||
|
let attrs = parse_attributes(&e)?;
|
||||||
|
let anim = AnimationSet {
|
||||||
|
idle: attrs.get("idle").and_then(|v| v.parse().ok()).unwrap_or(0),
|
||||||
|
walk: attrs.get("walk").and_then(|v| v.parse().ok()).unwrap_or(0),
|
||||||
|
run: attrs.get("run").and_then(|v| v.parse().ok()).unwrap_or(0),
|
||||||
|
weapon_attack: attrs.get("weaponattack").and_then(|v| v.parse().ok()).unwrap_or(0),
|
||||||
|
takehit: attrs.get("takehit").and_then(|v| v.parse().ok()).unwrap_or(0),
|
||||||
|
use_anim: attrs.get("use").and_then(|v| v.parse().ok()).unwrap_or(0),
|
||||||
|
};
|
||||||
|
item.animations = Some(anim);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b"generate" => {
|
||||||
|
// Generate rules are now handled by item_loader for full functionality
|
||||||
|
// This is kept for backwards compatibility but doesn't process them
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Event::End(e)) => {
|
||||||
|
match e.name().as_ref() {
|
||||||
|
b"item" => {
|
||||||
|
if let Some(item) = current_item.take() {
|
||||||
|
items.push(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Event::Eof) => break,
|
||||||
|
Err(e) => return Err(XmlParseError::XmlError(e)),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
buf.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(items)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_stat(attrs: &HashMap<String, String>) -> ItemStat {
|
||||||
|
ItemStat {
|
||||||
|
damagephysical: attrs.get("damagephysical").and_then(|v| v.parse().ok()),
|
||||||
|
damagemagical: attrs.get("damagemagical").and_then(|v| v.parse().ok()),
|
||||||
|
damageranged: attrs.get("damageranged").and_then(|v| v.parse().ok()),
|
||||||
|
accuracyphysical: attrs.get("accuracyphysical").and_then(|v| v.parse().ok()),
|
||||||
|
accuracymagical: attrs.get("accuracymagical").and_then(|v| v.parse().ok()),
|
||||||
|
accuracyranged: attrs.get("accuracyranged").and_then(|v| v.parse().ok()),
|
||||||
|
resistancephysical: attrs.get("resistancephysical").and_then(|v| v.parse().ok()),
|
||||||
|
resistancemagical: attrs.get("resistancemagical").and_then(|v| v.parse().ok()),
|
||||||
|
resistanceranged: attrs.get("resistanceranged").and_then(|v| v.parse().ok()),
|
||||||
|
health: attrs.get("health").and_then(|v| v.parse().ok()),
|
||||||
|
mana: attrs.get("mana").and_then(|v| v.parse().ok()),
|
||||||
|
manaregen: attrs.get("manaregen").and_then(|v| v.parse().ok()),
|
||||||
|
healing: attrs.get("healing").and_then(|v| v.parse().ok()),
|
||||||
|
harvestingspeedwoodcutting: attrs.get("harvestingspeedwoodcutting").and_then(|v| v.parse().ok()),
|
||||||
|
}
|
||||||
|
}
|
||||||
88
cursebreaker-parser/src/xml_parsers/loot.rs
Normal file
88
cursebreaker-parser/src/xml_parsers/loot.rs
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
//! Loot Table XML Parser
|
||||||
|
|
||||||
|
use crate::types::{LootTable, LootDrop};
|
||||||
|
use super::{parse_attributes, XmlParseError};
|
||||||
|
use quick_xml::events::Event;
|
||||||
|
use quick_xml::reader::Reader;
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::BufReader;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
pub fn parse_loot_xml<P: AsRef<Path>>(path: P) -> Result<Vec<LootTable>, XmlParseError> {
|
||||||
|
let file = File::open(path)?;
|
||||||
|
let buf_reader = BufReader::new(file);
|
||||||
|
let mut reader = Reader::from_reader(buf_reader);
|
||||||
|
reader.config_mut().trim_text(true);
|
||||||
|
|
||||||
|
let mut loot_tables = Vec::new();
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
let mut current_table: Option<LootTable> = None;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
match reader.read_event_into(&mut buf) {
|
||||||
|
Ok(Event::Start(ref e)) | Ok(Event::Empty(ref e)) => {
|
||||||
|
match e.name().as_ref() {
|
||||||
|
b"table" => {
|
||||||
|
let attrs = parse_attributes(e)?;
|
||||||
|
|
||||||
|
// Parse npcid - can be comma-separated like "45,459"
|
||||||
|
let npc_ids = if let Some(npcid_str) = attrs.get("npcid") {
|
||||||
|
npcid_str
|
||||||
|
.split(',')
|
||||||
|
.filter_map(|s| s.trim().parse::<i32>().ok())
|
||||||
|
.collect::<Vec<i32>>()
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut table = LootTable::new(npc_ids);
|
||||||
|
|
||||||
|
// Parse optional name
|
||||||
|
if let Some(v) = attrs.get("name") {
|
||||||
|
table.name = Some(v.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
current_table = Some(table);
|
||||||
|
}
|
||||||
|
b"drop" if current_table.is_some() => {
|
||||||
|
if let Some(ref mut table) = current_table {
|
||||||
|
let attrs = parse_attributes(e)?;
|
||||||
|
|
||||||
|
// Parse item ID (required for a drop)
|
||||||
|
if let Some(item_str) = attrs.get("item") {
|
||||||
|
if let Ok(item) = item_str.parse::<i32>() {
|
||||||
|
let drop = LootDrop {
|
||||||
|
item,
|
||||||
|
rate: attrs.get("rate").and_then(|v| v.parse().ok()),
|
||||||
|
minamount: attrs.get("minamount").and_then(|v| v.parse().ok()),
|
||||||
|
maxamount: attrs.get("maxamount").and_then(|v| v.parse().ok()),
|
||||||
|
checks: attrs.get("checks").cloned(),
|
||||||
|
comment: attrs.get("comment").cloned(),
|
||||||
|
};
|
||||||
|
table.drops.push(drop);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Event::End(ref e)) => {
|
||||||
|
match e.name().as_ref() {
|
||||||
|
b"table" => {
|
||||||
|
if let Some(table) = current_table.take() {
|
||||||
|
loot_tables.push(table);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Event::Eof) => break,
|
||||||
|
Err(e) => return Err(XmlParseError::XmlError(e)),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
buf.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(loot_tables)
|
||||||
|
}
|
||||||
128
cursebreaker-parser/src/xml_parsers/maps.rs
Normal file
128
cursebreaker-parser/src/xml_parsers/maps.rs
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
//! Map XML Parser
|
||||||
|
|
||||||
|
use crate::types::Map;
|
||||||
|
use super::{parse_attributes, XmlParseError};
|
||||||
|
use quick_xml::events::Event;
|
||||||
|
use quick_xml::reader::Reader;
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::BufReader;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
pub fn parse_maps_xml<P: AsRef<Path>>(path: P) -> Result<Vec<Map>, XmlParseError> {
|
||||||
|
let file = File::open(path)?;
|
||||||
|
let buf_reader = BufReader::new(file);
|
||||||
|
let mut reader = Reader::from_reader(buf_reader);
|
||||||
|
reader.config_mut().trim_text(true);
|
||||||
|
|
||||||
|
let mut maps = Vec::new();
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
match reader.read_event_into(&mut buf) {
|
||||||
|
Ok(Event::Start(ref e)) | Ok(Event::Empty(ref e)) => {
|
||||||
|
match e.name().as_ref() {
|
||||||
|
b"map" => {
|
||||||
|
let attrs = parse_attributes(e)?;
|
||||||
|
|
||||||
|
// Get required attributes
|
||||||
|
let scene_id = attrs.get("sceneid")
|
||||||
|
.ok_or_else(|| XmlParseError::MissingAttribute("sceneid".to_string()))?
|
||||||
|
.clone();
|
||||||
|
|
||||||
|
let music = attrs.get("music")
|
||||||
|
.ok_or_else(|| XmlParseError::MissingAttribute("music".to_string()))?
|
||||||
|
.parse::<i32>()
|
||||||
|
.map_err(|_| XmlParseError::InvalidAttribute("music".to_string()))?;
|
||||||
|
|
||||||
|
let ambience = attrs.get("ambience")
|
||||||
|
.ok_or_else(|| XmlParseError::MissingAttribute("ambience".to_string()))?
|
||||||
|
.parse::<i32>()
|
||||||
|
.map_err(|_| XmlParseError::InvalidAttribute("ambience".to_string()))?;
|
||||||
|
|
||||||
|
let mut map = Map::new(scene_id, music, ambience);
|
||||||
|
|
||||||
|
// Parse optional attributes
|
||||||
|
if let Some(v) = attrs.get("name") {
|
||||||
|
map.name = v.clone();
|
||||||
|
}
|
||||||
|
if let Some(v) = attrs.get("fogcolor") {
|
||||||
|
map.fog_color = Some(v.clone());
|
||||||
|
}
|
||||||
|
if let Some(v) = attrs.get("fogginess") {
|
||||||
|
map.fogginess = v.parse().ok();
|
||||||
|
}
|
||||||
|
if let Some(v) = attrs.get("viewdistance") {
|
||||||
|
map.view_distance = v.parse().ok();
|
||||||
|
}
|
||||||
|
if let Some(v) = attrs.get("npcviewdistance") {
|
||||||
|
map.npc_view_distance = v.parse().ok();
|
||||||
|
}
|
||||||
|
if let Some(v) = attrs.get("sunlight") {
|
||||||
|
map.sunlight = v.parse().ok();
|
||||||
|
}
|
||||||
|
if let Some(v) = attrs.get("suncolor") {
|
||||||
|
map.sun_color = Some(v.clone());
|
||||||
|
}
|
||||||
|
if let Some(v) = attrs.get("ambientcolor") {
|
||||||
|
map.ambient_color = Some(v.clone());
|
||||||
|
}
|
||||||
|
if let Some(v) = attrs.get("indoorsunlight") {
|
||||||
|
map.indoor_sunlight = v.parse().ok();
|
||||||
|
}
|
||||||
|
if let Some(v) = attrs.get("fogstart") {
|
||||||
|
map.fog_start = v.parse().ok();
|
||||||
|
}
|
||||||
|
if attrs.get("indoors").is_some() {
|
||||||
|
map.indoors = true;
|
||||||
|
}
|
||||||
|
if attrs.get("noworldmap").is_some() {
|
||||||
|
map.no_world_map = true;
|
||||||
|
}
|
||||||
|
if attrs.get("nominimap").is_some() {
|
||||||
|
map.no_minimap = true;
|
||||||
|
}
|
||||||
|
if attrs.get("tpdisabled").is_some() {
|
||||||
|
map.tp_disabled = true;
|
||||||
|
}
|
||||||
|
if attrs.get("dontloadnearbyscenes").is_some() {
|
||||||
|
map.dont_load_nearby_scenes = true;
|
||||||
|
}
|
||||||
|
if attrs.get("noborder").is_some() {
|
||||||
|
map.no_border = true;
|
||||||
|
}
|
||||||
|
if attrs.get("borderleft").is_some() {
|
||||||
|
map.border_left = true;
|
||||||
|
}
|
||||||
|
if attrs.get("borderright").is_some() {
|
||||||
|
map.border_right = true;
|
||||||
|
}
|
||||||
|
if attrs.get("borderup").is_some() {
|
||||||
|
map.border_up = true;
|
||||||
|
}
|
||||||
|
if attrs.get("borderdown").is_some() {
|
||||||
|
map.border_down = true;
|
||||||
|
}
|
||||||
|
if let Some(v) = attrs.get("respawnmap") {
|
||||||
|
map.respawn_map = Some(v.clone());
|
||||||
|
}
|
||||||
|
if let Some(v) = attrs.get("connectedmaps") {
|
||||||
|
map.connected_maps = Some(v.clone());
|
||||||
|
}
|
||||||
|
if let Some(v) = attrs.get("comment") {
|
||||||
|
map.comment = Some(v.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
maps.push(map);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Event::Eof) => break,
|
||||||
|
Err(e) => return Err(XmlParseError::XmlError(e)),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
buf.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(maps)
|
||||||
|
}
|
||||||
91
cursebreaker-parser/src/xml_parsers/mod.rs
Normal file
91
cursebreaker-parser/src/xml_parsers/mod.rs
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
//! XML Parsers - Individual parsers for each game data type
|
||||||
|
//!
|
||||||
|
//! This module contains separate parsers for each type of game data:
|
||||||
|
//! - Items
|
||||||
|
//! - NPCs
|
||||||
|
//! - Quests
|
||||||
|
//! - Harvestables
|
||||||
|
//! - Loot tables
|
||||||
|
//! - Maps
|
||||||
|
//! - Fast travel locations
|
||||||
|
//! - Player houses
|
||||||
|
//! - Traits
|
||||||
|
//! - Shops
|
||||||
|
|
||||||
|
mod items;
|
||||||
|
mod npcs;
|
||||||
|
mod quests;
|
||||||
|
mod harvestables;
|
||||||
|
mod loot;
|
||||||
|
mod maps;
|
||||||
|
mod fast_travel;
|
||||||
|
mod player_houses;
|
||||||
|
mod traits;
|
||||||
|
mod shops;
|
||||||
|
|
||||||
|
use quick_xml::events::BytesStart;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
// Re-export all parsers
|
||||||
|
pub use items::parse_items_xml;
|
||||||
|
pub use npcs::parse_npcs_xml;
|
||||||
|
pub use quests::parse_quests_xml;
|
||||||
|
pub use harvestables::parse_harvestables_xml;
|
||||||
|
pub use loot::parse_loot_xml;
|
||||||
|
pub use maps::parse_maps_xml;
|
||||||
|
pub use fast_travel::{
|
||||||
|
parse_fast_travel_locations_xml,
|
||||||
|
parse_fast_travel_canoe_xml,
|
||||||
|
parse_fast_travel_portals_xml,
|
||||||
|
};
|
||||||
|
pub use player_houses::parse_player_houses_xml;
|
||||||
|
pub use traits::parse_traits_xml;
|
||||||
|
pub use shops::parse_shops_xml;
|
||||||
|
|
||||||
|
/// Errors that can occur during XML parsing
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum XmlParseError {
|
||||||
|
#[error("XML parsing error: {0}")]
|
||||||
|
XmlError(#[from] quick_xml::Error),
|
||||||
|
|
||||||
|
#[error("IO error: {0}")]
|
||||||
|
IoError(#[from] std::io::Error),
|
||||||
|
|
||||||
|
#[error("Attribute error: {0}")]
|
||||||
|
AttrError(#[from] quick_xml::events::attributes::AttrError),
|
||||||
|
|
||||||
|
#[error("Missing required attribute: {0}")]
|
||||||
|
MissingAttribute(String),
|
||||||
|
|
||||||
|
#[error("Invalid attribute value: {0}")]
|
||||||
|
InvalidAttribute(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse all attributes from an XML element into a HashMap
|
||||||
|
pub(crate) fn parse_attributes(element: &BytesStart) -> Result<HashMap<String, String>, XmlParseError> {
|
||||||
|
let mut attrs = HashMap::new();
|
||||||
|
|
||||||
|
for attr in element.attributes() {
|
||||||
|
let attr = attr?;
|
||||||
|
let key = String::from_utf8_lossy(attr.key.as_ref()).to_string();
|
||||||
|
let value = attr.unescape_value()?.to_string();
|
||||||
|
attrs.insert(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(attrs)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse health range string like "3-5" or "3" into (min, max)
|
||||||
|
pub(crate) fn parse_health_range(health_str: &str) -> (i32, i32) {
|
||||||
|
if let Some(dash_pos) = health_str.find('-') {
|
||||||
|
let min_str = &health_str[..dash_pos];
|
||||||
|
let max_str = &health_str[dash_pos + 1..];
|
||||||
|
let min = min_str.trim().parse().unwrap_or(0);
|
||||||
|
let max = max_str.trim().parse().unwrap_or(0);
|
||||||
|
(min, max)
|
||||||
|
} else {
|
||||||
|
let val = health_str.trim().parse().unwrap_or(0);
|
||||||
|
(val, val)
|
||||||
|
}
|
||||||
|
}
|
||||||
243
cursebreaker-parser/src/xml_parsers/npcs.rs
Normal file
243
cursebreaker-parser/src/xml_parsers/npcs.rs
Normal file
@@ -0,0 +1,243 @@
|
|||||||
|
//! NPC XML Parser
|
||||||
|
|
||||||
|
use crate::types::{Npc, NpcStat, NpcLevel, RightClick, BarkGroup, QuestMarker, NpcAnimationSet};
|
||||||
|
use super::{parse_attributes, XmlParseError};
|
||||||
|
use quick_xml::events::Event;
|
||||||
|
use quick_xml::reader::Reader;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::BufReader;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
pub fn parse_npcs_xml<P: AsRef<Path>>(path: P) -> Result<Vec<Npc>, XmlParseError> {
|
||||||
|
let file = File::open(path)?;
|
||||||
|
let buf_reader = BufReader::new(file);
|
||||||
|
let mut reader = Reader::from_reader(buf_reader);
|
||||||
|
reader.config_mut().trim_text(true);
|
||||||
|
|
||||||
|
let mut npcs = Vec::new();
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
let mut current_npc: Option<Npc> = None;
|
||||||
|
let mut current_bark_group: Option<BarkGroup> = None;
|
||||||
|
let mut in_exitdialoguebarks = false;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
match reader.read_event_into(&mut buf) {
|
||||||
|
Ok(Event::Start(ref e)) | Ok(Event::Empty(ref e)) => {
|
||||||
|
match e.name().as_ref() {
|
||||||
|
b"npc" => {
|
||||||
|
let attrs = parse_attributes(e)?;
|
||||||
|
let id = attrs.get("id")
|
||||||
|
.ok_or_else(|| XmlParseError::MissingAttribute("id".to_string()))?
|
||||||
|
.parse::<i32>()
|
||||||
|
.map_err(|_| XmlParseError::InvalidAttribute("id".to_string()))?;
|
||||||
|
|
||||||
|
let name = attrs.get("name").cloned().unwrap_or_default();
|
||||||
|
let mut npc = Npc::new(id, name);
|
||||||
|
|
||||||
|
// Parse all optional attributes
|
||||||
|
if let Some(v) = attrs.get("tags") { npc.tags = Some(v.clone()); }
|
||||||
|
if let Some(v) = attrs.get("level") { npc.level = v.parse().ok(); }
|
||||||
|
if let Some(v) = attrs.get("description") { npc.description = Some(v.clone()); }
|
||||||
|
if let Some(v) = attrs.get("comment") { npc.comment = Some(v.clone()); }
|
||||||
|
if let Some(v) = attrs.get("model") { npc.model = Some(v.clone()); }
|
||||||
|
if let Some(v) = attrs.get("canfight") { npc.canfight = v.parse().ok(); }
|
||||||
|
if let Some(v) = attrs.get("aggressive") { npc.aggressive = v.parse().ok(); }
|
||||||
|
if let Some(v) = attrs.get("team") { npc.team = v.parse().ok(); }
|
||||||
|
if let Some(v) = attrs.get("aggrodistance") { npc.aggrodistance = v.parse().ok(); }
|
||||||
|
if let Some(v) = attrs.get("respawntime") { npc.respawntime = v.parse().ok(); }
|
||||||
|
if let Some(v) = attrs.get("health") { npc.health = v.parse().ok(); }
|
||||||
|
if let Some(v) = attrs.get("mana") { npc.mana = v.parse().ok(); }
|
||||||
|
if let Some(v) = attrs.get("accuracy") { npc.accuracy = v.parse().ok(); }
|
||||||
|
if let Some(v) = attrs.get("damagetype") { npc.damagetype = v.parse().ok(); }
|
||||||
|
if let Some(v) = attrs.get("damageblock") { npc.damageblock = v.parse().ok(); }
|
||||||
|
if let Some(v) = attrs.get("ability") { npc.ability = v.parse().ok(); }
|
||||||
|
if let Some(v) = attrs.get("attackdistance") { npc.attackdistance = v.parse().ok(); }
|
||||||
|
if let Some(v) = attrs.get("attackspeed") { npc.attackspeed = v.parse().ok(); }
|
||||||
|
if let Some(v) = attrs.get("attackdelay") { npc.attackdelay = v.parse().ok(); }
|
||||||
|
if let Some(v) = attrs.get("gfxattack") { npc.gfxattack = Some(v.clone()); }
|
||||||
|
if let Some(v) = attrs.get("projectile") { npc.projectile = v.parse().ok(); }
|
||||||
|
if let Some(v) = attrs.get("projectilerate") { npc.projectilerate = v.parse().ok(); }
|
||||||
|
if let Some(v) = attrs.get("projectileendgfx") { npc.projectileendgfx = Some(v.clone()); }
|
||||||
|
if let Some(v) = attrs.get("projectileattackdistance") { npc.projectileattackdistance = v.parse().ok(); }
|
||||||
|
if let Some(v) = attrs.get("movementspeed") { npc.movementspeed = v.parse().ok(); }
|
||||||
|
if let Some(v) = attrs.get("walkspeed") { npc.walkspeed = v.parse().ok(); }
|
||||||
|
if let Some(v) = attrs.get("wandering") { npc.wandering = v.parse().ok(); }
|
||||||
|
if let Some(v) = attrs.get("wanderingdistance") { npc.wanderingdistance = v.parse().ok(); }
|
||||||
|
if let Some(v) = attrs.get("aibehaviour") { npc.aibehaviour = v.parse().ok(); }
|
||||||
|
if let Some(v) = attrs.get("nobestiary") { npc.nobestiary = v.parse().ok(); }
|
||||||
|
if let Some(v) = attrs.get("interactable") { npc.interactable = v.parse().ok(); }
|
||||||
|
if let Some(v) = attrs.get("interactdistance") { npc.interactdistance = v.parse().ok(); }
|
||||||
|
if let Some(v) = attrs.get("dontrotateoninteract") { npc.dontrotateoninteract = v.parse().ok(); }
|
||||||
|
if let Some(v) = attrs.get("shop") { npc.shop = v.parse().ok(); }
|
||||||
|
if let Some(v) = attrs.get("sfxattack") { npc.sfxattack = Some(v.clone()); }
|
||||||
|
if let Some(v) = attrs.get("sfxdeath") { npc.sfxdeath = Some(v.clone()); }
|
||||||
|
if let Some(v) = attrs.get("sfxtakehit") { npc.sfxtakehit = Some(v.clone()); }
|
||||||
|
if let Some(v) = attrs.get("sfxidle") { npc.sfxidle = Some(v.clone()); }
|
||||||
|
if let Some(v) = attrs.get("idlesoundtext") { npc.idlesoundtext = Some(v.clone()); }
|
||||||
|
if let Some(v) = attrs.get("anim_attack") { npc.anim_attack = Some(v.clone()); }
|
||||||
|
if let Some(v) = attrs.get("anim_death") { npc.anim_death = Some(v.clone()); }
|
||||||
|
if let Some(v) = attrs.get("anim_idle") { npc.anim_idle = Some(v.clone()); }
|
||||||
|
if let Some(v) = attrs.get("anim_run") { npc.anim_run = Some(v.clone()); }
|
||||||
|
if let Some(v) = attrs.get("anim_walk") { npc.anim_walk = Some(v.clone()); }
|
||||||
|
if let Some(v) = attrs.get("anim_takehit") { npc.anim_takehit = Some(v.clone()); }
|
||||||
|
if let Some(v) = attrs.get("startanim") { npc.startanim = Some(v.clone()); }
|
||||||
|
|
||||||
|
current_npc = Some(npc);
|
||||||
|
}
|
||||||
|
b"stat" if current_npc.is_some() => {
|
||||||
|
if let Some(ref mut npc) = current_npc {
|
||||||
|
let attrs = parse_attributes(e)?;
|
||||||
|
let stat = parse_npc_stat(&attrs);
|
||||||
|
npc.stats.push(stat);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b"level" if current_npc.is_some() => {
|
||||||
|
if let Some(ref mut npc) = current_npc {
|
||||||
|
let attrs = parse_attributes(e)?;
|
||||||
|
let level = parse_npc_level(&attrs);
|
||||||
|
npc.levels.push(level);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b"rightclick" if current_npc.is_some() => {
|
||||||
|
if let Some(ref mut npc) = current_npc {
|
||||||
|
let attrs = parse_attributes(e)?;
|
||||||
|
if let Some(option) = attrs.get("option") {
|
||||||
|
npc.rightclick = Some(RightClick { option: option.clone() });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b"questmarker" if current_npc.is_some() => {
|
||||||
|
if let Some(ref mut npc) = current_npc {
|
||||||
|
let attrs = parse_attributes(e)?;
|
||||||
|
if let (Some(id), Some(phase)) = (attrs.get("id"), attrs.get("phase")) {
|
||||||
|
if let (Ok(id), Ok(phase)) = (id.parse::<i32>(), phase.parse::<i32>()) {
|
||||||
|
npc.questmarkers.push(QuestMarker {
|
||||||
|
id,
|
||||||
|
phase,
|
||||||
|
checks: attrs.get("checks").cloned(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b"barks" if current_npc.is_some() => {
|
||||||
|
let attrs = parse_attributes(e)?;
|
||||||
|
current_bark_group = Some(BarkGroup {
|
||||||
|
cooldown: attrs.get("cooldown").and_then(|v| v.parse().ok()),
|
||||||
|
rate: attrs.get("rate").and_then(|v| v.parse().ok()),
|
||||||
|
range: attrs.get("range").and_then(|v| v.parse().ok()),
|
||||||
|
checks: attrs.get("checks").cloned(),
|
||||||
|
npcs: attrs.get("npcs").cloned(),
|
||||||
|
barks: Vec::new(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
b"exitdialoguebarks" if current_npc.is_some() => {
|
||||||
|
in_exitdialoguebarks = true;
|
||||||
|
current_bark_group = Some(BarkGroup {
|
||||||
|
cooldown: None,
|
||||||
|
rate: None,
|
||||||
|
range: None,
|
||||||
|
checks: None,
|
||||||
|
npcs: None,
|
||||||
|
barks: Vec::new(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
b"anim" if current_npc.is_some() => {
|
||||||
|
if let Some(ref mut npc) = current_npc {
|
||||||
|
let attrs = parse_attributes(e)?;
|
||||||
|
npc.animations = Some(NpcAnimationSet {
|
||||||
|
idle: attrs.get("idle").cloned(),
|
||||||
|
walk: attrs.get("walk").cloned(),
|
||||||
|
run: attrs.get("run").cloned(),
|
||||||
|
attack: attrs.get("attack").cloned(),
|
||||||
|
death: attrs.get("death").cloned(),
|
||||||
|
talk: attrs.get("talk").cloned(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Event::Text(e)) => {
|
||||||
|
// Handle bark text content
|
||||||
|
if current_bark_group.is_some() {
|
||||||
|
let text = e.unescape()?.to_string().trim().to_string();
|
||||||
|
if !text.is_empty() {
|
||||||
|
// Text will be added to bark when we hit the end tag
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Event::End(ref e)) => {
|
||||||
|
match e.name().as_ref() {
|
||||||
|
b"npc" => {
|
||||||
|
if let Some(npc) = current_npc.take() {
|
||||||
|
npcs.push(npc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b"barks" => {
|
||||||
|
if let Some(bark_group) = current_bark_group.take() {
|
||||||
|
if let Some(ref mut npc) = current_npc {
|
||||||
|
if in_exitdialoguebarks {
|
||||||
|
npc.exitdialoguebarks.push(bark_group);
|
||||||
|
} else {
|
||||||
|
npc.barks.push(bark_group);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b"exitdialoguebarks" => {
|
||||||
|
in_exitdialoguebarks = false;
|
||||||
|
if let Some(bark_group) = current_bark_group.take() {
|
||||||
|
if let Some(ref mut npc) = current_npc {
|
||||||
|
npc.exitdialoguebarks.push(bark_group);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Event::Eof) => break,
|
||||||
|
Err(e) => return Err(XmlParseError::XmlError(e)),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
buf.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(npcs)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_npc_stat(attrs: &HashMap<String, String>) -> NpcStat {
|
||||||
|
NpcStat {
|
||||||
|
damagephysical: attrs.get("damagephysical").and_then(|v| v.parse().ok()),
|
||||||
|
damagemagical: attrs.get("damagemagical").and_then(|v| v.parse().ok()),
|
||||||
|
damageranged: attrs.get("damageranged").and_then(|v| v.parse().ok()),
|
||||||
|
accuracyphysical: attrs.get("accuracyphysical").and_then(|v| v.parse().ok()),
|
||||||
|
accuracymagical: attrs.get("accuracymagical").and_then(|v| v.parse().ok()),
|
||||||
|
accuracyranged: attrs.get("accuracyranged").and_then(|v| v.parse().ok()),
|
||||||
|
resistancephysical: attrs.get("resistancephysical").or_else(|| attrs.get("resistancePhysical")).and_then(|v| v.parse().ok()),
|
||||||
|
resistancemagical: attrs.get("resistancemagical").or_else(|| attrs.get("resistanceMagical")).and_then(|v| v.parse().ok()),
|
||||||
|
resistanceranged: attrs.get("resistanceranged").or_else(|| attrs.get("resistanceRanged")).and_then(|v| v.parse().ok()),
|
||||||
|
health: attrs.get("health").and_then(|v| v.parse().ok()),
|
||||||
|
mana: attrs.get("mana").and_then(|v| v.parse().ok()),
|
||||||
|
manaregen: attrs.get("manaregen").and_then(|v| v.parse().ok()),
|
||||||
|
healing: attrs.get("healing").and_then(|v| v.parse().ok()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_npc_level(attrs: &HashMap<String, String>) -> NpcLevel {
|
||||||
|
NpcLevel {
|
||||||
|
swordsmanship: attrs.get("swordsmanship").and_then(|v| v.parse().ok()),
|
||||||
|
archery: attrs.get("archery").and_then(|v| v.parse().ok()),
|
||||||
|
magic: attrs.get("magic").and_then(|v| v.parse().ok()),
|
||||||
|
defence: attrs.get("defence").and_then(|v| v.parse().ok()),
|
||||||
|
mining: attrs.get("mining").and_then(|v| v.parse().ok()),
|
||||||
|
woodcutting: attrs.get("woodcutting").and_then(|v| v.parse().ok()),
|
||||||
|
fishing: attrs.get("fishing").and_then(|v| v.parse().ok()),
|
||||||
|
cooking: attrs.get("cooking").and_then(|v| v.parse().ok()),
|
||||||
|
carpentry: attrs.get("carpentry").and_then(|v| v.parse().ok()),
|
||||||
|
blacksmithy: attrs.get("blacksmithy").and_then(|v| v.parse().ok()),
|
||||||
|
tailoring: attrs.get("tailoring").and_then(|v| v.parse().ok()),
|
||||||
|
alchemy: attrs.get("alchemy").and_then(|v| v.parse().ok()),
|
||||||
|
}
|
||||||
|
}
|
||||||
93
cursebreaker-parser/src/xml_parsers/player_houses.rs
Normal file
93
cursebreaker-parser/src/xml_parsers/player_houses.rs
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
//! Player House XML Parser
|
||||||
|
|
||||||
|
use crate::types::PlayerHouse;
|
||||||
|
use super::{parse_attributes, XmlParseError};
|
||||||
|
use quick_xml::events::Event;
|
||||||
|
use quick_xml::reader::Reader;
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::BufReader;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
/// Parse position string "x,y,z" and return (x, z), discarding y
|
||||||
|
fn parse_position(pos_str: &str) -> Result<(f32, f32), XmlParseError> {
|
||||||
|
let parts: Vec<&str> = pos_str.split(',').collect();
|
||||||
|
if parts.len() != 3 {
|
||||||
|
return Err(XmlParseError::InvalidAttribute("pos".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let x = parts[0]
|
||||||
|
.parse::<f32>()
|
||||||
|
.map_err(|_| XmlParseError::InvalidAttribute("pos".to_string()))?;
|
||||||
|
// Skip y (parts[1])
|
||||||
|
let z = parts[2]
|
||||||
|
.parse::<f32>()
|
||||||
|
.map_err(|_| XmlParseError::InvalidAttribute("pos".to_string()))?;
|
||||||
|
|
||||||
|
Ok((x, z))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_player_houses_xml<P: AsRef<Path>>(path: P) -> Result<Vec<PlayerHouse>, XmlParseError> {
|
||||||
|
let file = File::open(path)?;
|
||||||
|
let buf_reader = BufReader::new(file);
|
||||||
|
let mut reader = Reader::from_reader(buf_reader);
|
||||||
|
reader.config_mut().trim_text(true);
|
||||||
|
|
||||||
|
let mut houses = Vec::new();
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
match reader.read_event_into(&mut buf) {
|
||||||
|
Ok(Event::Start(ref e)) | Ok(Event::Empty(ref e)) => {
|
||||||
|
match e.name().as_ref() {
|
||||||
|
b"playerhouse" => {
|
||||||
|
let attrs = parse_attributes(e)?;
|
||||||
|
|
||||||
|
// Skip hidden houses
|
||||||
|
if attrs.contains_key("hidden") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get required attributes
|
||||||
|
let id = attrs
|
||||||
|
.get("id")
|
||||||
|
.ok_or_else(|| XmlParseError::MissingAttribute("id".to_string()))?
|
||||||
|
.parse::<i32>()
|
||||||
|
.map_err(|_| XmlParseError::InvalidAttribute("id".to_string()))?;
|
||||||
|
|
||||||
|
let name = attrs
|
||||||
|
.get("name")
|
||||||
|
.ok_or_else(|| XmlParseError::MissingAttribute("name".to_string()))?
|
||||||
|
.clone();
|
||||||
|
|
||||||
|
let description = attrs
|
||||||
|
.get("description")
|
||||||
|
.unwrap_or(&String::new())
|
||||||
|
.clone();
|
||||||
|
|
||||||
|
let pos_str = attrs
|
||||||
|
.get("pos")
|
||||||
|
.ok_or_else(|| XmlParseError::MissingAttribute("pos".to_string()))?;
|
||||||
|
|
||||||
|
let (pos_x, pos_z) = parse_position(pos_str)?;
|
||||||
|
|
||||||
|
let price = attrs
|
||||||
|
.get("price")
|
||||||
|
.ok_or_else(|| XmlParseError::MissingAttribute("price".to_string()))?
|
||||||
|
.parse::<i32>()
|
||||||
|
.map_err(|_| XmlParseError::InvalidAttribute("price".to_string()))?;
|
||||||
|
|
||||||
|
let house = PlayerHouse::new(id, name, description, pos_x, pos_z, price);
|
||||||
|
houses.push(house);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Event::Eof) => break,
|
||||||
|
Err(e) => return Err(XmlParseError::XmlError(e)),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
buf.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(houses)
|
||||||
|
}
|
||||||
106
cursebreaker-parser/src/xml_parsers/quests.rs
Normal file
106
cursebreaker-parser/src/xml_parsers/quests.rs
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
//! Quest XML Parser
|
||||||
|
|
||||||
|
use crate::types::{Quest, QuestPhase, QuestReward};
|
||||||
|
use super::{parse_attributes, XmlParseError};
|
||||||
|
use quick_xml::events::Event;
|
||||||
|
use quick_xml::reader::Reader;
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::BufReader;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
pub fn parse_quests_xml<P: AsRef<Path>>(path: P) -> Result<Vec<Quest>, XmlParseError> {
|
||||||
|
let file = File::open(path)?;
|
||||||
|
let buf_reader = BufReader::new(file);
|
||||||
|
let mut reader = Reader::from_reader(buf_reader);
|
||||||
|
reader.config_mut().trim_text(true);
|
||||||
|
|
||||||
|
let mut quests = Vec::new();
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
let mut current_quest: Option<Quest> = None;
|
||||||
|
let mut in_rewards = false;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
match reader.read_event_into(&mut buf) {
|
||||||
|
Ok(Event::Start(ref e)) | Ok(Event::Empty(ref e)) => {
|
||||||
|
match e.name().as_ref() {
|
||||||
|
b"quest" => {
|
||||||
|
let attrs = parse_attributes(e)?;
|
||||||
|
let id = attrs.get("id")
|
||||||
|
.ok_or_else(|| XmlParseError::MissingAttribute("id".to_string()))?
|
||||||
|
.parse::<i32>()
|
||||||
|
.map_err(|_| XmlParseError::InvalidAttribute("id".to_string()))?;
|
||||||
|
|
||||||
|
let name = attrs.get("name")
|
||||||
|
.ok_or_else(|| XmlParseError::MissingAttribute("name".to_string()))?
|
||||||
|
.clone();
|
||||||
|
|
||||||
|
let mut quest = Quest::new(id, name);
|
||||||
|
|
||||||
|
// Parse optional attributes
|
||||||
|
if let Some(v) = attrs.get("mainquest") { quest.mainquest = v.parse().ok(); }
|
||||||
|
if let Some(v) = attrs.get("hidden") { quest.hidden = v.parse().ok(); }
|
||||||
|
if let Some(v) = attrs.get("questdescription") { quest.questdescription = Some(v.clone()); }
|
||||||
|
if let Some(v) = attrs.get("completiontext") { quest.completiontext = Some(v.clone()); }
|
||||||
|
if let Some(v) = attrs.get("dontshowcompletionscreen") { quest.dontshowcompletionscreen = v.parse().ok(); }
|
||||||
|
if let Some(v) = attrs.get("comment") { quest.comment = Some(v.clone()); }
|
||||||
|
|
||||||
|
current_quest = Some(quest);
|
||||||
|
}
|
||||||
|
b"phase" if current_quest.is_some() => {
|
||||||
|
if let Some(ref mut quest) = current_quest {
|
||||||
|
let attrs = parse_attributes(e)?;
|
||||||
|
if let Some(id) = attrs.get("id") {
|
||||||
|
if let Ok(id) = id.parse::<i32>() {
|
||||||
|
quest.phases.push(QuestPhase {
|
||||||
|
id,
|
||||||
|
trackerdescription: attrs.get("trackerdescription").cloned(),
|
||||||
|
description: attrs.get("description").cloned(),
|
||||||
|
helperarrownpc: attrs.get("helperarrownpc").cloned(),
|
||||||
|
helperarrowpos: attrs.get("helperarrowpos").cloned(),
|
||||||
|
checks: attrs.get("checks").cloned(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b"rewards" if current_quest.is_some() => {
|
||||||
|
in_rewards = true;
|
||||||
|
}
|
||||||
|
b"reward" if current_quest.is_some() && in_rewards => {
|
||||||
|
if let Some(ref mut quest) = current_quest {
|
||||||
|
let attrs = parse_attributes(e)?;
|
||||||
|
quest.rewards.push(QuestReward {
|
||||||
|
item: attrs.get("item").and_then(|v| v.parse().ok()),
|
||||||
|
skill: attrs.get("skill").cloned(),
|
||||||
|
amount: attrs.get("amount").and_then(|v| v.parse().ok()),
|
||||||
|
xp: attrs.get("xp").and_then(|v| v.parse().ok()),
|
||||||
|
checks: attrs.get("checks").cloned(),
|
||||||
|
comment: attrs.get("comment").cloned(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Event::End(ref e)) => {
|
||||||
|
match e.name().as_ref() {
|
||||||
|
b"quest" => {
|
||||||
|
if let Some(quest) = current_quest.take() {
|
||||||
|
quests.push(quest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b"rewards" => {
|
||||||
|
in_rewards = false;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Event::Eof) => break,
|
||||||
|
Err(e) => return Err(XmlParseError::XmlError(e)),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
buf.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(quests)
|
||||||
|
}
|
||||||
105
cursebreaker-parser/src/xml_parsers/shops.rs
Normal file
105
cursebreaker-parser/src/xml_parsers/shops.rs
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
//! Shop XML Parser
|
||||||
|
|
||||||
|
use crate::types::{Shop, ShopItem};
|
||||||
|
use super::{parse_attributes, XmlParseError};
|
||||||
|
use quick_xml::events::Event;
|
||||||
|
use quick_xml::reader::Reader;
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::BufReader;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
pub fn parse_shops_xml<P: AsRef<Path>>(path: P) -> Result<Vec<Shop>, XmlParseError> {
|
||||||
|
let file = File::open(path)?;
|
||||||
|
let buf_reader = BufReader::new(file);
|
||||||
|
let mut reader = Reader::from_reader(buf_reader);
|
||||||
|
reader.config_mut().trim_text(true);
|
||||||
|
|
||||||
|
let mut shops = Vec::new();
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
let mut current_shop: Option<Shop> = None;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
match reader.read_event_into(&mut buf) {
|
||||||
|
Ok(Event::Start(ref e)) | Ok(Event::Empty(ref e)) => {
|
||||||
|
match e.name().as_ref() {
|
||||||
|
b"shop" => {
|
||||||
|
let attrs = parse_attributes(e)?;
|
||||||
|
|
||||||
|
// Get required attributes
|
||||||
|
let shop_id = attrs
|
||||||
|
.get("shopid")
|
||||||
|
.ok_or_else(|| XmlParseError::MissingAttribute("shopid".to_string()))?
|
||||||
|
.parse::<i32>()
|
||||||
|
.map_err(|_| XmlParseError::InvalidAttribute("shopid".to_string()))?;
|
||||||
|
|
||||||
|
let name = attrs
|
||||||
|
.get("name")
|
||||||
|
.unwrap_or(&String::new())
|
||||||
|
.clone();
|
||||||
|
|
||||||
|
let mut shop = Shop::new(shop_id, name);
|
||||||
|
|
||||||
|
// Parse optional attributes
|
||||||
|
if attrs.get("isgeneralstore").is_some() {
|
||||||
|
shop.is_general_store = true;
|
||||||
|
}
|
||||||
|
if let Some(v) = attrs.get("comment") {
|
||||||
|
shop.comment = Some(v.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
current_shop = Some(shop);
|
||||||
|
}
|
||||||
|
b"item" if current_shop.is_some() => {
|
||||||
|
if let Some(ref mut shop) = current_shop {
|
||||||
|
let attrs = parse_attributes(e)?;
|
||||||
|
|
||||||
|
// Get item ID (can be numeric or string)
|
||||||
|
if let Some(item_id) = attrs.get("id") {
|
||||||
|
let mut item = ShopItem::new(item_id.clone());
|
||||||
|
|
||||||
|
// Parse optional attributes
|
||||||
|
if let Some(v) = attrs.get("name") {
|
||||||
|
item.name = Some(v.clone());
|
||||||
|
}
|
||||||
|
if let Some(v) = attrs.get("price") {
|
||||||
|
item.price = v.parse().ok();
|
||||||
|
}
|
||||||
|
if let Some(v) = attrs.get("maxstock") {
|
||||||
|
item.max_stock = v.parse().ok();
|
||||||
|
}
|
||||||
|
if let Some(v) = attrs.get("restocktime") {
|
||||||
|
item.restock_time = v.parse().ok();
|
||||||
|
}
|
||||||
|
if let Some(v) = attrs.get("buyprice") {
|
||||||
|
item.buy_price = v.parse().ok();
|
||||||
|
}
|
||||||
|
if let Some(v) = attrs.get("comment") {
|
||||||
|
item.comment = Some(v.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
shop.add_item(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Event::End(ref e)) => {
|
||||||
|
match e.name().as_ref() {
|
||||||
|
b"shop" => {
|
||||||
|
if let Some(shop) = current_shop.take() {
|
||||||
|
shops.push(shop);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Event::Eof) => break,
|
||||||
|
Err(e) => return Err(XmlParseError::XmlError(e)),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
buf.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(shops)
|
||||||
|
}
|
||||||
99
cursebreaker-parser/src/xml_parsers/traits.rs
Normal file
99
cursebreaker-parser/src/xml_parsers/traits.rs
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
//! Trait XML Parser
|
||||||
|
|
||||||
|
use crate::types::{Trait, TraitTrainer};
|
||||||
|
use super::{parse_attributes, XmlParseError};
|
||||||
|
use quick_xml::events::Event;
|
||||||
|
use quick_xml::reader::Reader;
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::BufReader;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
pub fn parse_traits_xml<P: AsRef<Path>>(path: P) -> Result<Vec<Trait>, XmlParseError> {
|
||||||
|
let file = File::open(path)?;
|
||||||
|
let buf_reader = BufReader::new(file);
|
||||||
|
let mut reader = Reader::from_reader(buf_reader);
|
||||||
|
reader.config_mut().trim_text(true);
|
||||||
|
|
||||||
|
let mut traits = Vec::new();
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
let mut current_trait: Option<Trait> = None;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
match reader.read_event_into(&mut buf) {
|
||||||
|
Ok(Event::Start(ref e)) | Ok(Event::Empty(ref e)) => {
|
||||||
|
match e.name().as_ref() {
|
||||||
|
b"trait" => {
|
||||||
|
let attrs = parse_attributes(e)?;
|
||||||
|
|
||||||
|
// Get required attributes
|
||||||
|
let id = attrs
|
||||||
|
.get("id")
|
||||||
|
.ok_or_else(|| XmlParseError::MissingAttribute("id".to_string()))?
|
||||||
|
.parse::<i32>()
|
||||||
|
.map_err(|_| XmlParseError::InvalidAttribute("id".to_string()))?;
|
||||||
|
|
||||||
|
let name = attrs
|
||||||
|
.get("name")
|
||||||
|
.unwrap_or(&String::new())
|
||||||
|
.clone();
|
||||||
|
|
||||||
|
let description = attrs
|
||||||
|
.get("description")
|
||||||
|
.unwrap_or(&String::new())
|
||||||
|
.clone();
|
||||||
|
|
||||||
|
let mut trait_obj = Trait::new(id, name, description);
|
||||||
|
|
||||||
|
// Parse optional attributes
|
||||||
|
if let Some(v) = attrs.get("learnability") {
|
||||||
|
trait_obj.learnability = v.parse().ok();
|
||||||
|
}
|
||||||
|
if let Some(v) = attrs.get("comment") {
|
||||||
|
trait_obj.comment = Some(v.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
current_trait = Some(trait_obj);
|
||||||
|
}
|
||||||
|
b"trainer" if current_trait.is_some() => {
|
||||||
|
if let Some(ref mut trait_obj) = current_trait {
|
||||||
|
let attrs = parse_attributes(e)?;
|
||||||
|
|
||||||
|
// Parse trainer requirements
|
||||||
|
if let (Some(skill), Some(level_str)) =
|
||||||
|
(attrs.get("skill"), attrs.get("level"))
|
||||||
|
{
|
||||||
|
if let Ok(level) = level_str.parse::<i32>() {
|
||||||
|
let mut trainer = TraitTrainer::new(skill.clone(), level);
|
||||||
|
|
||||||
|
// Parse optional tier icon
|
||||||
|
if let Some(v) = attrs.get("tiericon") {
|
||||||
|
trainer.tier_icon = v.parse().ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
trait_obj.trainer = Some(trainer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Event::End(ref e)) => {
|
||||||
|
match e.name().as_ref() {
|
||||||
|
b"trait" => {
|
||||||
|
if let Some(trait_obj) = current_trait.take() {
|
||||||
|
traits.push(trait_obj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Event::Eof) => break,
|
||||||
|
Err(e) => return Err(XmlParseError::XmlError(e)),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
buf.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(traits)
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
//! ECS world building from Unity documents
|
//! ECS world building from Unity documents
|
||||||
|
|
||||||
use log::{info, warn};
|
use log::{info, warn, debug};
|
||||||
|
|
||||||
use crate::model::RawDocument;
|
use crate::model::RawDocument;
|
||||||
use crate::parser::{GuidResolver, PrefabGuidResolver};
|
use crate::parser::{GuidResolver, PrefabGuidResolver};
|
||||||
@@ -150,7 +150,7 @@ pub fn build_world_from_documents(
|
|||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
// Soft failure - warn but continue
|
// Soft failure - warn but continue
|
||||||
warn!("Failed to instantiate prefab: {}", e);
|
debug!("Failed to instantiate prefab: {}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user