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}")