Spaces:
Sleeping
Sleeping
File size: 14,680 Bytes
41c1a97 50498a4 41c1a97 50498a4 7136196 dfe769e 41c1a97 7136196 50498a4 dfe769e 50498a4 225f7b1 7136196 50498a4 7136196 225f7b1 7136196 225f7b1 7136196 225f7b1 7136196 50498a4 225f7b1 50498a4 7136196 50498a4 7136196 50498a4 7136196 50498a4 7136196 50498a4 225f7b1 41c1a97 7136196 225f7b1 7136196 225f7b1 7136196 225f7b1 7136196 225f7b1 7136196 225f7b1 7136196 225f7b1 7136196 225f7b1 7136196 225f7b1 7136196 225f7b1 50498a4 7136196 50498a4 7136196 225f7b1 50498a4 225f7b1 7136196 50498a4 7136196 225f7b1 50498a4 7136196 225f7b1 7136196 225f7b1 7136196 225f7b1 7136196 225f7b1 7136196 225f7b1 50498a4 225f7b1 50498a4 7136196 50498a4 7136196 50498a4 225f7b1 50498a4 7136196 50498a4 225f7b1 7136196 225f7b1 7136196 225f7b1 7136196 225f7b1 7136196 225f7b1 7136196 41c1a97 50498a4 7136196 41c1a97 225f7b1 41c1a97 7136196 41c1a97 225f7b1 f32aedf 7136196 41c1a97 7136196 50498a4 7825ef7 7136196 d93d024 50498a4 7136196 d93d024 7825ef7 7136196 f32aedf 7136196 f32aedf 7136196 14b289d f32aedf 7825ef7 7136196 7825ef7 f32aedf 7136196 7825ef7 f32aedf 7136196 50498a4 14b289d 7825ef7 f32aedf dfe769e 7825ef7 14b289d 7136196 dfe769e 7825ef7 f32aedf 225f7b1 7136196 f32aedf d93d024 50498a4 7136196 dfe769e 7136196 dfe769e 7825ef7 50498a4 7136196 7825ef7 14b289d 7825ef7 7136196 7825ef7 f32aedf 7136196 7825ef7 f32aedf 7136196 7825ef7 50498a4 14b289d 7825ef7 7136196 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 |
# app.py
import streamlit as st
import streamlit.components.v1 as components
import os
import json
import sqlite3 # Use SQLite for robust state management
import uuid
import math
import time # For potential delays if needed
from streamlit_js_eval import streamlit_js_eval # For JS communication
# --- Constants ---
DB_FILE = "world_state_v3.db" # Use a new DB file name to ensure fresh start
PLOT_WIDTH = 50.0
PLOT_DEPTH = 50.0
# --- Database Setup ---
def init_db():
"""Initializes the SQLite database and tables."""
st.write(f"DEBUG: Initializing Database '{os.path.abspath(DB_FILE)}'...")
try:
# Ensure connection closes even if errors occur during table creation
with sqlite3.connect(DB_FILE, timeout=10) as conn: # Added timeout
cursor = conn.cursor()
# Enable WAL mode for potentially better concurrency - might help?
# cursor.execute('PRAGMA journal_mode=WAL;')
# Plots table
cursor.execute('''
CREATE TABLE IF NOT EXISTS plots (
grid_x INTEGER NOT NULL, grid_z INTEGER NOT NULL, name TEXT,
last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (grid_x, grid_z)
)
''')
# Objects table
cursor.execute('''
CREATE TABLE IF NOT EXISTS objects (
obj_id TEXT PRIMARY KEY, plot_grid_x INTEGER NOT NULL, plot_grid_z INTEGER NOT NULL,
type TEXT NOT NULL, pos_x REAL NOT NULL, pos_y REAL NOT NULL, pos_z REAL NOT NULL,
rot_x REAL DEFAULT 0.0, rot_y REAL DEFAULT 0.0, rot_z REAL DEFAULT 0.0, rot_order TEXT DEFAULT 'XYZ',
FOREIGN KEY (plot_grid_x, plot_grid_z) REFERENCES plots(grid_x, grid_z) ON DELETE CASCADE
)
''')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_objects_plot ON objects (plot_grid_x, plot_grid_z)')
print(f"DEBUG: Database {DB_FILE} initialized/checked.") # To console
except sqlite3.Error as e:
st.exception(f"CRITICAL DATABASE INIT ERROR: {e}")
st.stop()
init_db() # Run initialization at the start
# --- Helper Functions (Database Operations - Robust Connections) ---
# *** REMOVED @st.cache_data - Load fresh every time ***
def load_world_state_from_db():
"""Loads all plot metadata and object data fresh from the SQLite DB."""
session_id = st.runtime.scriptrunner.get_script_run_ctx().session_id[:5] # Short ID for logging
st.write(f"DEBUG [{session_id}]: Executing load_world_state_from_db...")
plots_metadata = []
all_initial_objects_world = []
try:
# Ensure connection is opened and closed for each load operation
with sqlite3.connect(DB_FILE, timeout=10) as conn:
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
# 1. Load Plot Metadata
cursor.execute("SELECT grid_x, grid_z, name FROM plots ORDER BY grid_x, grid_z")
plot_rows = cursor.fetchall()
st.write(f"DEBUG [{session_id}]: DB Read Found {len(plot_rows)} plot rows.")
if not plot_rows:
st.write(f"DEBUG [{session_id}]: No plots found in DB.")
plot_keys = set() # To efficiently map objects later
for row in plot_rows:
gx, gz = row['grid_x'], row['grid_z']
plots_metadata.append({
'id': f"plot_X{gx}_Z{gz}", 'grid_x': gx, 'grid_z': gz,
'name': row['name'] or f"Plot ({gx},{gz})",
'x_offset': gx * PLOT_WIDTH, 'z_offset': gz * PLOT_DEPTH
})
plot_keys.add((gx, gz))
# 2. Load All Objects
cursor.execute("SELECT obj_id, plot_grid_x, plot_grid_z, type, pos_x, pos_y, pos_z, rot_x, rot_y, rot_z, rot_order FROM objects")
object_rows = cursor.fetchall()
st.write(f"DEBUG [{session_id}]: DB Read Found {len(object_rows)} object rows.")
# 3. Combine and Calculate World Coordinates
objects_loaded_count = 0
for row in object_rows:
plot_key = (row['plot_grid_x'], row['plot_grid_z'])
# Ensure the object belongs to a known plot (data integrity check)
if plot_key in plot_keys:
obj_data = dict(row) # Convert row object to dict
# Find corresponding plot metadata for offset calculation
plot_meta = next((p for p in plots_metadata if p['grid_x'] == row['plot_grid_x'] and p['grid_z'] == row['plot_grid_z']), None)
if plot_meta:
world_obj_data = obj_data.copy()
world_obj_data['pos_x'] = obj_data['pos_x'] + plot_meta['x_offset']
world_obj_data['pos_z'] = obj_data['pos_z'] + plot_meta['z_offset']
world_obj_data['pos_y'] = obj_data['pos_y'] # Y is already world Y
all_initial_objects_world.append(world_obj_data)
objects_loaded_count += 1
else:
print(f"WARN: Object {obj_data['obj_id']} references non-existent plot {plot_key}") # To console
else:
print(f"WARN: Object {obj_data['obj_id']} references unknown plot {plot_key}") # To console
st.write(f"DEBUG [{session_id}]: DB Load Complete. Processed {len(plots_metadata)} plots, {objects_loaded_count} objects.")
except sqlite3.Error as e:
st.exception(f"Database load error for session {session_id}: {e}")
return [], [] # Return empty on error
return plots_metadata, all_initial_objects_world
def save_plot_data_to_db(target_grid_x, target_grid_z, objects_data_list):
"""Saves object data list to DB for a specific plot. Overwrites existing objects."""
st.write(f"DEBUG: Executing save_plot_data_to_db for plot ({target_grid_x},{target_grid_z})...")
plot_x_offset = target_grid_x * PLOT_WIDTH
plot_z_offset = target_grid_z * PLOT_DEPTH
# Use a generic name or allow passing one if needed later
plot_name = f"Plot ({target_grid_x},{target_grid_z})"
if not isinstance(objects_data_list, list):
st.error("Save Error: Invalid object data format (expected list).")
return False
try:
# Use 'with' for automatic transaction handling and closing
with sqlite3.connect(DB_FILE, timeout=10) as conn:
cursor = conn.cursor()
# 1. Upsert Plot (ensures plot exists, updates timestamp)
cursor.execute('''
INSERT INTO plots (grid_x, grid_z, name) VALUES (?, ?, ?)
ON CONFLICT(grid_x, grid_z) DO UPDATE SET last_updated = CURRENT_TIMESTAMP
''', (target_grid_x, target_grid_z, plot_name))
# st.write(f"DEBUG: Upserted plot metadata for ({target_grid_x},{target_grid_z})")
# 2. Delete ALL existing objects for this specific plot
cursor.execute("DELETE FROM objects WHERE plot_grid_x = ? AND plot_grid_z = ?", (target_grid_x, target_grid_z))
# st.write(f"DEBUG: Deleted {cursor.rowcount} old objects for plot.")
# 3. Insert the new objects
objects_to_insert = []
for obj in objects_data_list:
pos = obj.get('position', {})
rot = obj.get('rotation', {})
obj_type = obj.get('type', 'Unknown')
obj_id = obj.get('obj_id', str(uuid.uuid4()))
if not all(k in pos for k in ['x', 'y', 'z']) or obj_type == 'Unknown': continue
rel_x = pos.get('x', 0.0) - plot_x_offset
rel_z = pos.get('z', 0.0) - plot_z_offset
rel_y = pos.get('y', 0.0)
objects_to_insert.append((
obj_id, target_grid_x, target_grid_z, obj_type,
rel_x, rel_y, rel_z,
rot.get('_x', 0.0), rot.get('_y', 0.0), rot.get('_z', 0.0), rot.get('_order', 'XYZ')
))
if objects_to_insert:
cursor.executemany('''
INSERT OR REPLACE INTO objects
(obj_id, plot_grid_x, plot_grid_z, type, pos_x, pos_y, pos_z, rot_x, rot_y, rot_z, rot_order)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
''', objects_to_insert) # Use INSERT OR REPLACE based on obj_id primary key
st.write(f"DEBUG: Inserted/Replaced {len(objects_to_insert)} objects.")
else:
st.write(f"DEBUG: No objects provided to insert for plot ({target_grid_x},{target_grid_z}). Plot cleared.")
# Commit happens automatically when 'with' block exits without error
st.success(f"Plot ({target_grid_x},{target_grid_z}) saved successfully to DB.")
return True
except sqlite3.Error as e:
st.exception(f"DATABASE SAVE ERROR for plot ({target_grid_x},{target_grid_z}): {e}")
return False
# --- Page Config ---
st.set_page_config( page_title="DB Synced World Builder v3", layout="wide")
# --- Initialize Session State ---
if 'selected_object' not in st.session_state: st.session_state.selected_object = 'None'
if 'js_save_data_result' not in st.session_state: st.session_state.js_save_data_result = None
# No refresh counter needed
# --- Load World State From DB (Fresh on each run/rerun) ---
plots_metadata, all_initial_objects = load_world_state_from_db()
# --- Sidebar ---
with st.sidebar:
st.title("🏗️ World Controls")
# Refresh Button (Just reruns the script)
if st.button("🔄 Refresh World View", key="refresh_button"):
st.info("Reloading world state from database...")
st.rerun() # Rerun forces call to load_world_state_from_db
st.header("Navigation (Plots)")
# ... (Navigation button code unchanged - uses latest plots_metadata) ...
st.caption("Click to teleport player to a plot.")
max_cols = 2; cols = st.columns(max_cols); col_idx = 0
sorted_plots_for_nav = sorted(plots_metadata, key=lambda p: (p['grid_x'], p['grid_z']))
for plot in sorted_plots_for_nav:
button_label = f"➡️ {plot.get('name', plot['id'])} ({plot['grid_x']},{plot['grid_z']})"
if cols[col_idx].button(button_label, key=f"nav_{plot['id']}"):
target_x = plot['x_offset'] + PLOT_WIDTH/2; target_z = plot['z_offset'] + PLOT_DEPTH/2
try: streamlit_js_eval(js_code=f"teleportPlayer({target_x}, {target_z});", key=f"teleport_{plot['id']}")
except Exception as e: st.error(f"Teleport command failed: {e}")
col_idx = (col_idx + 1) % max_cols
st.markdown("---")
# --- Object Placement ---
st.header("Place Objects")
# ... (Object selection unchanged) ...
object_types = ["None", "Simple House", "Tree", "Rock", "Fence Post"]
current_object_index = object_types.index(st.session_state.selected_object) if st.session_state.selected_object in object_types else 0
selected_object_type_widget = st.selectbox( "Select Object:", options=object_types, index=current_object_index, key="selected_object_widget")
if selected_object_type_widget != st.session_state.selected_object: st.session_state.selected_object = selected_object_type_widget
st.markdown("---")
# --- Saving ---
st.header("Save Work")
st.caption("Saves ALL objects in the player's current plot to the central database.")
if st.button("💾 Save Current Plot", key="save_button"):
js_get_data_code = "getSaveDataAndPosition();"
streamlit_js_eval(js_code=js_get_data_code, key="js_save_processor")
st.rerun()
# --- Process Save Data ---
save_data_from_js = st.session_state.get("js_save_processor", None)
if save_data_from_js is not None:
st.write("DEBUG: Processing save request from JS...") # Debug
save_processed_successfully = False
try:
payload = json.loads(save_data_from_js) if isinstance(save_data_from_js, str) else save_data_from_js
if isinstance(payload, dict) and 'playerPosition' in payload and 'objectsToSave' in payload:
player_pos = payload['playerPosition']
objects_to_save = payload['objectsToSave'] # World coords from JS
if isinstance(objects_to_save, list):
target_grid_x = math.floor(player_pos.get('x', 0.0) / PLOT_WIDTH)
target_grid_z = math.floor(player_pos.get('z', 0.0) / PLOT_DEPTH)
# --- Save the data to SQLite DB ---
save_ok = save_plot_data_to_db(target_grid_x, target_grid_z, objects_to_save)
if save_ok: save_processed_successfully = True # Flag success
else: st.error(f"Failed DB save for plot ({target_grid_x},{target_grid_z}).")
else: st.error("Save Error: 'objectsToSave' format invalid (expected list).")
else: st.error("Save Error: Invalid payload structure received.")
except json.JSONDecodeError: st.error("Save Error: Failed to decode JSON data from client.")
except Exception as e: st.exception(f"Save Error: An unexpected error occurred: {e}")
# Clear the trigger data from session state ALWAYS after processing attempt
st.session_state.js_save_processor = None
# Rerun ONLY if save was flagged successful to reload the state
if save_processed_successfully:
st.rerun()
# --- Main Area ---
st.header("Database Synced 3D World")
st.caption(f"DB: '{os.path.abspath(DB_FILE)}'. Plots loaded: {len(plots_metadata)}. Use 'Refresh' button to sync.")
# --- Load and Prepare HTML ---
html_file_path = 'index.html'
html_content_with_state = None
try:
with open(html_file_path, 'r', encoding='utf-8') as f:
html_template = f.read()
# Inject data loaded fresh from DB
js_injection_script = f"""
<script>
window.ALL_INITIAL_OBJECTS = {json.dumps(all_initial_objects)};
window.PLOTS_METADATA = {json.dumps(plots_metadata)};
window.SELECTED_OBJECT_TYPE = {json.dumps(st.session_state.selected_object)};
window.PLOT_WIDTH = {json.dumps(PLOT_WIDTH)};
window.PLOT_DEPTH = {json.dumps(PLOT_DEPTH)};
console.log("Streamlit State Injected:", {{ /* Basic logging */ }});
</script>
"""
html_content_with_state = html_template.replace('</head>', js_injection_script + '\n</head>', 1)
# Embed HTML Component
components.html( html_content_with_state, height=750, scrolling=False )
except FileNotFoundError: st.error(f"CRITICAL ERROR: Could not find file '{html_file_path}'.")
except Exception as e: st.exception(f"HTML prep/render error: {e}") |