import streamlit as st # 🌐 Streamlit magic import streamlit.components.v1 as components # 🖼️ Embed custom HTML/JS import os # 📂 File operations import json # 🔄 JSON encoding/decoding import pandas as pd # 📊 DataFrame handling import uuid # 🆔 Unique IDs import math # ➗ Math utils import time # ⏳ Time utilities from gamestate import GameState # 💼 Shared game-state singleton # 🚀 Page setup st.set_page_config(page_title="Infinite World Builder", layout="wide") # 📏 Constants for world dimensions & CSV schema SAVE_DIR = "saved_worlds" PLOT_WIDTH = 50.0 # ↔️ Plot width in world units PLOT_DEPTH = 50.0 # ↕️ Plot depth in world units CSV_COLUMNS = [ 'obj_id', 'type', 'pos_x', 'pos_y', 'pos_z', 'rot_x', 'rot_y', 'rot_z', 'rot_order' ] # 🗂️ Ensure directory for plots exists os.makedirs(SAVE_DIR, exist_ok=True) @st.cache_data(ttl=3600) # 🕒 Cache for 1h def load_plot_metadata(): # 🔍 Scan SAVE_DIR for plot CSVs try: plot_files = [f for f in os.listdir(SAVE_DIR) if f.endswith(".csv") and f.startswith("plot_X")] except FileNotFoundError: st.error(f"Folder '{SAVE_DIR}' missing! 🚨") return [] except Exception as e: st.error(f"Error reading '{SAVE_DIR}': {e}") return [] parsed = [] for fn in plot_files: try: parts = fn[:-4].split('_') # strip .csv gx = int(parts[1][1:]) # X index gz = int(parts[2][1:]) # Z index name = " ".join(parts[3:]) if len(parts)>3 else f"Plot({gx},{gz})" parsed.append({ 'id': fn[:-4], 'filename': fn, 'grid_x': gx, 'grid_z': gz, 'name': name, 'x_offset': gx * PLOT_WIDTH, 'z_offset': gz * PLOT_DEPTH }) except Exception: st.warning(f"Skip invalid file: {fn}") parsed.sort(key=lambda p: (p['grid_x'], p['grid_z'])) return parsed def load_plot_objects(filename, x_offset, z_offset): # 📥 Load objects from a plot CSV and shift by offsets path = os.path.join(SAVE_DIR, filename) try: df = pd.read_csv(path) # 🛡️ Ensure essentials exist if not all(c in df.columns for c in ['type','pos_x','pos_y','pos_z']): st.warning(f"Missing cols in {filename} 🧐") return [] # 🆔 Guarantee obj_id df['obj_id'] = df.get('obj_id', pd.Series([str(uuid.uuid4()) for _ in df.index])) # 🔄 Fill missing rotation for col, default in [('rot_x',0.0),('rot_y',0.0),('rot_z',0.0),('rot_order','XYZ')]: if col not in df.columns: df[col] = default objs = [] for _, row in df.iterrows(): o = row.to_dict() o['pos_x'] += x_offset o['pos_z'] += z_offset objs.append(o) return objs except FileNotFoundError: st.error(f"CSV not found: {filename}") return [] except pd.errors.EmptyDataError: return [] except Exception as e: st.error(f"Load error {filename}: {e}") return [] def save_plot_data(filename, objects_list, px, pz): # 💾 Save list of new objects relative to plot origin path = os.path.join(SAVE_DIR, filename) if not isinstance(objects_list, list): st.error("👎 Invalid data format for save") return False rel = [] for o in objects_list: pos = o.get('position', {}) rot = o.get('rotation', {}) typ = o.get('type','Unknown') oid = o.get('obj_id', str(uuid.uuid4())) # 🛑 Skip bad objects if not all(k in pos for k in ['x','y','z']) or typ=='Unknown': continue rel.append({ 'obj_id': oid, 'type': typ, 'pos_x': pos['x']-px, 'pos_y': pos['y'], 'pos_z': pos['z']-pz, 'rot_x': rot.get('_x',0.0), 'rot_y': rot.get('_y',0.0), 'rot_z': rot.get('_z',0.0), 'rot_order': rot.get('_order','XYZ') }) try: pd.DataFrame(rel, columns=CSV_COLUMNS).to_csv(path, index=False) st.success(f"🎉 Saved {len(rel)} to {filename}") return True except Exception as e: st.error(f"Save failed: {e}") return False # 🔒 Singleton for global world state @st.cache_resource def get_game_state(): return GameState(save_dir=SAVE_DIR, csv_filename="world_state.csv") game_state = get_game_state() # 🧠 Session state defaults st.session_state.setdefault('selected_object','None') st.session_state.setdefault('new_plot_name','') st.session_state.setdefault('js_save_data_result',None) # 🔄 Load everything plots_metadata = load_plot_metadata() all_initial_objects = [] for p in plots_metadata: all_initial_objects += load_plot_objects(p['filename'], p['x_offset'], p['z_offset']) # 🖥️ Sidebar UI with st.sidebar: st.title("🏗️ World Controls") st.header("📍 Navigate Plots") cols = st.columns(2) i = 0 for p in sorted(plots_metadata, key=lambda x:(x['grid_x'],x['grid_z'])): label = f"➡️ {p['name']} ({p['grid_x']},{p['grid_z']})" if cols[i].button(label, key=f"nav_{p['id']}"): try: from streamlit_js_eval import streamlit_js_eval js = f"teleportPlayer({p['x_offset']+PLOT_WIDTH/2},{p['z_offset']+PLOT_DEPTH/2});" streamlit_js_eval(js_code=js, key=f"tp_{p['id']}") except Exception as e: st.error(f"TP fail: {e}") i = (i+1)%2 st.markdown("---") st.header("🌲 Place Objects") opts = ["None","Simple House","Tree","Rock","Fence Post"] idx = opts.index(st.session_state.selected_object) if st.session_state.selected_object in opts else 0 sel = st.selectbox("Select:", opts, index=idx, key="selected_object_widget") if sel != st.session_state.selected_object: st.session_state.selected_object = sel st.markdown("---") st.header("💾 Save Work") if st.button("💾 Save Current Work", key="save_button"): from streamlit_js_eval import streamlit_js_eval streamlit_js_eval(js_code="getSaveDataAndPosition();", key="js_save_processor") st.rerun() # 📨 Handle incoming save data raw = st.session_state.get("js_save_processor") if raw: st.info("📬 Got save data!") ok=False try: pay = json.loads(raw) if isinstance(raw,str) else raw pos, objs = pay.get('playerPosition'), pay.get('objectsToSave') if isinstance(objs,list) and pos: gx, gz = math.floor(pos['x']/PLOT_WIDTH), math.floor(pos['z']/PLOT_DEPTH) fn = f"plot_X{gx}_Z{gz}.csv" if save_plot_data(fn, objs, gx*PLOT_WIDTH, gz*PLOT_DEPTH): load_plot_metadata.clear() try: from streamlit_js_eval import streamlit_js_eval streamlit_js_eval(js_code="resetNewlyPlacedObjects();", key="reset_js") except: pass game_state.update_state(objs) ok=True if not ok: st.error("❌ Save error") except Exception as e: st.error(f"Err: {e}") st.session_state.js_save_processor=None if ok: st.rerun() # 🏠 Main view st.header("🌍 Infinite Shared 3D World") st.caption("➡️ Explore, click to build, 💾 to save!") # 🔌 Inject state into JS state = { "ALL_INITIAL_OBJECTS": all_initial_objects, "PLOTS_METADATA": plots_metadata, "SELECTED_OBJECT_TYPE": st.session_state.selected_object, "PLOT_WIDTH": PLOT_WIDTH, "PLOT_DEPTH": PLOT_DEPTH, "GAME_STATE": game_state.get_state() } try: with open('index.html','r',encoding='utf-8') as f: html = f.read() script = f""" """ html = html.replace('', script + '\n', 1) components.html(html, height=750, scrolling=False) except FileNotFoundError: st.error("❌ index.html missing!") except Exception as e: st.error(f"😱 HTML inject failed: {e}")