Fixed crash
All checks were successful
Build Dusk / build-linux (push) Successful in 51s
Build Dusk / build-psp (push) Successful in 56s

This commit is contained in:
2025-11-16 17:24:54 -06:00
parent 1b741a81e5
commit ae941a0fdb
12 changed files with 215 additions and 225 deletions

View File

@@ -25,9 +25,9 @@ class Camera:
(self.pixelsPerUnit * self.scale) * math.tan(math.radians(self.fov / 2.0)) (self.pixelsPerUnit * self.scale) * math.tan(math.radians(self.fov / 2.0))
) )
lookAt = [ lookAt = [
self.parent.parent.map.position[0] * TILE_WIDTH, self.parent.map.position[0] * TILE_WIDTH,
self.parent.parent.map.position[1] * TILE_HEIGHT, self.parent.map.position[1] * TILE_HEIGHT,
self.parent.parent.map.position[2] * TILE_DEPTH, self.parent.map.position[2] * TILE_DEPTH,
] ]
aspectRatio = vw / vh aspectRatio = vw / vh

View File

@@ -1,14 +1,53 @@
from editortool.map.chunk import Tile import json
import os
from dusk.event import Event
class Chunk: class Chunk:
def __init__(self, x=0, y=0, z=0, size_x=1, size_y=1, size_z=1): def __init__(self, map, x, y, z):
self.map = map
self.x = x self.x = x
self.y = y self.y = y
self.z = z self.z = z
self.data = [[[Tile() for _ in range(size_z)] for _ in range(size_y)] for _ in range(size_x)] self.current = {}
self.original = {}
self.onChunkData = Event()
def draw(self): def load(self):
for x in range(len(self.data)): fname = self.getFilename()
for y in range(len(self.data[x])): if not fname or not os.path.exists(fname):
for z in range(len(self.data[x][y])): self.new()
self.data[x][y][z].draw(x, y, z) return
try:
with open(fname, 'r') as f:
self.current = json.load(f)
self.original = json.loads(json.dumps(self.current)) # Deep copy
self.onChunkData.invoke(self.current)
except Exception as e:
raise RuntimeError(f"Failed to load chunk file: {e}")
def save(self):
fname = self.getFilename()
if not fname:
raise ValueError("No filename specified for saving chunk.")
try:
with open(fname, 'w') as f:
json.dump(self.current, f, indent=2)
self.original = json.loads(json.dumps(self.current)) # Deep copy
except Exception as e:
raise RuntimeError(f"Failed to save chunk file: {e}")
def new(self):
self.current = {}
self.original = {}
self.onChunkData.invoke(self.current)
def isDirty(self):
return json.dumps(self.current, sort_keys=True) != json.dumps(self.original, sort_keys=True)
def getFilename(self):
if not self.map or not hasattr(self.map, 'getMapDirectory'):
return None
dir_path = self.map.getMapDirectory()
if dir_path is None:
return None
return f"{dir_path}/{self.x}_{self.y}_{self.z}.json"

View File

@@ -1,53 +0,0 @@
import json
import os
from dusk.event import Event
class ChunkData:
def __init__(self, mapData, x, y, z):
self.mapData = mapData
self.x = x
self.y = y
self.z = z
self.current = {}
self.original = {}
self.onChunkData = Event()
def load(self):
fname = self.getFilename()
if not fname or not os.path.exists(fname):
self.new()
return
try:
with open(fname, 'r') as f:
self.current = json.load(f)
self.original = json.loads(json.dumps(self.current)) # Deep copy
self.onChunkData.invoke(self.current)
except Exception as e:
raise RuntimeError(f"Failed to load chunk file: {e}")
def save(self):
fname = self.getFilename()
if not fname:
raise ValueError("No filename specified for saving chunk.")
try:
with open(fname, 'w') as f:
json.dump(self.current, f, indent=2)
self.original = json.loads(json.dumps(self.current)) # Deep copy
except Exception as e:
raise RuntimeError(f"Failed to save chunk file: {e}")
def new(self):
self.current = {}
self.original = {}
self.onChunkData.invoke(self.current)
def isDirty(self):
return json.dumps(self.current, sort_keys=True) != json.dumps(self.original, sort_keys=True)
def getFilename(self):
if not self.mapData or not hasattr(self.mapData, 'getMapDirectory'):
return None
dir_path = self.mapData.getMapDirectory()
if dir_path is None:
return None
return f"{dir_path}/{self.x}_{self.y}_{self.z}.json"

