File size: 15,191 Bytes
41c1a97
 
 
 
 
50498a4
41c1a97
50498a4
225f7b1
dfe769e
41c1a97
 
50498a4
 
 
dfe769e
50498a4
 
225f7b1
50498a4
225f7b1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50498a4
225f7b1
 
 
50498a4
225f7b1
50498a4
225f7b1
50498a4
225f7b1
50498a4
 
225f7b1
50498a4
225f7b1
 
41c1a97
 
225f7b1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50498a4
 
 
 
 
 
 
 
 
225f7b1
50498a4
 
 
225f7b1
50498a4
 
 
 
 
225f7b1
 
50498a4
 
225f7b1
 
 
 
 
50498a4
 
225f7b1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50498a4
 
 
 
225f7b1
50498a4
 
 
 
 
 
 
 
 
225f7b1
50498a4
 
 
 
 
 
 
225f7b1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41c1a97
50498a4
 
 
225f7b1
41c1a97
225f7b1
41c1a97
 
225f7b1
41c1a97
225f7b1
f32aedf
 
41c1a97
50498a4
 
7825ef7
 
 
 
 
225f7b1
d93d024
50498a4
 
d93d024
7825ef7
225f7b1
f32aedf
14b289d
dfe769e
7825ef7
f32aedf
 
 
 
50498a4
 
80d0af3
d93d024
f32aedf
14b289d
f32aedf
7825ef7
 
 
 
 
225f7b1
7825ef7
f32aedf
7825ef7
f32aedf
7825ef7
 
 
225f7b1
7825ef7
 
 
 
f32aedf
225f7b1
50498a4
14b289d
 
 
7825ef7
f32aedf
 
dfe769e
7825ef7
14b289d
 
dfe769e
7825ef7
f32aedf
 
 
 
225f7b1
f32aedf
225f7b1
f32aedf
d93d024
f32aedf
50498a4
 
 
f32aedf
 
225f7b1
f32aedf
 
225f7b1
d93d024
50498a4
7825ef7
 
f32aedf
225f7b1
7825ef7
 
 
dfe769e
225f7b1
dfe769e
50498a4
dfe769e
 
7825ef7
 
 
50498a4
225f7b1
7825ef7
 
 
14b289d
7825ef7
 
 
 
 
 
 
225f7b1
f32aedf
225f7b1
7825ef7
 
f32aedf
225f7b1
7825ef7
 
 
 
50498a4
14b289d
7825ef7
 
 
 
14b289d
f32aedf
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
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
# 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 adding slight delays if needed for debugging FS issues
from streamlit_js_eval import streamlit_js_eval # For JS communication

# --- Constants ---
DB_FILE = "world_state.db" # SQLite database file
PLOT_WIDTH = 50.0
PLOT_DEPTH = 50.0

