utils/luzviewer.pyw

149 lines
6.0 KiB
Python

import configparser
import os.path
import sqlite3
import sys
import tkinter.filedialog as filedialog
import tkinter.messagebox as messagebox
from tkinter import END, Menu
import viewer
from pyraknet.bitstream import BitStream, c_float, c_int64, c_ubyte, c_uint, c_uint64, c_ushort
class LUZViewer(viewer.Viewer):
def __init__(self):
super().__init__()
config = configparser.ConfigParser()
config.read("luzviewer.ini")
try:
self.db = sqlite3.connect(config["paths"]["db_path"])
except:
messagebox.showerror("Can not open database", "Make sure db_path in the INI is set correctly.")
sys.exit()
self.create_widgets()
def create_widgets(self):
super().create_widgets()
menubar = Menu()
menubar.add_command(label="Open", command=self.askopenfile)
self.master.config(menu=menubar)
def askopenfile(self):
path = filedialog.askopenfilename(filetypes=[("LEGO Universe Zone", "*.luz")])
if path:
self.load_luz(path)
def load_luz(self, luz_path):
self.tree.set_children("")
print("Loading", luz_path)
with open(luz_path, "rb") as file:
stream = BitStream(file.read())
version = stream.read(c_uint)
assert version in (40, 41), version
unknown1 = stream.read(c_uint)
world_id = stream.read(c_uint)
spawnpoint_pos = stream.read(c_float), stream.read(c_float), stream.read(c_float)
spawnpoint_rot = stream.read(c_float), stream.read(c_float), stream.read(c_float), stream.read(c_float)
zone = self.tree.insert("", END, text="Zone", values=(version, unknown1, world_id, spawnpoint_pos, spawnpoint_rot))
### scenes
scenes = self.tree.insert(zone, END, text="Scenes")
for _ in range(stream.read(c_uint)):
filename = stream.read(str, char_size=1, length_type=c_ubyte)
scene_id = stream.read(c_uint)
is_audio = stream.read(c_uint)
scene_name = stream.read(str, char_size=1, length_type=c_ubyte)
if is_audio:
assert scene_name == "Audio"
scene = self.tree.insert(scenes, END, text="Scene", values=(filename, scene_id, is_audio, scene_name))
assert stream.read(bytes, length=3) == b"\xff\xff\xff"
with open(os.path.join(os.path.dirname(luz_path), filename), "rb") as lvl:
print("Loading lvl", filename)
self.parse_lvl(BitStream(lvl.read()), scene)
assert stream.read(c_ubyte) == 0
### terrain
filename = stream.read(str, char_size=1, length_type=c_ubyte)
name = stream.read(str, char_size=1, length_type=c_ubyte)
description = stream.read(str, char_size=1, length_type=c_ubyte)
self.tree.insert(zone, END, text="Terrain", values=(filename, name, description))
### unknown
unknowns = self.tree.insert(zone, END, text="Unknowns")
for _ in range(stream.read(c_uint)):
for _ in range(2):
unknown1 = stream.read(c_uint64)
unknown2 = stream.read(c_float), stream.read(c_float), stream.read(c_float)
self.tree.insert(unknowns, END, text="Unknown", values=(unknown1, unknown2))
remaining_length = stream.read(c_uint)
assert len(stream) - stream._read_offset//8 == remaining_length
assert stream.read(c_uint) == 1
def parse_lvl(self, stream, scene):
while not stream.all_read():
assert stream._read_offset//8 % 16 == 0 # seems everything is aligned like this?
start_pos = stream._read_offset//8
assert stream.read(bytes, length=4) == b"CHNK"
chunktype = stream.read(c_uint)
assert stream.read(c_ushort) == 1
assert stream.read(c_ushort) in (1, 2)
chunk_length = stream.read(c_uint)
data_pos = stream.read(c_uint)
stream._read_offset = data_pos * 8
assert stream._read_offset//8 % 16 == 0
if chunktype == 1000:
pass
elif chunktype == 2000:
pass
elif chunktype == 2001:
for _ in range(stream.read(c_uint)):
object_id = stream.read(c_int64) # seems like the object id, but without some bits
lot = stream.read(c_uint)
unknown1 = stream.read(c_uint)
unknown2 = stream.read(c_uint)
position = stream.read(c_float), stream.read(c_float), stream.read(c_float)
rotation = stream.read(c_float), stream.read(c_float), stream.read(c_float), stream.read(c_float)
scale = stream.read(c_float)
config_data = stream.read(str, length_type=c_uint)
config_data = config_data.replace("{", "<crlbrktopen>").replace("}", "<crlbrktclose>").replace("\\", "<backslash>") # for some reason these characters aren't properly escaped when sent to Tk
assert stream.read(c_uint) == 0
lot_name = ""
if lot == 176:
lot_name = "Spawner - "
lot = config_data[config_data.index("spawntemplate")+16:config_data.index("\n", config_data.index("spawntemplate")+16)]
try:
lot_name += self.db.execute("select name from Objects where id == "+str(lot)).fetchone()[0]
except TypeError:
print("Name for lot", lot, "not found")
lot_name += " - "+str(lot)
self.tree.insert(scene, END, text="Object", values=(object_id, lot_name, unknown1, unknown2, position, rotation, scale, config_data))
elif chunktype == 2002:
pass
stream._read_offset = (start_pos + chunk_length) * 8 # go to the next CHNK
def on_item_select(self, _):
item = self.tree.selection()[0]
item_type = self.tree.item(item, "text")
if item_type == "Zone":
cols = "version", "unknown1", "world_id", "spawnpoint_pos", "spawnpoint_rot"
elif item_type == "Scene":
cols = "filename", "scene_id", "is_audio", "scene_name"
elif item_type == "Terrain":
cols = "filename", "name", "description"
elif item_type == "Unknown":
cols = "unknown1", "unknown2"
elif item_type == "Object":
cols = "object_id", "lot", "unknown1", "unknown2", "position", "rotation", "scale"
else:
cols = ()
if cols:
self.tree.configure(columns=cols)
for col in cols:
self.tree.heading(col, text=col, command=(lambda col: lambda: self.sort_column(col, False))(col))
self.item_inspector.delete(1.0, END)
self.item_inspector.insert(END, "\n".join(self.tree.item(item, "values")))
if __name__ == "__main__":
app = LUZViewer()
app.mainloop()