View File

@@ -36,7 +36,11 @@ class ChunkPanel(QWidget):
QTreeWidgetItem(parentItem2, ["Tile C"]) QTreeWidgetItem(parentItem2, ["Tile C"])
QTreeWidgetItem(parentItem2, ["Tile D"]) QTreeWidgetItem(parentItem2, ["Tile D"])
self.tree.expandAll() # Expand by default, remove if you want collapsed self.tree.expandAll() # Expand by default, remove if you want collapsed
layout.addWidget(self.tree, 1) # Give tree stretch factor so it expands layout.addWidget(self.tree) # Removed invalid stretch factor
# Add stretch so tree expands
layout.setStretchFactor(grid, 0)
layout.setStretchFactor(self.tree, 1)
# Event subscriptions # Event subscriptions
self.btnN.clicked.connect(lambda: self.parent.map.moveRelative(0, -1, 0)) self.btnN.clicked.connect(lambda: self.parent.map.moveRelative(0, -1, 0))

View File

@@ -2,9 +2,6 @@ from PyQt5.QtCore import QTimer
from PyQt5.QtWidgets import QOpenGLWidget from PyQt5.QtWidgets import QOpenGLWidget
from OpenGL.GL import * from OpenGL.GL import *
from OpenGL.GLU import * from OpenGL.GLU import *
from editortool.map.selectbox import SelectBox
from editortool.map.camera import Camera
from editortool.map.grid import Grid
class GLWidget(QOpenGLWidget): class GLWidget(QOpenGLWidget):
def __init__(self, parent): def __init__(self, parent):
@@ -13,9 +10,6 @@ class GLWidget(QOpenGLWidget):
self.timer = QTimer(self) self.timer = QTimer(self)
self.timer.timeout.connect(self.update) self.timer.timeout.connect(self.update)
self.timer.start(16) # ~60 FPS self.timer.start(16) # ~60 FPS
self.camera = Camera(self)
self.grid = Grid()
self.selectBox = SelectBox(self)
def initializeGL(self): def initializeGL(self):
glClearColor(0.392, 0.584, 0.929, 1.0) glClearColor(0.392, 0.584, 0.929, 1.0)
@@ -36,7 +30,6 @@ class GLWidget(QOpenGLWidget):
w = 1 w = 1
glViewport(0, 0, w, h) glViewport(0, 0, w, h)
self.camera.setup(w, h) self.parent.camera.setup(w, h)
self.parent.grid.draw()
self.grid.draw() self.parent.selectBox.draw()
self.selectBox.draw()

View File