# --- Database Setup ---
def init_db():
    """Initializes the SQLite database and tables."""
    try:
        # Use WAL mode for potentially better concurrency, though might have visibility delays on some systems
        # conn = sqlite3.connect(DB_FILE, isolation_level=None) # Auto-commit mode
        # cursor.execute('PRAGMA journal_mode=WAL;')
        # conn.close()

        with sqlite3.connect(DB_FILE) as conn: # Use context manager
            cursor = conn.cursor()
            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)
                )
            ''')
            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)')
            # No explicit commit needed with 'with' statement unless changes made
        print(f"Database {os.path.abspath(DB_FILE)} initialized/checked.") # Print to console
    except sqlite3.Error as e:
        # Use st.exception for full traceback in the app UI
        st.exception(f"CRITICAL DATABASE INIT ERROR: {e}")
        st.stop()

init_db() # Ensure DB exists and has tables

# --- Helper Functions (Database Operations - Using 'with') ---

# No Caching - always load fresh from DB
def load_world_state_from_db():
    """Loads all plot metadata and object data fresh from the SQLite DB."""
    st.write(f"Executing load_world_state_from_db for session {st.runtime.scriptrunner.get_script_run_ctx().session_id[:5]}...") # Debug session ID
    plots_metadata = []
    all_objects_by_plot = {}
    all_initial_objects_world = []

    try:
        with sqlite3.connect(DB_FILE) as conn: # Auto-closes connection
            conn.row_factory = sqlite3.Row
            cursor = conn.cursor()

            st.write("DB Read: Loading plot metadata...")
            cursor.execute("SELECT grid_x, grid_z, name, strftime('%Y-%m-%d %H:%M:%S', last_updated) as updated_ts FROM plots ORDER BY grid_x, grid_z")
            plot_rows = cursor.fetchall()
            st.write(f"DB Read: Found {len(plot_rows)} plot rows.")

            for row in plot_rows:
                gx, gz = row['grid_x'], row['grid_z']
                plot_meta = {
                    '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,
                    'last_updated': row['updated_ts'] # Add timestamp for debugging
                }
                plots_metadata.append(plot_meta)
                all_objects_by_plot[(gx, gz)] = []

            st.write("DB Read: Loading all objects...")
            cursor.execute("SELECT * FROM objects")
            object_rows = cursor.fetchall()
            st.write(f"DB Read: Found {len(object_rows)} object rows.")

            # Group objects by plot
            for row in object_rows:
                plot_key = (row['plot_grid_x'], row['plot_grid_z'])
                if plot_key in all_objects_by_plot:
                     all_objects_by_plot[plot_key].append(dict(row))

        # Combine and calculate world coordinates (outside DB connection)
        for plot_meta in plots_metadata:
            plot_key = (plot_meta['grid_x'], plot_meta['grid_z'])
            for obj_data in all_objects_by_plot[plot_key]:
                 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']
                 all_initial_objects_world.append(world_obj_data)

        st.write(f"DB Load Complete: {len(plots_metadata)} plots, {len(all_initial_objects_world)} total objects processed.")

    except sqlite3.Error as e:
        st.error(f"Database load error: {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"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
    plot_name = f"Plot ({target_grid_x},{target_grid_z})" # Simple default name for upsert

    if not isinstance(objects_data_list, list):
        st.error("Save Error: Invalid object data format (expected list).")
        return False

    try:
        with sqlite3.connect(DB_FILE) as conn: # Auto commit/rollback/close
            cursor = conn.cursor()
            # Use savepoint for finer transaction control within 'with' if needed, but default is fine
            # cursor.execute('BEGIN') # Implicitly handled by 'with' unless isolation_level=None

            # 1. Upsert Plot - ensure plot exists and update timestamp
            st.write(f"DB Save: Upserting plot ({target_grid_x},{target_grid_z})...")
            cursor.execute('''
                INSERT INTO plots (grid_x, grid_z, name) VALUES (?, ?, ?)
                ON CONFLICT(grid_x, grid_z) DO UPDATE SET name = excluded.name, last_updated = CURRENT_TIMESTAMP
            ''', (target_grid_x, target_grid_z, plot_name))
            st.write(f"DB Save: Plot upserted.")


            # 2. Delete ALL existing objects for this specific plot
            st.write(f"DB Save: Deleting old objects for plot ({target_grid_x},{target_grid_z})...")
            cursor.execute("DELETE FROM objects WHERE plot_grid_x = ? AND plot_grid_z = ?", (target_grid_x, target_grid_z))
            st.write(f"DB Save: Deleted {cursor.rowcount} old objects.")

            # 3. Insert the new objects
            insert_count = 0
            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':
                    print(f"Skipping malformed object during DB save prep: {obj}")
                    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) # Y is absolute

                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')
                ))
                insert_count += 1

            if objects_to_insert:
                st.write(f"DB Save: Inserting {insert_count} new objects...")
                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 to handle potential obj_id conflicts cleanly
                st.write(f"DB Save: Objects inserted.")
            else:
                 st.write(f"DB Save: No new objects to insert.")

            # 'with' statement handles commit on success, rollback on error

        st.success(f"DB Save Commit: Plot ({target_grid_x},{target_grid_z}) saved with {insert_count} objects.")
        # Add a tiny delay AFTER commit, maybe helps filesystem cache? (Experimental)
        # time.sleep(0.1)
        return True

    except sqlite3.Error as e:
        st.error(f"Database save error for plot ({target_grid_x},{target_grid_z}): {e}")
        st.exception(e) # Show full traceback
        return False


# --- Page Config ---
st.set_page_config( page_title="DB Synced World Builder v2", 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

# --- Load World State From DB (Runs on every script execution/rerun) ---
plots_metadata, all_initial_objects = load_world_state_from_db()

# --- Sidebar ---
with st.sidebar:
    st.title("🏗️ World Controls")

    # Refresh Button (Just triggers rerun, load function always hits DB)
    if st.button("🔄 Refresh World View", key="refresh_button"):
        st.info("Reloading world state from database...")
        st.rerun()

    st.header("Navigation (Plots)")
    # ... (Navigation button code remains the same as previous version) ...
    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:
                js_code = f"teleportPlayer({target_x}, {target_z});"
                streamlit_js_eval(js_code=js_code, 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 code remains the same) ...
    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
         # Rerun updates injection, sessionStorage persists JS side

    st.markdown("---")

    # --- Saving ---
    st.header("Save Work")
    st.caption("Saves ALL objects currently within the player's 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.info("Processing save request...")
    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): # Allow saving empty list (clears plot)
                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)

                st.write(f"Attempting DB save for plot ({target_grid_x},{target_grid_z})")
                # --- 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:
                    # No cache clear needed. Success message is inside save_plot_data_to_db
                    save_processed_successfully = True
                else:
                    st.error(f"Failed DB save for plot ({target_grid_x},{target_grid_z}).")
            else: st.error("Invalid 'objectsToSave' format (expected list).")
        else: st.error("Invalid save payload structure received.")

    except json.JSONDecodeError:
        st.error("Failed to decode save data from client.")
        print("Received raw data:", save_data_from_js) # Log raw data
    except Exception as e:
        st.error(f"Error processing save: {e}")
        st.exception(e)

    # Clear the trigger data from session state ALWAYS after attempting processing
    st.session_state.js_save_processor = None
    # Rerun after processing save to reload world state from DB
    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' to see updates.")

# --- 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()

    js_injection_script = f"""
<script>
    // Inject data loaded fresh from DB
    window.ALL_INITIAL_OBJECTS = {json.dumps(all_initial_objects)};
    window.PLOTS_METADATA = {json.dumps(plots_metadata)}; // Still useful for JS ground generation
    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:", {{ /* ... 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 the file '{html_file_path}'.")
except Exception as e:
    st.error(f"An critical error occurred during HTML prep/render: {e}")
    st.exception(e)