Spaces:
Running
Running
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) | |
# ๐ Cache for 1h | |
def load_plot_metadata(): | |
"""Scan SAVE_DIR for existing plot CSVs and parse their grid coords.""" | |
try: | |
files = [f for f in os.listdir(SAVE_DIR) if f.endswith(".csv") and f.startswith("plot_X")] | |
except FileNotFoundError: | |
return [] | |
parsed = [] | |
for fn in files: | |
parts = fn[:-4].split('_') | |
try: | |
gx = int(parts[1][1:]) | |
gz = int(parts[2][1:]) | |
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: | |
continue | |
parsed.sort(key=lambda p: (p['grid_x'], p['grid_z'])) | |
return parsed | |
def load_plot_objects(filename, x_offset, z_offset): | |
"""Read a plot CSV and shift each object's position by the plot offset.""" | |
path = os.path.join(SAVE_DIR, filename) | |
try: | |
df = pd.read_csv(path) | |
except (FileNotFoundError, pd.errors.EmptyDataError): | |
return [] | |
# ensure columns | |
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 | |
if 'obj_id' not in df.columns: | |
df['obj_id'] = [str(uuid.uuid4()) for _ in df.index] | |
objs = [] | |
for row in df.to_dict(orient='records'): | |
objs.append({ | |
'obj_id': row['obj_id'], | |
'type': row['type'], | |
'pos_x': row['pos_x'] + x_offset, | |
'pos_y': row['pos_y'], | |
'pos_z': row['pos_z'] + z_offset, | |
'rot_x': row['rot_x'], | |
'rot_y': row['rot_y'], | |
'rot_z': row['rot_z'], | |
'rot_order': row['rot_order'] | |
}) | |
return objs | |
def save_plot_data(filename, objects_list, px, pz): | |
"""Save newly placed objects back to the corresponding plot CSV.""" | |
path = os.path.join(SAVE_DIR, filename) | |
records = [] | |
for o in objects_list: | |
pos = o.get('position', {}) | |
rot = o.get('rotation', {}) | |
if 'x' not in pos or 'y' not in pos or 'z' not in pos: | |
continue | |
records.append({ | |
'obj_id': o.get('obj_id', str(uuid.uuid4())), | |
'type': o.get('type', 'Unknown'), | |
'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(records, columns=CSV_COLUMNS).to_csv(path, index=False) | |
st.success(f"๐ Saved {len(records)} objects to {filename}") | |
return True | |
except Exception as e: | |
st.error(f"Save failed: {e}") | |
return False | |
# ๐ Singleton for global world state | |
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('js_save_processor', None) | |
# ๐ Load all saved plots + objects | |
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) | |
for idx, p in enumerate(plots_metadata): | |
label = f"โก๏ธ {p['name']} ({p['grid_x']},{p['grid_z']})" | |
if cols[idx % 2].button(label, key=f"nav_{p['id']}"): | |
from streamlit_js_eval import streamlit_js_eval | |
js = f"teleportPlayer({p['x_offset']+PLOT_WIDTH/2},{p['z_offset']+PLOT_DEPTH/2});" | |
try: | |
streamlit_js_eval(js_code=js, key=f"tp_{p['id']}") | |
except Exception as e: | |
st.error(f"Teleport failed: {e}") | |
st.markdown("---") | |
st.header("๐ฒ Place Objects") | |
# โโโโ four new builder-tool options added here โโโโโ | |
options = [ | |
"None", | |
"Simple House", | |
"Tree", | |
"Rock", | |
"Fence Post", | |
"Cyberpunk City Builder Kit", | |
"POLYGON - Fantasy Kingdom Pack", | |
"HEROIC FANTASY CREATURES FULL PACKย VOLย 1", | |
"POLYGON - Apocalypse Pack" | |
] | |
sel = st.selectbox("Select:", options, | |
index=options.index(st.session_state.selected_object) | |
if st.session_state.selected_object in options else 0, | |
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"): | |
from streamlit_js_eval import streamlit_js_eval | |
streamlit_js_eval(js_code="getSaveDataAndPosition();", key="js_save_processor") | |
st.experimental_rerun() | |
# ๐จ Handle incoming save-data callback | |
raw = st.session_state.get("js_save_processor") | |
if raw: | |
try: | |
payload = json.loads(raw) if isinstance(raw, str) else raw | |
pos = payload.get('playerPosition') | |
objs = payload.get('objectsToSave', []) | |
gx = math.floor(pos['x'] / PLOT_WIDTH) | |
gz = 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() | |
st.session_state.js_save_processor = None | |
st.experimental_rerun() | |
except Exception as e: | |
st.error(f"Error saving data: {e}") | |
# ๐ Main view & HTML injection | |
st.header("๐ Infinite Shared 3D World") | |
st.caption("โก๏ธ Explore, click to build, ๐พ to save!") | |
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() | |
inject = f""" | |
<script> | |
window.ALL_INITIAL_OBJECTS = {json.dumps(state["ALL_INITIAL_OBJECTS"])}; | |
window.PLOTS_METADATA = {json.dumps(state["PLOTS_METADATA"])}; | |
window.SELECTED_OBJECT_TYPE = {json.dumps(state["SELECTED_OBJECT_TYPE"])}; | |
window.PLOT_WIDTH = {json.dumps(state["PLOT_WIDTH"])}; | |
window.PLOT_DEPTH = {json.dumps(state["PLOT_DEPTH"])}; | |
window.GAME_STATE = {json.dumps(state["GAME_STATE"])}; | |
</script> | |
""" | |
html = html.replace("</head>", inject + "\n</head>", 1) | |
components.html(html, height=750, scrolling=False) | |
except FileNotFoundError: | |
st.error("โ index.html not found!") | |
except Exception as e: | |
st.error(f"HTML injection failed: {e}") | |