@@ -1,15 +1,135 @@
import json
from dusk.event import Event from dusk.event import Event
from editortool.map.mapdata import MAP_WIDTH, MAP_HEIGHT, MAP_DEPTH from PyQt5.QtWidgets import QFileDialog, QMessageBox
from PyQt5.QtCore import QTimer
import os
from editortool.map.chunk import Chunk
from editortool.map.mapdefs import MAP_WIDTH, MAP_HEIGHT, MAP_DEPTH
import traceback
MAP_DEFAULT_PATH = os.path.join(os.path.dirname(__file__), '../../../assets/map/')
EDITOR_CONFIG_PATH = os.path.join(os.path.dirname(__file__), '.editor')
class Map: class Map:
def __init__(self, parent): def __init__(self, parent):
self.parent = parent self.parent = parent
self.data = {}
self.dataOriginal = {}
self.position = [0, 0, 0] # x, y, z self.position = [0, 0, 0] # x, y, z
self.positionEvent = Event() self.chunks = {}
self.onMapData = Event()
self.onPositionChange = Event()
self.mapFileName = None
self.lastFile = None
index = 0
for x in range(MAP_WIDTH):
for y in range(MAP_HEIGHT):
for z in range(MAP_DEPTH):
self.chunks[index] = Chunk(self, x, y, z)
index += 1
QTimer.singleShot(16, self.loadLastFile)
def loadLastFile(self):
if not os.path.exists(EDITOR_CONFIG_PATH):
return
try:
with open(EDITOR_CONFIG_PATH, 'r') as f:
config = json.load(f)
lastFile = config.get('lastFile')
lastPosition = config.get('lastPosition')
if lastFile and os.path.exists(lastFile):
self.load(lastFile)
if lastPosition and isinstance(lastPosition, list) and len(lastPosition) == 3:
self.moveTo(*lastPosition)
except Exception:
traceback.print_exc()
def updateEditorConfig(self):
print("Updating editor config...")
try:
mapFileName = self.getMapFilename()
config = {
'lastFile': mapFileName if mapFileName else "",
'lastPosition': self.position
}
config_dir = os.path.dirname(EDITOR_CONFIG_PATH)
if not os.path.exists(config_dir):
os.makedirs(config_dir, exist_ok=True)
with open(EDITOR_CONFIG_PATH, 'w') as f:
json.dump(config, f, indent=2)
except Exception:
traceback.print_exc()
def newFile(self):
self.data = {}
self.dataOriginal = {}
self.mapFileName = None
self.lastFile = None
for chunk in self.chunks.values():
chunk.new()
self.moveTo(0, 0, 0)
self.onMapData.invoke(self.data)
self.updateEditorConfig()
def save(self, fname=None):
if not self.getMapFilename() and fname is None:
filePath, _ = QFileDialog.getSaveFileName(None, "Save Map File", MAP_DEFAULT_PATH, "Map Files (*.json)")
if not filePath:
return
self.mapFileName = filePath
if fname:
self.mapFileName = fname
try:
with open(self.getMapFilename(), 'w') as f:
json.dump(self.data, f, indent=2)
self.dataOriginal = json.loads(json.dumps(self.data)) # Deep copy
for chunk in self.chunks.values():
chunk.save()
self.updateEditorConfig()
except Exception as e:
traceback.print_exc()
QMessageBox.critical(None, "Save Error", f"Failed to save map file:\n{e}")
def load(self, fileName):
try:
with open(fileName, 'r') as f:
self.data = json.load(f)
self.mapFileName = fileName
self.dataOriginal = json.loads(json.dumps(self.data)) # Deep copy
for chunk in self.chunks.values():
chunk.load()
self.onMapData.invoke(self.data)
self.updateEditorConfig()
except Exception as e:
traceback.print_exc()
QMessageBox.critical(None, "Load Error", f"Failed to load map file:\n{e}")
def isMapFileDirty(self):
return json.dumps(self.data, sort_keys=True) != json.dumps(self.dataOriginal, sort_keys=True)
def isDirty(self):
return self.isMapFileDirty() or self.anyChunksDirty()
def getMapFilename(self):
return self.mapFileName if self.mapFileName and os.path.exists(self.mapFileName) else None
def getMapDirectory(self):
fname = self.getMapFilename()
if not fname or not fname.endswith('.json'):
return None
return fname[:-5] # Remove '.json' extension
def anyChunksDirty(self):
for chunk in self.chunks.values():
if chunk.isDirty():
return True
return False
def moveTo(self, x, y, z): def moveTo(self, x, y, z):
self.position = [x, y, z] self.position = [x, y, z]
self.positionEvent.invoke(self.position) self.onPositionChange.invoke(self.position)
self.updateEditorConfig()
def moveRelative(self, x, y, z): def moveRelative(self, x, y, z):
self.moveTo( self.moveTo(

View File

@@ -1,109 +0,0 @@
import json
from dusk.event import Event
from PyQt5.QtWidgets import QFileDialog, QMessageBox
from PyQt5.QtCore import QTimer
import os
from editortool.map.chunkdata import ChunkData
from editortool.map.mapdefs import MAP_WIDTH, MAP_HEIGHT, MAP_DEPTH
import traceback
MAP_DEFAULT_PATH = os.path.join(os.path.dirname(__file__), '../../../assets/map/')
EDITOR_CONFIG_PATH = os.path.join(os.path.dirname(__file__), '.editor')
class MapData:
def __init__(self):
self.data = {}
self.dataOriginal = {}
self.onMapData = Event()
self.mapFileName = None
self.chunks = {}
index = 0
for x in range(MAP_WIDTH):
for y in range(MAP_HEIGHT):
for z in range(MAP_DEPTH):
self.chunks[index] = ChunkData(self, x, y, z)
index += 1
QTimer.singleShot(16, self.loadLastFile)
def loadLastFile(self):
if os.path.exists(EDITOR_CONFIG_PATH):
try:
with open(EDITOR_CONFIG_PATH, 'r') as f:
config = json.load(f)
lastFile = config.get('lastFile')
if lastFile and os.path.exists(lastFile):
self.load(lastFile)
except Exception:
pass
def updateEditorConfig(self):
try:
config = {'lastFile': self.getMapFilename() if self.getMapFilename() else ""}
with open(EDITOR_CONFIG_PATH, 'w') as f:
json.dump(config, f, indent=2)
except Exception:
pass
def newFile(self):
self.data = {}
self.dataOriginal = {}
self.mapFileName = None
for chunk in self.chunks.values():
chunk.new()
self.onMapData.invoke(self.data)
self.updateEditorConfig()
def save(self, fname=None):
if not self.getMapFilename() and fname is None:
filePath, _ = QFileDialog.getSaveFileName(None, "Save Map File", MAP_DEFAULT_PATH, "Map Files (*.json)")
if not filePath:
return
self.mapFileName = filePath
if fname:
self.mapFileName = fname
try:
with open(self.getMapFilename(), 'w') as f:
json.dump(self.data, f, indent=2)
self.dataOriginal = json.loads(json.dumps(self.data)) # Deep copy
for chunk in self.chunks.values():
chunk.save()
self.updateEditorConfig()
except Exception as e:
traceback.print_exc()
QMessageBox.critical(None, "Save Error", f"Failed to save map file:\n{e}")
def load(self, fileName):
try:
with open(fileName, 'r') as f:
self.data = json.load(f)
self.mapFileName = fileName
self.dataOriginal = json.loads(json.dumps(self.data)) # Deep copy
for chunk in self.chunks.values():
chunk.load()
self.onMapData.invoke(self.data)
self.updateEditorConfig()
except Exception as e:
traceback.print_exc()
QMessageBox.critical(None, "Load Error", f"Failed to load map file:\n{e}")
def isMapFileDirty(self):
return json.dumps(self.data, sort_keys=True) != json.dumps(self.dataOriginal, sort_keys=True)
def isDirty(self):
return self.isMapFileDirty() or self.anyChunksDirty()
def getMapFilename(self):
return self.mapFileName if self.mapFileName else None
def getMapDirectory(self):
fname = self.getMapFilename()
if not fname or not fname.endswith('.json'):
return None
return fname[:-5] # Remove '.json' extension
def anyChunksDirty(self):
for chunk in self.chunks.values():
if chunk.isDirty():
return True
return False

View File

@@ -43,8 +43,8 @@ class MapInfoPanel(QWidget):
self.tileXInput.returnPressed.connect(self.goToPosition) self.tileXInput.returnPressed.connect(self.goToPosition)
self.tileYInput.returnPressed.connect(self.goToPosition) self.tileYInput.returnPressed.connect(self.goToPosition)
self.tileZInput.returnPressed.connect(self.goToPosition) self.tileZInput.returnPressed.connect(self.goToPosition)
self.parent.map.positionEvent.sub(self.updatePositionLabels) self.parent.map.onPositionChange.sub(self.updatePositionLabels)
self.parent.mapData.onMapData.sub(self.onMapData) self.parent.map.onMapData.sub(self.onMapData)
self.mapTitleInput.textChanged.connect(self.onMapNameChange) self.mapTitleInput.textChanged.connect(self.onMapNameChange)
# Initial label setting # Initial label setting
@@ -55,7 +55,7 @@ class MapInfoPanel(QWidget):
x = int(self.tileXInput.text()) x = int(self.tileXInput.text())
y = int(self.tileYInput.text()) y = int(self.tileYInput.text())
z = int(self.tileZInput.text()) z = int(self.tileZInput.text())
map.moveTo(x, y, z) self.parent.map.moveTo(x, y, z)
except ValueError: except ValueError:
QMessageBox.warning(self, "Invalid Input", "Please enter valid integer coordinates.") QMessageBox.warning(self, "Invalid Input", "Please enter valid integer coordinates.")
@@ -80,4 +80,4 @@ class MapInfoPanel(QWidget):
self.mapTitleInput.setText(data.get("mapName", "")) self.mapTitleInput.setText(data.get("mapName", ""))
def onMapNameChange(self, text): def onMapNameChange(self, text):
self.parent.mapData.data['mapName'] = text self.parent.map.data['mapName'] = text

View File

@@ -1,13 +1,14 @@
import os import os
from PyQt5.QtWidgets import QAction, QMenuBar, QFileDialog from PyQt5.QtWidgets import QAction, QMenuBar, QFileDialog
from editortool.map.mapdata import MAP_DEFAULT_PATH from editortool.map.map import MAP_DEFAULT_PATH
class MapToolbar: class MapMenubar:
def __init__(self, parent): def __init__(self, parent):
self.parent = parent self.parent = parent
self.menubar = parent.menuBar() self.menubar = QMenuBar(parent)
self.fileMenu = self.menubar.addMenu("File") parent.setMenuBar(self.menubar)
self.fileMenu = self.menubar.addMenu("File")
self.actionNew = QAction("New", parent) self.actionNew = QAction("New", parent)
self.actionOpen = QAction("Open", parent) self.actionOpen = QAction("Open", parent)
self.actionSave = QAction("Save", parent) self.actionSave = QAction("Save", parent)
@@ -23,17 +24,17 @@ class MapToolbar:
self.fileMenu.addAction(self.actionSaveAs) self.fileMenu.addAction(self.actionSaveAs)
def newFile(self): def newFile(self):
self.parent.mapData.newFile() self.parent.map.newFile()
def openFile(self): def openFile(self):
filePath, _ = QFileDialog.getOpenFileName(self.menubar, "Open Map File", MAP_DEFAULT_PATH, "Map Files (*.json)") filePath, _ = QFileDialog.getOpenFileName(self.menubar, "Open Map File", MAP_DEFAULT_PATH, "Map Files (*.json)")
if filePath: if filePath:
self.parent.mapData.load(filePath) self.parent.map.load(filePath)
def saveFile(self): def saveFile(self):
self.parent.mapData.save() self.parent.map.save()
def saveAsFile(self): def saveAsFile(self):
filePath, _ = QFileDialog.getSaveFileName(self.menubar, "Save Map File As", MAP_DEFAULT_PATH, "Map Files (*.json)") filePath, _ = QFileDialog.getSaveFileName(self.menubar, "Save Map File As", MAP_DEFAULT_PATH, "Map Files (*.json)")
if filePath: if filePath:
self.parent.mapData.save(filePath) self.parent.map.save(filePath)

View File

@@ -10,9 +10,9 @@ class SelectBox:
def draw(self): def draw(self):
position = [ position = [
self.parent.parent.map.position[0] * TILE_WIDTH - (TILE_WIDTH / 2.0), self.parent.map.position[0] * TILE_WIDTH - (TILE_WIDTH / 2.0),
self.parent.parent.map.position[1] * TILE_HEIGHT - (TILE_HEIGHT / 2.0), self.parent.map.position[1] * TILE_HEIGHT - (TILE_HEIGHT / 2.0),
self.parent.parent.map.position[2] * TILE_DEPTH - (TILE_DEPTH / 2.0) self.parent.map.position[2] * TILE_DEPTH - (TILE_DEPTH / 2.0)
] ]
vertices = [ vertices = [

View File

@@ -9,10 +9,10 @@ class StatusBar(QStatusBar):
self.addWidget(self.leftLabel, 1) self.addWidget(self.leftLabel, 1)
self.addPermanentWidget(self.rightLabel) self.addPermanentWidget(self.rightLabel)
parent.mapData.onMapData.sub(self.onMapData) parent.map.onMapData.sub(self.onMapData)
def setStatus(self, message): def setStatus(self, message):
self.leftLabel.setText(message) self.leftLabel.setText(message)
def onMapData(self, data): def onMapData(self, data):
self.rightLabel.setText(self.parent.mapData.mapFileName if self.parent.mapData.mapFileName else "Untitled.json") self.rightLabel.setText(self.parent.map.mapFileName if self.parent.map.mapFileName else "Untitled.json")

View File

@@ -4,38 +4,39 @@ from PyQt5.QtCore import Qt
from editortool.map.glwidget import GLWidget from editortool.map.glwidget import GLWidget
from editortool.map.chunkpanel import ChunkPanel from editortool.map.chunkpanel import ChunkPanel
from editortool.map.mapinfopanel import MapInfoPanel from editortool.map.mapinfopanel import MapInfoPanel
from editortool.map.mapdata import MapData from editortool.map.menubar import MapMenubar
from editortool.map.toolbar import MapToolbar
from editortool.map.statusbar import StatusBar from editortool.map.statusbar import StatusBar
from editortool.map.map import Map from editortool.map.map import Map
from editortool.map.mapdefs import TILE_WIDTH, TILE_HEIGHT, TILE_DEPTH from editortool.map.mapdefs import TILE_WIDTH, TILE_HEIGHT, TILE_DEPTH
from editortool.map.selectbox import SelectBox
from editortool.map.camera import Camera
from editortool.map.grid import Grid
class MapWindow(QMainWindow): class MapWindow(QMainWindow):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
# Subclasses # Subclasses
self.mapData = MapData()
self.map = Map(self) self.map = Map(self)
self.camera = Camera(self)
self.grid = Grid()
self.selectBox = SelectBox(self)
# Window setup # Window setup
self.setWindowTitle("Dusk Map Editor") self.setWindowTitle("Dusk Map Editor")
self.resize(1280, 720) self.resize(1280, 720)
# Components # Menubar (TESTING)
self.toolbar = MapToolbar(self) self.menubar = MapMenubar(self)
central = QWidget() central = QWidget()
self.setCentralWidget(central) self.setCentralWidget(central)
mainLayout = QHBoxLayout(central) mainLayout = QHBoxLayout(central)
# Left panel (ChunkPanel) # Left panel (ChunkPanel)
self.chunkPanel = ChunkPanel(self) self.chunkPanel = ChunkPanel(self)
leftWidget = QWidget() self.chunkPanel.setFixedWidth(200)
leftWidget.setLayout(self.chunkPanel.layout()) mainLayout.addWidget(self.chunkPanel)
leftWidget.setFixedWidth(200)
mainLayout.addWidget(leftWidget)
# Center panel (GLWidget + controls) # Center panel (GLWidget + controls)
self.glWidget = GLWidget(self) self.glWidget = GLWidget(self)
@@ -52,21 +53,15 @@ class MapWindow(QMainWindow):
self.setStatusBar(self.statusBar) self.setStatusBar(self.statusBar)
self.installEventFilter(self) self.installEventFilter(self)
self._install_event_filter_recursive(self) self.installEventFilterRecursively(self)
def _install_event_filter_recursive(self, widget): def installEventFilterRecursively(self, widget):
for child in widget.findChildren(QWidget): for child in widget.findChildren(QWidget):
child.installEventFilter(self) child.installEventFilter(self)
self._install_event_filter_recursive(child) self.installEventFilterRecursively(child)
def generateData(self):
objOut = {}
self.fileSaving.invoke(objOut)
self.mapData.data = objOut
return objOut
def closeEvent(self, event): def closeEvent(self, event):
if not self.mapData.isDirty(): if not self.map.isDirty():
event.accept() event.accept()
return return
@@ -79,7 +74,7 @@ class MapWindow(QMainWindow):
) )
if reply == QMessageBox.Save: if reply == QMessageBox.Save:
self.saveFile() self.saveFile()
if self.mapData.isDirty(): if self.map.isDirty():
event.ignore() event.ignore()
return return
elif reply == QMessageBox.Cancel: elif reply == QMessageBox.Cancel: