Compare commits
10 Commits
9b5eea54ca
...
9143f73317
| Author | SHA1 | Date | |
|---|---|---|---|
| 9143f73317 | |||
| 3012599263 | |||
| e53deb57bb | |||
| 50c28932e3 | |||
| 2df606cbdd | |||
| cca3469c1f | |||
| 7e399fe544 | |||
| cf4fb922f0 | |||
| fae8594bc1 | |||
| 4abca5928c |
@@ -39,7 +39,12 @@
|
||||
"Bash(DATABASE_URL=../cursebreaker-parser/cursebreaker.db cargo run:*)",
|
||||
"Bash(identify:*)",
|
||||
"Bash(diesel migration revert:*)",
|
||||
"Bash(xargs:*)"
|
||||
"Bash(xargs:*)",
|
||||
"Bash(ss:*)",
|
||||
"Bash(timeout 10 cargo run:*)",
|
||||
"Bash(timeout 60 cargo run:*)",
|
||||
"Bash(DATABASE_URL=../cursebreaker.db diesel print-schema:*)",
|
||||
"Bash(DATABASE_URL=../cursebreaker.db diesel database:*)"
|
||||
],
|
||||
"additionalDirectories": [
|
||||
"/home/connor/repos/CBAssets/"
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -19,3 +19,4 @@ target/
|
||||
|
||||
# Test data (cloned Unity projects for integration tests)
|
||||
test_data/
|
||||
cursebreaker.db
|
||||
|
||||
7
Cargo.lock
generated
7
Cargo.lock
generated
@@ -215,6 +215,12 @@ dependencies = [
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.22.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||
|
||||
[[package]]
|
||||
name = "bit_field"
|
||||
version = "0.10.3"
|
||||
@@ -363,6 +369,7 @@ name = "cursebreaker-map"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"base64",
|
||||
"cursebreaker-parser",
|
||||
"diesel",
|
||||
"dotenvy",
|
||||
|
||||
@@ -14,6 +14,7 @@ diesel = { version = "2.1", features = ["sqlite", "returning_clauses_for_sqlite_
|
||||
dotenvy = "0.15"
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = "0.3"
|
||||
base64 = "0.22"
|
||||
|
||||
[dependencies.cursebreaker-parser]
|
||||
path = "../cursebreaker-parser"
|
||||
|
||||
@@ -5,6 +5,7 @@ use axum::{
|
||||
routing::get,
|
||||
Json, Router,
|
||||
};
|
||||
use base64::Engine;
|
||||
use diesel::prelude::*;
|
||||
use serde::Serialize;
|
||||
use std::sync::Arc;
|
||||
@@ -27,6 +28,27 @@ struct MapBounds {
|
||||
max_y: i32,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ResourceResponse {
|
||||
resources: Vec<ResourceGroup>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ResourceGroup {
|
||||
item_id: i32,
|
||||
name: String,
|
||||
skill: String,
|
||||
level: i32,
|
||||
icon_base64: String,
|
||||
positions: Vec<Position>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct Position {
|
||||
x: f32,
|
||||
y: f32,
|
||||
}
|
||||
|
||||
// Establish database connection
|
||||
fn establish_connection(database_url: &str) -> Result<DbConnection, diesel::ConnectionError> {
|
||||
SqliteConnection::establish(database_url)
|
||||
@@ -111,6 +133,74 @@ async fn get_tile(
|
||||
}
|
||||
}
|
||||
|
||||
// Get all resources with icons from database
|
||||
async fn get_resources(
|
||||
State(state): State<Arc<AppState>>,
|
||||
) -> Result<Json<ResourceResponse>, StatusCode> {
|
||||
use cursebreaker_parser::schema::{harvestables, resource_icons, world_resources};
|
||||
|
||||
let mut conn = establish_connection(&state.database_url).map_err(|e| {
|
||||
tracing::error!("Database connection error: {}", e);
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
})?;
|
||||
|
||||
// Query with three-way join
|
||||
let results = world_resources::table
|
||||
.inner_join(
|
||||
resource_icons::table.on(world_resources::item_id.eq(resource_icons::item_id)),
|
||||
)
|
||||
.inner_join(harvestables::table.on(resource_icons::item_id.eq(harvestables::id)))
|
||||
.select((
|
||||
resource_icons::item_id,
|
||||
resource_icons::name,
|
||||
harvestables::skill,
|
||||
harvestables::level,
|
||||
resource_icons::icon_64,
|
||||
world_resources::pos_x,
|
||||
world_resources::pos_y,
|
||||
))
|
||||
.order_by((harvestables::skill, harvestables::level))
|
||||
.load::<(i32, String, String, i32, Vec<u8>, f32, f32)>(&mut conn)
|
||||
.map_err(|e| {
|
||||
tracing::error!("Error querying resources: {}", e);
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
})?;
|
||||
|
||||
// Group results by item_id
|
||||
use std::collections::HashMap;
|
||||
let mut grouped: HashMap<i32, ResourceGroup> = HashMap::new();
|
||||
|
||||
for (item_id, name, skill, level, icon_bytes, pos_x, pos_y) in results {
|
||||
let entry = grouped.entry(item_id).or_insert_with(|| {
|
||||
// Convert icon to base64 (only once per resource type)
|
||||
let icon_base64 = base64::engine::general_purpose::STANDARD.encode(&icon_bytes);
|
||||
|
||||
ResourceGroup {
|
||||
item_id,
|
||||
name,
|
||||
skill,
|
||||
level,
|
||||
icon_base64,
|
||||
positions: Vec::new(),
|
||||
}
|
||||
});
|
||||
|
||||
// Add position with multiplier applied
|
||||
entry.positions.push(Position {
|
||||
x: pos_x * 5.12,
|
||||
y: pos_y * 5.12,
|
||||
});
|
||||
}
|
||||
|
||||
// Convert to vec and sort by skill and level
|
||||
let mut resources: Vec<ResourceGroup> = grouped.into_values().collect();
|
||||
resources.sort_by(|a, b| a.skill.cmp(&b.skill).then(a.level.cmp(&b.level)));
|
||||
|
||||
info!("Returning {} resource types", resources.len());
|
||||
|
||||
Ok(Json(ResourceResponse { resources }))
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
// Initialize tracing
|
||||
@@ -128,6 +218,7 @@ async fn main() {
|
||||
let app = Router::new()
|
||||
.route("/api/bounds", get(get_bounds))
|
||||
.route("/api/tiles/:z/:x/:y", get(get_tile))
|
||||
.route("/api/resources", get(get_resources))
|
||||
.nest_service("/", ServeDir::new("static"))
|
||||
.layer(CorsLayer::permissive())
|
||||
.with_state(state);
|
||||
|
||||
@@ -10,7 +10,7 @@ const MapConfig = {
|
||||
// Leaflet zoom 1 → Database zoom 1 (2x2 merged)
|
||||
{ leafletZoom: -0.5, dbZoom: 1, mergeFactor: 2, label: "2x2 merged" },
|
||||
// Leaflet zoom 2+ → Database zoom 2 (original tiles)
|
||||
{ leafletZoom: 1.5, dbZoom: 2, mergeFactor: 1, label: "original" },
|
||||
{ leafletZoom: 1, dbZoom: 2, mergeFactor: 1, label: "original" },
|
||||
],
|
||||
|
||||
// Leaflet map settings
|
||||
@@ -23,6 +23,9 @@ const MapConfig = {
|
||||
// Debug mode - shows tile boundaries and coordinates
|
||||
debug: true,
|
||||
|
||||
// Resource icon configuration
|
||||
resourceIconSize: 48, // Icon size in pixels (configurable)
|
||||
|
||||
// Get zoom configuration for a specific Leaflet zoom level
|
||||
getZoomConfig(leafletZoom) {
|
||||
// Find the appropriate config for this zoom level
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
<div id="app">
|
||||
<!-- Sidebar -->
|
||||
<div id="sidebar" class="sidebar collapsed">
|
||||
<button id="toggle-sidebar" class="toggle-btn">☰</button>
|
||||
<div class="sidebar-content">
|
||||
<h2>Cursebreaker Map</h2>
|
||||
<div class="info-section">
|
||||
@@ -23,15 +22,13 @@
|
||||
</div>
|
||||
|
||||
<div class="filters-section">
|
||||
<h3>Filters</h3>
|
||||
<p class="coming-soon">Coming soon: Filter shops, resources, and more</p>
|
||||
|
||||
<!-- Placeholder for future filters -->
|
||||
<div class="filter-group" style="opacity: 0.5; pointer-events: none;">
|
||||
<label><input type="checkbox" checked disabled> Shops</label>
|
||||
<label><input type="checkbox" checked disabled> Resources</label>
|
||||
<label><input type="checkbox" checked disabled> Fast Travel</label>
|
||||
<label><input type="checkbox" checked disabled> Workbenches</label>
|
||||
<h3>Resources</h3>
|
||||
<div class="filter-controls">
|
||||
<button id="select-all-resources" class="filter-btn">Show All</button>
|
||||
<button id="deselect-all-resources" class="filter-btn">Hide All</button>
|
||||
</div>
|
||||
<div id="resource-filters" class="filter-group">
|
||||
<p class="loading-text">Loading resources...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -61,5 +58,6 @@
|
||||
|
||||
<!-- Custom JS -->
|
||||
<script src="map.js"></script>
|
||||
<script src="resources.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -86,10 +86,40 @@ async function initMap() {
|
||||
L.control.attribution({
|
||||
position: 'bottomright',
|
||||
prefix: false
|
||||
}).addHTML('The Black Grimoire: Cursebreaker').addTo(map);
|
||||
}).addAttribution('The Black Grimoire: Cursebreaker').addTo(map);
|
||||
|
||||
// Add sidebar toggle control
|
||||
const SidebarControl = L.Control.extend({
|
||||
options: {
|
||||
position: 'topleft'
|
||||
},
|
||||
|
||||
onAdd: function(map) {
|
||||
const container = L.DomUtil.create('div', 'leaflet-bar leaflet-control');
|
||||
const button = L.DomUtil.create('a', 'leaflet-control-sidebar', container);
|
||||
button.innerHTML = '☰';
|
||||
button.href = '#';
|
||||
button.title = 'Toggle Sidebar';
|
||||
|
||||
L.DomEvent.on(button, 'click', function(e) {
|
||||
L.DomEvent.preventDefault(e);
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
sidebar.classList.toggle('collapsed');
|
||||
});
|
||||
|
||||
return container;
|
||||
}
|
||||
});
|
||||
|
||||
map.addControl(new SidebarControl());
|
||||
|
||||
console.log('Map initialized successfully');
|
||||
|
||||
// Load resources asynchronously
|
||||
loadResources().catch(error => {
|
||||
console.error('Failed to load resources:', error);
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error initializing map:', error);
|
||||
document.getElementById('map-stats').innerHTML =
|
||||
@@ -236,11 +266,5 @@ function updateMapInfo(bounds) {
|
||||
`;
|
||||
}
|
||||
|
||||
// Toggle sidebar
|
||||
document.getElementById('toggle-sidebar').addEventListener('click', function() {
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
sidebar.classList.toggle('collapsed');
|
||||
});
|
||||
|
||||
// Initialize map when page loads
|
||||
window.addEventListener('DOMContentLoaded', initMap);
|
||||
|
||||
261
cursebreaker-map/static/resources.js
Normal file
261
cursebreaker-map/static/resources.js
Normal file
@@ -0,0 +1,261 @@
|
||||
// Resource management for Cursebreaker map
|
||||
let resourceLayerGroups = {}; // Map: resource name -> L.layerGroup
|
||||
let resourceIcons = {}; // Map: resource name -> L.icon
|
||||
let resourceData = {}; // Map: resource name -> resource metadata (skill, level, etc.)
|
||||
let filterState = {}; // Map: resource name -> boolean (visible)
|
||||
|
||||
// Load resources from API
|
||||
async function loadResources() {
|
||||
try {
|
||||
console.log('Loading resources from API...');
|
||||
const response = await fetch('/api/resources');
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log(`Received ${data.resources.length} resource types`);
|
||||
|
||||
// Create icons and layer groups for each resource
|
||||
for (const group of data.resources) {
|
||||
createResourceGroup(group);
|
||||
}
|
||||
|
||||
// Initialize filter UI
|
||||
initializeFilterUI();
|
||||
|
||||
// Restore filter state from localStorage
|
||||
restoreFilterState();
|
||||
|
||||
console.log(`Loaded ${data.resources.length} resource types successfully`);
|
||||
} catch (error) {
|
||||
console.error('Error loading resources:', error);
|
||||
const container = document.getElementById('resource-filters');
|
||||
if (container) {
|
||||
container.innerHTML = '<p style="color: #ff6b6b;">Failed to load resources. Check console for details.</p>';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create resource group with icon and markers
|
||||
function createResourceGroup(group) {
|
||||
const config = window.MapConfig;
|
||||
|
||||
// Create icon definition (cached per resource type)
|
||||
const iconUrl = `data:image/webp;base64,${group.icon_base64}`;
|
||||
const icon = L.icon({
|
||||
iconUrl: iconUrl,
|
||||
iconSize: [config.resourceIconSize, config.resourceIconSize],
|
||||
iconAnchor: [config.resourceIconSize / 2, config.resourceIconSize / 2],
|
||||
popupAnchor: [0, -(config.resourceIconSize / 2)],
|
||||
});
|
||||
|
||||
resourceIcons[group.name] = icon;
|
||||
|
||||
// Store metadata
|
||||
resourceData[group.name] = {
|
||||
item_id: group.item_id,
|
||||
skill: group.skill,
|
||||
level: group.level,
|
||||
};
|
||||
|
||||
// Create layer group for this resource type
|
||||
const layerGroup = L.layerGroup();
|
||||
|
||||
// Add markers for all positions
|
||||
for (const pos of group.positions) {
|
||||
const marker = L.marker([pos.y, pos.x], {
|
||||
icon: icon,
|
||||
title: group.name,
|
||||
});
|
||||
|
||||
// Add popup with resource details
|
||||
marker.bindPopup(
|
||||
`<strong>${group.name}</strong><br/>Position: (${pos.x.toFixed(1)}, ${pos.y.toFixed(1)})`
|
||||
);
|
||||
|
||||
marker.addTo(layerGroup);
|
||||
}
|
||||
|
||||
// Add to map (initially visible)
|
||||
layerGroup.addTo(map);
|
||||
resourceLayerGroups[group.name] = layerGroup;
|
||||
filterState[group.name] = true; // Initially visible
|
||||
}
|
||||
|
||||
// Initialize filter UI with skill grouping
|
||||
function initializeFilterUI() {
|
||||
const container = document.getElementById('resource-filters');
|
||||
if (!container) {
|
||||
console.error('resource-filters container not found');
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = ''; // Clear loading text
|
||||
|
||||
// Group resources by skill
|
||||
const skillGroups = {};
|
||||
for (const name in resourceLayerGroups) {
|
||||
const metadata = resourceData[name];
|
||||
if (!skillGroups[metadata.skill]) {
|
||||
skillGroups[metadata.skill] = [];
|
||||
}
|
||||
skillGroups[metadata.skill].push({
|
||||
name: name,
|
||||
level: metadata.level,
|
||||
});
|
||||
}
|
||||
|
||||
// Sort skills alphabetically
|
||||
const sortedSkills = Object.keys(skillGroups).sort();
|
||||
|
||||
// Create UI for each skill group
|
||||
for (const skill of sortedSkills) {
|
||||
const skillDiv = document.createElement('div');
|
||||
skillDiv.className = 'skill-group';
|
||||
|
||||
const header = document.createElement('div');
|
||||
header.className = 'skill-header';
|
||||
// Capitalize first letter of skill
|
||||
header.textContent = skill.charAt(0).toUpperCase() + skill.slice(1);
|
||||
skillDiv.appendChild(header);
|
||||
|
||||
// Resources are already sorted by level in backend, but sort again to be sure
|
||||
skillGroups[skill].sort((a, b) => a.level - b.level);
|
||||
|
||||
// Create checkbox for each resource
|
||||
for (const resource of skillGroups[skill]) {
|
||||
const label = createFilterLabel(resource.name);
|
||||
skillDiv.appendChild(label);
|
||||
}
|
||||
|
||||
container.appendChild(skillDiv);
|
||||
}
|
||||
|
||||
// Attach bulk filter handlers
|
||||
const selectAllBtn = document.getElementById('select-all-resources');
|
||||
const deselectAllBtn = document.getElementById('deselect-all-resources');
|
||||
|
||||
if (selectAllBtn) {
|
||||
selectAllBtn.addEventListener('click', () => {
|
||||
setAllFilters(true);
|
||||
});
|
||||
}
|
||||
|
||||
if (deselectAllBtn) {
|
||||
deselectAllBtn.addEventListener('click', () => {
|
||||
setAllFilters(false);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Create filter label with checkbox and icon
|
||||
function createFilterLabel(resourceName) {
|
||||
const label = document.createElement('label');
|
||||
label.className = 'filter-label';
|
||||
|
||||
const checkbox = document.createElement('input');
|
||||
checkbox.type = 'checkbox';
|
||||
checkbox.checked = filterState[resourceName];
|
||||
checkbox.dataset.resource = resourceName;
|
||||
checkbox.addEventListener('change', handleFilterChange);
|
||||
|
||||
const icon = document.createElement('img');
|
||||
icon.src = resourceIcons[resourceName].options.iconUrl;
|
||||
icon.className = 'filter-icon';
|
||||
icon.alt = resourceName;
|
||||
|
||||
const text = document.createElement('span');
|
||||
text.textContent = resourceName;
|
||||
|
||||
label.appendChild(checkbox);
|
||||
label.appendChild(icon);
|
||||
label.appendChild(text);
|
||||
|
||||
return label;
|
||||
}
|
||||
|
||||
// Handle filter checkbox change
|
||||
function handleFilterChange(event) {
|
||||
const resourceName = event.target.dataset.resource;
|
||||
const isVisible = event.target.checked;
|
||||
|
||||
filterState[resourceName] = isVisible;
|
||||
|
||||
// Show/hide layer group
|
||||
const layerGroup = resourceLayerGroups[resourceName];
|
||||
if (isVisible) {
|
||||
layerGroup.addTo(map);
|
||||
} else {
|
||||
map.removeLayer(layerGroup);
|
||||
}
|
||||
|
||||
// Persist state
|
||||
saveFilterState();
|
||||
}
|
||||
|
||||
// Set all filters to visible or hidden
|
||||
function setAllFilters(visible) {
|
||||
for (const name in filterState) {
|
||||
filterState[name] = visible;
|
||||
const layerGroup = resourceLayerGroups[name];
|
||||
|
||||
if (visible) {
|
||||
layerGroup.addTo(map);
|
||||
} else {
|
||||
map.removeLayer(layerGroup);
|
||||
}
|
||||
}
|
||||
|
||||
// Update checkboxes
|
||||
document.querySelectorAll('#resource-filters input[type="checkbox"]').forEach((cb) => {
|
||||
cb.checked = visible;
|
||||
});
|
||||
|
||||
saveFilterState();
|
||||
}
|
||||
|
||||
// Save filter state to localStorage
|
||||
function saveFilterState() {
|
||||
try {
|
||||
localStorage.setItem('cursebreaker_resource_filters', JSON.stringify(filterState));
|
||||
} catch (error) {
|
||||
console.warn('Failed to save filter state to localStorage:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Restore filter state from localStorage
|
||||
function restoreFilterState() {
|
||||
const saved = localStorage.getItem('cursebreaker_resource_filters');
|
||||
if (!saved) return;
|
||||
|
||||
try {
|
||||
const savedState = JSON.parse(saved);
|
||||
|
||||
for (const name in savedState) {
|
||||
if (resourceLayerGroups[name]) {
|
||||
filterState[name] = savedState[name];
|
||||
|
||||
const layerGroup = resourceLayerGroups[name];
|
||||
if (!savedState[name]) {
|
||||
map.removeLayer(layerGroup);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update checkboxes after UI is created
|
||||
setTimeout(() => {
|
||||
document.querySelectorAll('#resource-filters input[type="checkbox"]').forEach((cb) => {
|
||||
const name = cb.dataset.resource;
|
||||
if (filterState[name] !== undefined) {
|
||||
cb.checked = filterState[name];
|
||||
}
|
||||
});
|
||||
}, 100);
|
||||
|
||||
console.log('Restored filter state from localStorage');
|
||||
} catch (error) {
|
||||
console.warn('Failed to restore filter state:', error);
|
||||
}
|
||||
}
|
||||
@@ -32,24 +32,17 @@ body {
|
||||
margin-left: -320px;
|
||||
}
|
||||
|
||||
.toggle-btn {
|
||||
position: absolute;
|
||||
right: -40px;
|
||||
top: 10px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: #2a2a2a;
|
||||
border: none;
|
||||
border-radius: 0 5px 5px 0;
|
||||
color: #e0e0e0;
|
||||
font-size: 20px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
z-index: 1001;
|
||||
}
|
||||
|
||||
.toggle-btn:hover {
|
||||
background: #3a3a3a;
|
||||
/* Sidebar toggle control */
|
||||
.leaflet-control-sidebar {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 18px;
|
||||
line-height: 30px;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.sidebar-content {
|
||||
@@ -176,6 +169,96 @@ body {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Filter controls */
|
||||
.filter-controls {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.filter-btn {
|
||||
flex: 1;
|
||||
padding: 6px 12px;
|
||||
background: #3a3a3a;
|
||||
color: #e0e0e0;
|
||||
border: 1px solid #4a4a4a;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.filter-btn:hover {
|
||||
background: #4a4a4a;
|
||||
}
|
||||
|
||||
.filter-btn:active {
|
||||
background: #2a2a2a;
|
||||
}
|
||||
|
||||
/* Filter groups by skill */
|
||||
.skill-group {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.skill-header {
|
||||
color: #8b5cf6;
|
||||
font-size: 13px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 4px;
|
||||
padding: 4px 8px;
|
||||
background: rgba(139, 92, 246, 0.1);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
/* Filter items */
|
||||
.filter-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
padding: 4px 8px;
|
||||
margin-left: 8px;
|
||||
border-radius: 4px;
|
||||
transition: background 0.2s;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.filter-label:hover {
|
||||
background: #3a3a3a;
|
||||
}
|
||||
|
||||
.filter-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
image-rendering: pixelated;
|
||||
image-rendering: -moz-crisp-edges;
|
||||
image-rendering: crisp-edges;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
color: #a0a0a0;
|
||||
font-style: italic;
|
||||
font-size: 13px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
/* Dark theme popups */
|
||||
.leaflet-popup-content-wrapper {
|
||||
background: #2a2a2a;
|
||||
color: #e0e0e0;
|
||||
border: 1px solid #3a3a3a;
|
||||
}
|
||||
|
||||
.leaflet-popup-content {
|
||||
margin: 8px 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.leaflet-popup-tip {
|
||||
background: #2a2a2a;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.sidebar {
|
||||
|
||||
@@ -42,14 +42,19 @@ The project provides multiple binaries to handle different parsing tasks. This a
|
||||
cargo run --bin xml-parser
|
||||
```
|
||||
|
||||
2. **scene-parser** - Parses Unity scenes and extracts world resource locations
|
||||
2. **scene-parser** - Parses Unity scenes and extracts world objects
|
||||
- Slow execution (Unity project initialization)
|
||||
- Extracts InteractableResource components and their positions
|
||||
- Saves to `world_resources` table (harvestable_id and 2D coordinates)
|
||||
- Extracts multiple types of interactable components and their positions:
|
||||
- **InteractableResource**: Harvestable resources → `world_resources` table
|
||||
- **InteractableTeleporter**: Teleporters with source/destination positions → `world_teleporters` table
|
||||
- **InteractableWorkbench**: Workbenches with workbench ID → `world_workbenches` table
|
||||
- **LootSpawner**: Loot spawners with item, amount, respawn time → `world_loot` table
|
||||
- **MapIcon**: Map icons with type, size, text, etc. → `world_map_icons` table
|
||||
- **MapNameChanger**: Map name changers → `world_map_name_changers` table
|
||||
- Processes item icons for harvestables:
|
||||
- Looks up the first item drop for each harvestable from `harvestable_drops` table
|
||||
- Loads the icon from `Data/Textures/ItemIcons/{item_id}.png`
|
||||
- Applies white outline (1px) and resizes to 64x64
|
||||
- Applies white outline (4px) and resizes to 64x64
|
||||
- Converts to WebP and stores in `resource_icons` table
|
||||
- Run this when scene files change
|
||||
```bash
|
||||
@@ -343,6 +348,11 @@ The parser uses Diesel for database operations with SQLite. Database migrations
|
||||
- Harvestable resources and drop tables
|
||||
- World resource locations from Unity scenes
|
||||
- Resource icons for harvestables (64x64 WebP with white borders)
|
||||
- World teleporters with source/destination coordinates
|
||||
- World workbenches with workbench IDs
|
||||
- World loot spawners with item, amount, and respawn time
|
||||
- Map icons with type, size, text, and hover text
|
||||
- Map name changers with location and map name
|
||||
- Minimap tiles and metadata
|
||||
- Shop inventories and pricing
|
||||
- Player houses and locations
|
||||
|
||||
140
cursebreaker-parser/examples/verify_world_objects.rs
Normal file
140
cursebreaker-parser/examples/verify_world_objects.rs
Normal file
@@ -0,0 +1,140 @@
|
||||
//! Example: Query world objects from the database
|
||||
//!
|
||||
//! Run with: cargo run --example verify_world_objects
|
||||
|
||||
use diesel::prelude::*;
|
||||
use diesel::sqlite::SqliteConnection;
|
||||
use std::env;
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Connect to database
|
||||
let database_url = env::var("DATABASE_URL").unwrap_or_else(|_| "../cursebreaker.db".to_string());
|
||||
let mut conn = SqliteConnection::establish(&database_url)?;
|
||||
|
||||
// Query teleporters
|
||||
{
|
||||
use cursebreaker_parser::schema::world_teleporters::dsl::*;
|
||||
|
||||
#[derive(Queryable, Debug)]
|
||||
struct Teleporter {
|
||||
pos_x: f32,
|
||||
pos_y: f32,
|
||||
tp_x: Option<f32>,
|
||||
tp_y: Option<f32>,
|
||||
}
|
||||
|
||||
let results = world_teleporters.load::<Teleporter>(&mut conn)?;
|
||||
println!("=== World Teleporters ===");
|
||||
println!("Found {} teleporters\n", results.len());
|
||||
for tp in results {
|
||||
print!(" At ({:.2}, {:.2})", tp.pos_x, tp.pos_y);
|
||||
if let (Some(tx), Some(ty)) = (tp.tp_x, tp.tp_y) {
|
||||
println!(" -> teleports to ({:.2}, {:.2})", tx, ty);
|
||||
} else {
|
||||
println!(" -> no destination");
|
||||
}
|
||||
}
|
||||
println!();
|
||||
}
|
||||
|
||||
// Query workbenches
|
||||
{
|
||||
use cursebreaker_parser::schema::world_workbenches::dsl::*;
|
||||
|
||||
#[derive(Queryable, Debug)]
|
||||
struct Workbench {
|
||||
pos_x: f32,
|
||||
pos_y: f32,
|
||||
workbench_id: i32,
|
||||
}
|
||||
|
||||
let results = world_workbenches.load::<Workbench>(&mut conn)?;
|
||||
println!("=== World Workbenches ===");
|
||||
println!("Found {} workbenches\n", results.len());
|
||||
for wb in results {
|
||||
println!(" Workbench ID {} at ({:.2}, {:.2})", wb.workbench_id, wb.pos_x, wb.pos_y);
|
||||
}
|
||||
println!();
|
||||
}
|
||||
|
||||
// Query loot spawners
|
||||
{
|
||||
use cursebreaker_parser::schema::world_loot::dsl::*;
|
||||
|
||||
#[derive(Queryable, Debug)]
|
||||
struct Loot {
|
||||
pos_x: f32,
|
||||
pos_y: f32,
|
||||
item_id: i32,
|
||||
amount: i32,
|
||||
respawn_time: i32,
|
||||
visibility_checks: String,
|
||||
}
|
||||
|
||||
let results = world_loot.load::<Loot>(&mut conn)?;
|
||||
println!("=== World Loot ===");
|
||||
println!("Found {} loot spawners\n", results.len());
|
||||
for loot in results {
|
||||
println!(" Item {} x{} (respawn: {}s) at ({:.2}, {:.2})",
|
||||
loot.item_id, loot.amount, loot.respawn_time, loot.pos_x, loot.pos_y);
|
||||
if !loot.visibility_checks.is_empty() {
|
||||
println!(" Visibility checks: {}", loot.visibility_checks);
|
||||
}
|
||||
}
|
||||
println!();
|
||||
}
|
||||
|
||||
// Query map icons
|
||||
{
|
||||
use cursebreaker_parser::schema::world_map_icons::dsl::*;
|
||||
|
||||
#[derive(Queryable, Debug)]
|
||||
struct MapIcon {
|
||||
pos_x: f32,
|
||||
pos_y: f32,
|
||||
icon_type: i32,
|
||||
icon_size: i32,
|
||||
icon: String,
|
||||
text: String,
|
||||
font_size: i32,
|
||||
hover_text: String,
|
||||
}
|
||||
|
||||
let results = world_map_icons.load::<MapIcon>(&mut conn)?;
|
||||
println!("=== World Map Icons ===");
|
||||
println!("Found {} map icons\n", results.len());
|
||||
for map_icon in results {
|
||||
print!(" Type {} at ({:.2}, {:.2})", map_icon.icon_type, map_icon.pos_x, map_icon.pos_y);
|
||||
if !map_icon.text.is_empty() {
|
||||
print!(" - Text: \"{}\"", map_icon.text);
|
||||
}
|
||||
if !map_icon.hover_text.is_empty() {
|
||||
print!(" - Hover: \"{}\"", map_icon.hover_text);
|
||||
}
|
||||
println!();
|
||||
}
|
||||
println!();
|
||||
}
|
||||
|
||||
// Query map name changers
|
||||
{
|
||||
use cursebreaker_parser::schema::world_map_name_changers::dsl::*;
|
||||
|
||||
#[derive(Queryable, Debug)]
|
||||
struct MapNameChanger {
|
||||
pos_x: f32,
|
||||
pos_y: f32,
|
||||
map_name: String,
|
||||
}
|
||||
|
||||
let results = world_map_name_changers.load::<MapNameChanger>(&mut conn)?;
|
||||
println!("=== World Map Name Changers ===");
|
||||
println!("Found {} map name changers\n", results.len());
|
||||
for changer in results {
|
||||
println!(" \"{}\" at ({:.2}, {:.2})", changer.map_name, changer.pos_x, changer.pos_y);
|
||||
}
|
||||
println!();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
-- Drop world scene object tables
|
||||
DROP TABLE world_teleporters;
|
||||
DROP TABLE world_workbenches;
|
||||
DROP TABLE world_loot;
|
||||
DROP TABLE world_map_icons;
|
||||
DROP TABLE world_map_name_changers;
|
||||
@@ -0,0 +1,48 @@
|
||||
-- Create world_teleporters table
|
||||
CREATE TABLE world_teleporters (
|
||||
pos_x REAL NOT NULL,
|
||||
pos_y REAL NOT NULL,
|
||||
tp_x REAL,
|
||||
tp_y REAL,
|
||||
PRIMARY KEY (pos_x, pos_y)
|
||||
);
|
||||
|
||||
-- Create world_workbenches table
|
||||
CREATE TABLE world_workbenches (
|
||||
pos_x REAL NOT NULL,
|
||||
pos_y REAL NOT NULL,
|
||||
workbench_id INTEGER NOT NULL,
|
||||
PRIMARY KEY (pos_x, pos_y)
|
||||
);
|
||||
|
||||
-- Create world_loot table
|
||||
CREATE TABLE world_loot (
|
||||
pos_x REAL NOT NULL,
|
||||
pos_y REAL NOT NULL,
|
||||
item_id INTEGER NOT NULL,
|
||||
amount INTEGER NOT NULL,
|
||||
respawn_time INTEGER NOT NULL,
|
||||
visibility_checks TEXT NOT NULL DEFAULT '',
|
||||
PRIMARY KEY (pos_x, pos_y)
|
||||
);
|
||||
|
||||
-- Create world_map_icons table
|
||||
CREATE TABLE world_map_icons (
|
||||
pos_x REAL NOT NULL,
|
||||
pos_y REAL NOT NULL,
|
||||
icon_type INTEGER NOT NULL,
|
||||
icon_size INTEGER NOT NULL,
|
||||
icon TEXT NOT NULL,
|
||||
text TEXT NOT NULL DEFAULT '',
|
||||
font_size INTEGER NOT NULL,
|
||||
hover_text TEXT NOT NULL DEFAULT '',
|
||||
PRIMARY KEY (pos_x, pos_y)
|
||||
);
|
||||
|
||||
-- Create world_map_name_changers table
|
||||
CREATE TABLE world_map_name_changers (
|
||||
pos_x REAL NOT NULL,
|
||||
pos_y REAL NOT NULL,
|
||||
map_name TEXT NOT NULL,
|
||||
PRIMARY KEY (pos_x, pos_y)
|
||||
);
|
||||
@@ -20,11 +20,12 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
.unwrap();
|
||||
|
||||
info!("🎮 Cursebreaker - Image Parser");
|
||||
info!("Generates all zoom levels (0, 1, 2) with merged tiles\n");
|
||||
info!("Generates all zoom levels (0, 1, 2) with merged tiles");
|
||||
info!("⚠️ Will override existing database entries\n");
|
||||
|
||||
// Process minimap tiles
|
||||
info!("🗺️ Processing minimap tiles...");
|
||||
let database_url = env::var("DATABASE_URL").unwrap_or_else(|_| "../cursebreaker.db".to_string());
|
||||
let database_url = env::var("DATABASE_URL").unwrap_or_else(|_| "cursebreaker.db".to_string());
|
||||
let minimap_db = MinimapDatabase::new(database_url);
|
||||
|
||||
let cb_assets_path = env::var("CB_ASSETS_PATH")
|
||||
|
||||
@@ -8,7 +8,10 @@
|
||||
//! - Saving resource locations to the database
|
||||
//! - Processing and saving item icons for resources
|
||||
|
||||
use cursebreaker_parser::{InteractableResource, ImageProcessor, OutlineConfig};
|
||||
use cursebreaker_parser::{
|
||||
InteractableResource, InteractableTeleporter, InteractableWorkbench,
|
||||
LootSpawner, MapIcon, MapNameChanger, ImageProcessor, OutlineConfig
|
||||
};
|
||||
use unity_parser::{UnityProject, TypeFilter};
|
||||
use std::path::{Path, PathBuf};
|
||||
use unity_parser::log::DedupLogger;
|
||||
@@ -40,7 +43,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
info!(" • Custom MonoBehaviours: InteractableResource");
|
||||
let type_filter = TypeFilter::new(
|
||||
vec!["GameObject", "Transform", "PrefabInstance"],
|
||||
vec!["InteractableResource"]
|
||||
vec!["InteractableResource", "InteractableTeleporter", "InteractableWorkbench", "LootSpawner", "MapIcon", "MapNameChanger"]
|
||||
);
|
||||
|
||||
// Now parse the scene using the pre-built GUID resolvers with filtering
|
||||
@@ -104,6 +107,22 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// 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);
|
||||
@@ -141,7 +160,7 @@ fn process_item_icons(
|
||||
|
||||
// Create image processor with white outline
|
||||
let processor = ImageProcessor::default();
|
||||
let outline_config = OutlineConfig::white(1);
|
||||
let outline_config = OutlineConfig::white(4);
|
||||
|
||||
let mut processed_count = 0;
|
||||
let mut failed_count = 0;
|
||||
@@ -229,3 +248,204 @@ fn process_item_icons(
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Save teleporter data to database
|
||||
fn save_teleporters(
|
||||
conn: &mut SqliteConnection,
|
||||
scene: &unity_parser::UnityScene,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
use cursebreaker_parser::schema::world_teleporters;
|
||||
|
||||
// Clear existing teleporters
|
||||
diesel::delete(world_teleporters::table).execute(conn)?;
|
||||
|
||||
let mut count = 0;
|
||||
|
||||
// Query all teleporters
|
||||
scene.world
|
||||
.query_all::<(&InteractableTeleporter, &unity_parser::WorldTransform, &unity_parser::GameObject)>()
|
||||
.for_each(|(teleporter, transform, object)| {
|
||||
let world_pos = transform.position();
|
||||
|
||||
// Get the tp_transform position if it exists
|
||||
let (tp_x, tp_y) = if let Some(tp_entity) = teleporter.tp_transform {
|
||||
if let Some(tp_transform) = scene.world.borrow::<unity_parser::WorldTransform>().get(tp_entity) {
|
||||
let tp_pos = tp_transform.position();
|
||||
(Some(tp_pos.x as f32), Some(tp_pos.z as f32))
|
||||
} else {
|
||||
(None, None)
|
||||
}
|
||||
} else {
|
||||
(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)
|
||||
.values((
|
||||
world_teleporters::pos_x.eq(world_pos.x as f32),
|
||||
world_teleporters::pos_y.eq(world_pos.z as f32),
|
||||
world_teleporters::tp_x.eq(tp_x),
|
||||
world_teleporters::tp_y.eq(tp_y),
|
||||
))
|
||||
.execute(conn);
|
||||
|
||||
count += 1;
|
||||
});
|
||||
|
||||
info!("✅ Saved {} teleporters to database", count);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Save workbench data to database
|
||||
fn save_workbenches(
|
||||
conn: &mut SqliteConnection,
|
||||
scene: &unity_parser::UnityScene,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
use cursebreaker_parser::schema::world_workbenches;
|
||||
|
||||
// Clear existing workbenches
|
||||
diesel::delete(world_workbenches::table).execute(conn)?;
|
||||
|
||||
let mut count = 0;
|
||||
|
||||
// Query all workbenches
|
||||
scene.world
|
||||
.query_all::<(&InteractableWorkbench, &unity_parser::WorldTransform, &unity_parser::GameObject)>()
|
||||
.for_each(|(workbench, transform, object)| {
|
||||
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)
|
||||
.values((
|
||||
world_workbenches::pos_x.eq(world_pos.x as f32),
|
||||
world_workbenches::pos_y.eq(world_pos.z as f32),
|
||||
world_workbenches::workbench_id.eq(workbench.workbench_id as i32),
|
||||
))
|
||||
.execute(conn);
|
||||
|
||||
count += 1;
|
||||
});
|
||||
|
||||
info!("✅ Saved {} workbenches to database", count);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Save loot spawner data to database
|
||||
fn save_loot_spawners(
|
||||
conn: &mut SqliteConnection,
|
||||
scene: &unity_parser::UnityScene,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
use cursebreaker_parser::schema::world_loot;
|
||||
|
||||
// Clear existing loot spawners
|
||||
diesel::delete(world_loot::table).execute(conn)?;
|
||||
|
||||
let mut count = 0;
|
||||
|
||||
// Query all loot spawners
|
||||
scene.world
|
||||
.query_all::<(&LootSpawner, &unity_parser::WorldTransform, &unity_parser::GameObject)>()
|
||||
.for_each(|(loot, transform, object)| {
|
||||
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)
|
||||
.values((
|
||||
world_loot::pos_x.eq(world_pos.x as f32),
|
||||
world_loot::pos_y.eq(world_pos.z as f32),
|
||||
world_loot::item_id.eq(loot.item_id as i32),
|
||||
world_loot::amount.eq(loot.amount as i32),
|
||||
world_loot::respawn_time.eq(loot.respawn_time as i32),
|
||||
world_loot::visibility_checks.eq(&loot.visibility_checks),
|
||||
))
|
||||
.execute(conn);
|
||||
|
||||
count += 1;
|
||||
});
|
||||
|
||||
info!("✅ Saved {} loot spawners to database", count);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Save map icon data to database
|
||||
fn save_map_icons(
|
||||
conn: &mut SqliteConnection,
|
||||
scene: &unity_parser::UnityScene,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
use cursebreaker_parser::schema::world_map_icons;
|
||||
|
||||
// Clear existing map icons
|
||||
diesel::delete(world_map_icons::table).execute(conn)?;
|
||||
|
||||
let mut count = 0;
|
||||
|
||||
// Query all map icons
|
||||
scene.world
|
||||
.query_all::<(&MapIcon, &unity_parser::WorldTransform, &unity_parser::GameObject)>()
|
||||
.for_each(|(icon, transform, object)| {
|
||||
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)
|
||||
.values((
|
||||
world_map_icons::pos_x.eq(world_pos.x as f32),
|
||||
world_map_icons::pos_y.eq(world_pos.z as f32),
|
||||
world_map_icons::icon_type.eq(icon.icon_type as i32),
|
||||
world_map_icons::icon_size.eq(icon.icon_size as i32),
|
||||
world_map_icons::icon.eq(&icon.icon),
|
||||
world_map_icons::text.eq(&icon.text),
|
||||
world_map_icons::font_size.eq(icon.font_size as i32),
|
||||
world_map_icons::hover_text.eq(&icon.hover_text),
|
||||
))
|
||||
.execute(conn);
|
||||
|
||||
count += 1;
|
||||
});
|
||||
|
||||
info!("✅ Saved {} map icons to database", count);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Save map name changer data to database
|
||||
fn save_map_name_changers(
|
||||
conn: &mut SqliteConnection,
|
||||
scene: &unity_parser::UnityScene,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
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;
|
||||
|
||||
// Query all map name changers
|
||||
scene.world
|
||||
.query_all::<(&MapNameChanger, &unity_parser::WorldTransform, &unity_parser::GameObject)>()
|
||||
.for_each(|(changer, transform, object)| {
|
||||
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)
|
||||
.values((
|
||||
world_map_name_changers::pos_x.eq(world_pos.x as f32),
|
||||
world_map_name_changers::pos_y.eq(world_pos.z as f32),
|
||||
world_map_name_changers::map_name.eq(&changer.map_name),
|
||||
))
|
||||
.execute(conn);
|
||||
|
||||
count += 1;
|
||||
});
|
||||
|
||||
info!("✅ Saved {} map name changers to database", count);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
use cursebreaker_parser::ItemDatabase;
|
||||
use diesel::prelude::*;
|
||||
use diesel::sqlite::SqliteConnection;
|
||||
use std::env;
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let database_url = env::var("DATABASE_URL").unwrap_or_else(|_| "../cursebreaker.db".to_string());
|
||||
let mut conn = SqliteConnection::establish(&database_url)?;
|
||||
|
||||
let item_db = ItemDatabase::load_from_db(&mut conn)?;
|
||||
|
||||
println!("✅ Database contains {} items", item_db.len());
|
||||
|
||||
if item_db.len() > 0 {
|
||||
println!("\nFirst 5 items:");
|
||||
for (i, item) in item_db.all_items().iter().take(5).enumerate() {
|
||||
println!(" {}. {} (ID: {})", i + 1, item.item_name, item.type_id);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
use diesel::prelude::*;
|
||||
use diesel::sqlite::SqliteConnection;
|
||||
use std::env;
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let database_url = env::var("DATABASE_URL").unwrap_or_else(|_| "../cursebreaker.db".to_string());
|
||||
let mut conn = SqliteConnection::establish(&database_url)?;
|
||||
|
||||
// Check items with new columns
|
||||
#[derive(Queryable)]
|
||||
struct ItemInfo {
|
||||
id: Option<i32>,
|
||||
name: String,
|
||||
item_type: String,
|
||||
level: i32,
|
||||
price: i32,
|
||||
max_stack: i32,
|
||||
skill: String,
|
||||
}
|
||||
|
||||
use cursebreaker_parser::schema::items::dsl::*;
|
||||
|
||||
let sample_items = items
|
||||
.select((id, name, item_type, level, price, max_stack, skill))
|
||||
.limit(5)
|
||||
.load::<ItemInfo>(&mut conn)?;
|
||||
|
||||
println!("✅ Sample items with expanded columns:\n");
|
||||
for item in sample_items {
|
||||
println!(
|
||||
" {} - {} (Type: {}, Level: {}, Price: {}, MaxStack: {}, Skill: {})",
|
||||
item.id.unwrap_or(0),
|
||||
item.name,
|
||||
item.item_type,
|
||||
item.level,
|
||||
item.price,
|
||||
item.max_stack,
|
||||
item.skill
|
||||
);
|
||||
}
|
||||
|
||||
// Check crafting recipes
|
||||
#[derive(Queryable)]
|
||||
struct RecipeCount {
|
||||
count: i64,
|
||||
}
|
||||
|
||||
use diesel::dsl::count_star;
|
||||
use cursebreaker_parser::schema::crafting_recipes;
|
||||
|
||||
let recipe_count = crafting_recipes::table
|
||||
.select(count_star())
|
||||
.first::<i64>(&mut conn)?;
|
||||
|
||||
println!("\n✅ Total crafting recipes: {}", recipe_count);
|
||||
|
||||
// Show sample recipes
|
||||
if recipe_count > 0 {
|
||||
#[derive(Queryable)]
|
||||
struct RecipeInfo {
|
||||
id: Option<i32>,
|
||||
product_item_id: i32,
|
||||
skill: String,
|
||||
level: i32,
|
||||
workbench_id: i32,
|
||||
}
|
||||
|
||||
let sample_recipes = crafting_recipes::table
|
||||
.select((
|
||||
crafting_recipes::id,
|
||||
crafting_recipes::product_item_id,
|
||||
crafting_recipes::skill,
|
||||
crafting_recipes::level,
|
||||
crafting_recipes::workbench_id,
|
||||
))
|
||||
.limit(3)
|
||||
.load::<RecipeInfo>(&mut conn)?;
|
||||
|
||||
println!("\nSample crafting recipes:");
|
||||
for recipe in sample_recipes {
|
||||
// Get product name
|
||||
let product_name: String = items
|
||||
.filter(id.eq(recipe.product_item_id))
|
||||
.select(name)
|
||||
.first(&mut conn)?;
|
||||
|
||||
// Get ingredient count
|
||||
use cursebreaker_parser::schema::crafting_recipe_items;
|
||||
let ingredient_count: i64 = crafting_recipe_items::table
|
||||
.filter(crafting_recipe_items::recipe_id.eq(recipe.id.unwrap()))
|
||||
.select(count_star())
|
||||
.first(&mut conn)?;
|
||||
|
||||
println!(
|
||||
" Recipe #{}: {} (Skill: {}, Level: {}, Workbench: {}, Ingredients: {})",
|
||||
recipe.id.unwrap(),
|
||||
product_name,
|
||||
recipe.skill,
|
||||
recipe.level,
|
||||
recipe.workbench_id,
|
||||
ingredient_count
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
use diesel::prelude::*;
|
||||
use diesel::sqlite::SqliteConnection;
|
||||
use std::env;
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let database_url = env::var("DATABASE_URL").unwrap_or_else(|_| "../cursebreaker.db".to_string());
|
||||
let mut conn = SqliteConnection::establish(&database_url)?;
|
||||
|
||||
// Check items with images
|
||||
#[derive(Queryable)]
|
||||
struct ItemImageInfo {
|
||||
id: Option<i32>,
|
||||
name: String,
|
||||
icon_large: Option<Vec<u8>>,
|
||||
icon_medium: Option<Vec<u8>>,
|
||||
icon_small: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
use cursebreaker_parser::schema::items::dsl::*;
|
||||
|
||||
// Count items with images
|
||||
let all_items: Vec<ItemImageInfo> = items
|
||||
.select((id, name, icon_large, icon_medium, icon_small))
|
||||
.load(&mut conn)?;
|
||||
|
||||
let items_with_images = all_items.iter().filter(|item| item.icon_large.is_some()).count();
|
||||
let items_without_images = all_items.len() - items_with_images;
|
||||
|
||||
println!("✅ Image Statistics:\n");
|
||||
println!(" Total items: {}", all_items.len());
|
||||
println!(" Items with icons: {}", items_with_images);
|
||||
println!(" Items without icons: {}", items_without_images);
|
||||
|
||||
// Show sample images with sizes
|
||||
println!("\n📸 Sample items with icons:\n");
|
||||
|
||||
for (i, item) in all_items.iter().filter(|item| item.icon_large.is_some()).take(5).enumerate() {
|
||||
let large_size = item.icon_large.as_ref().map(|v| v.len()).unwrap_or(0);
|
||||
let medium_size = item.icon_medium.as_ref().map(|v| v.len()).unwrap_or(0);
|
||||
let small_size = item.icon_small.as_ref().map(|v| v.len()).unwrap_or(0);
|
||||
let total_size = large_size + medium_size + small_size;
|
||||
|
||||
println!(
|
||||
" {}. {} (ID: {})",
|
||||
i + 1,
|
||||
item.name,
|
||||
item.id.unwrap_or(0)
|
||||
);
|
||||
println!(
|
||||
" Large (256px): {:.1} KB | Medium (64px): {:.1} KB | Small (16px): {:.1} KB | Total: {:.1} KB",
|
||||
large_size as f64 / 1024.0,
|
||||
medium_size as f64 / 1024.0,
|
||||
small_size as f64 / 1024.0,
|
||||
total_size as f64 / 1024.0
|
||||
);
|
||||
}
|
||||
|
||||
// Calculate total storage used by images
|
||||
let total_storage: usize = all_items.iter().map(|item| {
|
||||
item.icon_large.as_ref().map(|v| v.len()).unwrap_or(0) +
|
||||
item.icon_medium.as_ref().map(|v| v.len()).unwrap_or(0) +
|
||||
item.icon_small.as_ref().map(|v| v.len()).unwrap_or(0)
|
||||
}).sum();
|
||||
|
||||
println!("\n💾 Total image storage: {:.2} MB", total_storage as f64 / 1024.0 / 1024.0);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
//! Verify resource_icons table
|
||||
|
||||
use diesel::prelude::*;
|
||||
use diesel::sqlite::SqliteConnection;
|
||||
use std::env;
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
use cursebreaker_parser::schema::resource_icons;
|
||||
|
||||
let database_url = env::var("DATABASE_URL").unwrap_or_else(|_| "../cursebreaker.db".to_string());
|
||||
let mut conn = SqliteConnection::establish(&database_url)?;
|
||||
|
||||
// Count total icons
|
||||
let count: i64 = resource_icons::table.count().get_result(&mut conn)?;
|
||||
println!("✅ Database contains {} resource icons\n", count);
|
||||
|
||||
// Get all icons and show details
|
||||
#[derive(Queryable, Debug)]
|
||||
struct ResourceIcon {
|
||||
item_id: i32,
|
||||
name: String,
|
||||
icon_64: Vec<u8>,
|
||||
}
|
||||
|
||||
let icons: Vec<ResourceIcon> = resource_icons::table
|
||||
.load(&mut conn)?;
|
||||
|
||||
if icons.is_empty() {
|
||||
println!("ℹ️ No resource icons found in database.");
|
||||
println!(" This is expected if no item icons were available during scene parsing.");
|
||||
} else {
|
||||
println!("Resource icons:");
|
||||
for icon in icons {
|
||||
println!(" • Item {}: {} ({} bytes)",
|
||||
icon.item_id, icon.name, icon.icon_64.len());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
use diesel::prelude::*;
|
||||
use diesel::sqlite::SqliteConnection;
|
||||
use std::env;
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let database_url = env::var("DATABASE_URL").unwrap_or_else(|_| "../cursebreaker.db".to_string());
|
||||
let mut conn = SqliteConnection::establish(&database_url)?;
|
||||
|
||||
// Count total stats
|
||||
use cursebreaker_parser::schema::item_stats::dsl::*;
|
||||
use diesel::dsl::count_star;
|
||||
|
||||
let total_stats: i64 = item_stats.select(count_star()).first(&mut conn)?;
|
||||
|
||||
println!("✅ Item Stats Statistics:\n");
|
||||
println!(" Total stat entries: {}", total_stats);
|
||||
|
||||
// Get stats breakdown by type
|
||||
#[derive(Queryable)]
|
||||
struct StatTypeCount {
|
||||
stat_type: String,
|
||||
count: i64,
|
||||
}
|
||||
|
||||
let stats_by_type: Vec<StatTypeCount> = item_stats
|
||||
.group_by(stat_type)
|
||||
.select((stat_type, count_star()))
|
||||
.order_by(count_star().desc())
|
||||
.load(&mut conn)?;
|
||||
|
||||
println!("\n📊 Stats breakdown by type:\n");
|
||||
for stat in &stats_by_type {
|
||||
println!(" {}: {} items", stat.stat_type, stat.count);
|
||||
}
|
||||
|
||||
// Find items with the most stats
|
||||
#[derive(Queryable)]
|
||||
struct ItemStatCount {
|
||||
item_id: i32,
|
||||
stat_count: i64,
|
||||
}
|
||||
|
||||
let items_with_most_stats: Vec<ItemStatCount> = item_stats
|
||||
.group_by(item_id)
|
||||
.select((item_id, count_star()))
|
||||
.order_by(count_star().desc())
|
||||
.limit(5)
|
||||
.load(&mut conn)?;
|
||||
|
||||
println!("\n🏆 Items with most stats:\n");
|
||||
for item_stat in items_with_most_stats {
|
||||
// Get item name
|
||||
use cursebreaker_parser::schema::items;
|
||||
let item_name: String = items::table
|
||||
.filter(items::id.eq(item_stat.item_id))
|
||||
.select(items::name)
|
||||
.first(&mut conn)?;
|
||||
|
||||
println!(" {} (ID: {}) - {} stats", item_name, item_stat.item_id, item_stat.stat_count);
|
||||
|
||||
// Get the actual stats for this item
|
||||
#[derive(Queryable)]
|
||||
struct ItemStatDetail {
|
||||
stat_type: String,
|
||||
value: f32,
|
||||
}
|
||||
|
||||
let stats: Vec<ItemStatDetail> = item_stats
|
||||
.filter(item_id.eq(item_stat.item_id))
|
||||
.select((stat_type, value))
|
||||
.load(&mut conn)?;
|
||||
|
||||
for stat in stats {
|
||||
println!(" {}: {}", stat.stat_type, stat.value);
|
||||
}
|
||||
println!();
|
||||
}
|
||||
|
||||
// Show some example stat queries
|
||||
println!("📈 Example queries:\n");
|
||||
|
||||
// Items with high physical damage
|
||||
#[derive(Queryable)]
|
||||
struct ItemWithStat {
|
||||
item_id: i32,
|
||||
value: f32,
|
||||
}
|
||||
|
||||
let high_damage_items: Vec<ItemWithStat> = item_stats
|
||||
.filter(stat_type.eq("damage_physical"))
|
||||
.filter(value.gt(50.0))
|
||||
.select((item_id, value))
|
||||
.order_by(value.desc())
|
||||
.limit(5)
|
||||
.load(&mut conn)?;
|
||||
|
||||
if !high_damage_items.is_empty() {
|
||||
println!(" Items with Physical Damage > 50:");
|
||||
for item in high_damage_items {
|
||||
use cursebreaker_parser::schema::items;
|
||||
let item_name: String = items::table
|
||||
.filter(items::id.eq(item.item_id))
|
||||
.select(items::name)
|
||||
.first(&mut conn)?;
|
||||
println!(" {} - {:.1} damage", item_name, item.value);
|
||||
}
|
||||
}
|
||||
|
||||
// Items with health bonuses
|
||||
let health_items: Vec<ItemWithStat> = item_stats
|
||||
.filter(stat_type.eq("health"))
|
||||
.filter(value.gt(0.0))
|
||||
.select((item_id, value))
|
||||
.order_by(value.desc())
|
||||
.limit(5)
|
||||
.load(&mut conn)?;
|
||||
|
||||
if !health_items.is_empty() {
|
||||
println!("\n Items with Health bonuses:");
|
||||
for item in health_items {
|
||||
use cursebreaker_parser::schema::items;
|
||||
let item_name: String = items::table
|
||||
.filter(items::id.eq(item.item_id))
|
||||
.select(items::name)
|
||||
.first(&mut conn)?;
|
||||
println!(" {} - {:.0} health", item_name, item.value);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -89,7 +89,7 @@ impl MinimapDatabase {
|
||||
source_path: relative_path.to_str().unwrap_or(""),
|
||||
};
|
||||
|
||||
diesel::insert_into(minimap_tiles::table)
|
||||
diesel::replace_into(minimap_tiles::table)
|
||||
.values(&new_tile)
|
||||
.execute(&mut conn)?;
|
||||
|
||||
@@ -223,7 +223,7 @@ impl MinimapDatabase {
|
||||
source_path: &source_tiles,
|
||||
};
|
||||
|
||||
diesel::insert_into(minimap_tiles::table)
|
||||
diesel::replace_into(minimap_tiles::table)
|
||||
.values(&new_tile)
|
||||
.execute(conn)?;
|
||||
|
||||
|
||||
@@ -91,6 +91,12 @@ pub use types::{
|
||||
MAX_STACK,
|
||||
// Other types
|
||||
InteractableResource,
|
||||
InteractableTeleporter,
|
||||
InteractableWorkbench,
|
||||
LootSpawner,
|
||||
MapIcon,
|
||||
MapIconType,
|
||||
MapNameChanger,
|
||||
Npc,
|
||||
NpcStat,
|
||||
NpcLevel,
|
||||
|
||||
@@ -186,6 +186,38 @@ diesel::table! {
|
||||
}
|
||||
}
|
||||
|
||||
diesel::table! {
|
||||
world_loot (pos_x, pos_y) {
|
||||
pos_x -> Float,
|
||||
pos_y -> Float,
|
||||
item_id -> Integer,
|
||||
amount -> Integer,
|
||||
respawn_time -> Integer,
|
||||
visibility_checks -> Text,
|
||||
}
|
||||
}
|
||||
|
||||
diesel::table! {
|
||||
world_map_icons (pos_x, pos_y) {
|
||||
pos_x -> Float,
|
||||
pos_y -> Float,
|
||||
icon_type -> Integer,
|
||||
icon_size -> Integer,
|
||||
icon -> Text,
|
||||
text -> Text,
|
||||
font_size -> Integer,
|
||||
hover_text -> Text,
|
||||
}
|
||||
}
|
||||
|
||||
diesel::table! {
|
||||
world_map_name_changers (pos_x, pos_y) {
|
||||
pos_x -> Float,
|
||||
pos_y -> Float,
|
||||
map_name -> Text,
|
||||
}
|
||||
}
|
||||
|
||||
diesel::table! {
|
||||
world_resources (item_id, pos_x, pos_y) {
|
||||
item_id -> Integer,
|
||||
@@ -194,6 +226,23 @@ diesel::table! {
|
||||
}
|
||||
}
|
||||
|
||||
diesel::table! {
|
||||
world_teleporters (pos_x, pos_y) {
|
||||
pos_x -> Float,
|
||||
pos_y -> Float,
|
||||
tp_x -> Nullable<Float>,
|
||||
tp_y -> Nullable<Float>,
|
||||
}
|
||||
}
|
||||
|
||||
diesel::table! {
|
||||
world_workbenches (pos_x, pos_y) {
|
||||
pos_x -> Float,
|
||||
pos_y -> Float,
|
||||
workbench_id -> Integer,
|
||||
}
|
||||
}
|
||||
|
||||
diesel::joinable!(crafting_recipe_items -> crafting_recipes (recipe_id));
|
||||
diesel::joinable!(crafting_recipe_items -> items (item_id));
|
||||
diesel::joinable!(crafting_recipes -> items (product_item_id));
|
||||
@@ -218,5 +267,10 @@ diesel::allow_tables_to_appear_in_same_query!(
|
||||
resource_icons,
|
||||
shops,
|
||||
traits,
|
||||
world_loot,
|
||||
world_map_icons,
|
||||
world_map_name_changers,
|
||||
world_resources,
|
||||
world_teleporters,
|
||||
world_workbenches,
|
||||
);
|
||||
|
||||
BIN
cursebreaker.db
BIN
cursebreaker.db
Binary file not shown.
30
settings.json
Normal file
30
settings.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"ssh_connections": [
|
||||
{
|
||||
"host": "192.168.11.7",
|
||||
"username": "connor",
|
||||
"args": [],
|
||||
"projects": [
|
||||
{
|
||||
"paths": ["/home/connor/repos/cursebreaker-parser-rust"]
|
||||
}
|
||||
],
|
||||
"nickname": "connor-mini"
|
||||
}
|
||||
],
|
||||
"ui_font_size": 16,
|
||||
"buffer_font_size": 15,
|
||||
"theme": {
|
||||
"mode": "system",
|
||||
"light": "One Light",
|
||||
"dark": "One Dark"
|
||||
},
|
||||
"agent_servers": {
|
||||
"Grok Build Agent": {
|
||||
"type": "custom",
|
||||
"command": "/usr/bin/grok",
|
||||
"args": ["agent", "--yolo", "stdio"],
|
||||
"env": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user