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' ] # 🎨 Model categories and types MODEL_CATEGORIES = { "None": ["None"], "Buildings": [ "Simple House", "Cyberpunk Wall Panel", "Modular Hab Block", "MegaCorp Skyscraper", "Castle Wall Section", "Wooden Door", "House Roof Section", "Concrete Bunker Wall", "Damaged House Facade" ], "Nature": [ "Tree", "Rock", "Pine Tree", "Boulder", "Alien Plant", "Floating Rock Platform", "Rubble Pile" ], "Props": [ "Fence Post", "Rooftop AC Unit", "Holographic Window Display", "Jersey Barrier", "Oil Drum", "Canned Food", "Treasure Chest", "Wall Torch", "Bone Pile" ], "Characters": [ "King Figure", "Soldier Figure", "Mage Figure", "Zombie Figure", "Survivor Figure", "Dwarf Miner Figure", "Undead Knight Figure", "Hero Figure" ], "Vehicles": [ "Wooden Cart", "Ballista", "Siege Tower", "Buggy Frame", "Motorbike", "Hover Bike", "APC", "Sand Boat" ], "Weapons": [ "Makeshift Machete", "Pistol Body", "Scope Attachment", "Laser Pistol", "Energy Sword", "Dwarven Axe", "Magic Staff" ], "Effects": [ "Candle Flame", "Dust Cloud", "Blood Splat Decal", "Burning Barrel Fire", "Warp Tunnel Effect", "Laser Beam", "Gold Sparkle", "Steam Vent" ] } # 🗂️ 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('selected_category','None') st.session_state.setdefault('new_plot_name','') st.session_state.setdefault('js_save_data_result',None) st.session_state.setdefault('custom_scale', 1.0) st.session_state.setdefault('custom_rotation_y', 0) # 🔄 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") # 📂 Category selector category_list = list(MODEL_CATEGORIES.keys()) selected_category = st.selectbox( "Category:", category_list, index=category_list.index(st.session_state.selected_category) if st.session_state.selected_category in category_list else 0, key="selected_category_widget" ) if selected_category != st.session_state.selected_category: st.session_state.selected_category = selected_category if st.session_state.selected_category != "None": # Default to first item in the new category st.session_state.selected_object = MODEL_CATEGORIES[selected_category][0] # 🖼️ Object selector within the category if selected_category in MODEL_CATEGORIES: object_list = MODEL_CATEGORIES[selected_category] current_object = st.session_state.selected_object if current_object not in object_list: current_object = object_list[0] selected_object = st.selectbox( "Select:", object_list, index=object_list.index(current_object) if current_object in object_list else 0, key="selected_object_widget" ) if selected_object != st.session_state.selected_object: st.session_state.selected_object = selected_object # ↕️ Scale slider (not shown for the None selection) if st.session_state.selected_object != "None": st.session_state.custom_scale = st.slider( "Scale:", min_value=0.2, max_value=3.0, value=st.session_state.custom_scale, step=0.1, key="scale_slider" ) # 🔄 Rotation slider (Y-axis) st.session_state.custom_rotation_y = st.slider( "Rotation:", min_value=0, max_value=359, value=st.session_state.custom_rotation_y, step=15, key="rotation_slider" ) 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() # 📝 New plot naming with st.expander("Create Named Plot"): st.text_input("New Plot Name:", key="new_plot_name") if st.button("Create at Current Position"): if st.session_state.new_plot_name.strip(): from streamlit_js_eval import streamlit_js_eval streamlit_js_eval(js_code="getSaveDataForNamedPlot();", key="named_plot_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() # 📨 Handle named plot data named_raw = st.session_state.get("named_plot_processor") if named_raw: st.info("📬 Got data for named plot!") ok=False try: pay = json.loads(named_raw) if isinstance(named_raw,str) else named_raw pos, objs = pay.get('playerPosition'), pay.get('objectsToSave') if isinstance(objs,list) and pos and st.session_state.new_plot_name.strip(): gx, gz = math.floor(pos['x']/PLOT_WIDTH), math.floor(pos['z']/PLOT_DEPTH) name = st.session_state.new_plot_name.strip().replace(' ', '_') fn = f"plot_X{gx}_Z{gz}_{name}.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 st.session_state.new_plot_name = "" if not ok: st.error("❌ Named plot save error") except Exception as e: st.error(f"Named plot err: {e}") st.session_state.named_plot_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, "CUSTOM_SCALE": st.session_state.custom_scale, "CUSTOM_ROTATION_Y": st.session_state.custom_rotation_y * (math.pi / 180), # Convert to radians "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}")