Begin adding entities to editor
Some checks failed
Build Dusk / build-linux (push) Failing after 56s
Build Dusk / build-psp (push) Failing after 1m8s

This commit is contained in:
2025-11-22 10:38:16 -06:00
parent 3697cc3eef
commit 6f33522c1c
17 changed files with 261 additions and 56 deletions

View File

@@ -7,11 +7,13 @@
#include "asset/asset.h"
#include "assert/assert.h"
#include "rpg/entity/entity.h"
#pragma pack(push, 1)
typedef struct {
uint32_t tileCount;
uint8_t modelCount;
uint8_t entityCount;
} assetchunkheader_t;
#pragma pack(pop)
@@ -27,6 +29,15 @@ typedef struct {
} assetchunkmodelheader_t;
#pragma pack(pop)
#pragma pack(push, 1)
typedef struct {
entitytype_t entityType;
uint8_t localX;
uint8_t localY;
uint8_t localZ;
} assetchunkentityheader_t;
#pragma pack(pop)
errorret_t assetChunkLoad(assetcustom_t custom) {
assertNotNull(custom.output, "Output pointer cannot be NULL");
assertNotNull(custom.zipFile, "Zip file pointer cannot be NULL");
@@ -62,6 +73,15 @@ errorret_t assetChunkLoad(assetcustom_t custom) {
);
}
if(header.entityCount > CHUNK_ENTITY_COUNT_MAX) {
zip_fclose(custom.zipFile);
errorThrow(
"Chunk asset has too many entities: %d (max %d).",
header.entityCount,
CHUNK_ENTITY_COUNT_MAX
);
}
chunk->meshCount = header.modelCount;
// Read tile data
@@ -122,5 +142,37 @@ errorret_t assetChunkLoad(assetcustom_t custom) {
}
}
// Read entity data
for(uint8_t i = 0; i < header.entityCount; i++) {
assetchunkentityheader_t entityHeader;
bytesRead = zip_fread(
custom.zipFile, &entityHeader, sizeof(assetchunkentityheader_t)
);
if(bytesRead != sizeof(assetchunkentityheader_t)) {
zip_fclose(custom.zipFile);
errorThrow("Failed to read chunk entity header.");
}
uint8_t entityIndex = entityGetAvailable();
if(entityIndex == 0xFF) {
zip_fclose(custom.zipFile);
errorThrow("No available entity slots.");
}
entity_t *entity = &ENTITIES[entityIndex];
entityInit(entity, (entitytype_t)entityHeader.entityType);
entity->position.x = (
(chunk->position.x * CHUNK_WIDTH) + entityHeader.localX
);
entity->position.y = (
(chunk->position.y * CHUNK_HEIGHT) + entityHeader.localY
);
entity->position.z = (
(chunk->position.z * CHUNK_DEPTH) + entityHeader.localZ
);
chunk->entities[i] = entityIndex;
}
errorOk();
}

View File

@@ -8,12 +8,20 @@ ENTITY_DIR_WEST = 1
ENTITY_DIR_EAST = 2
ENTITY_DIR_NORTH = 3
ENTITY_COUNT = 256
ENTITY_TYPE_NULL = 0
ENTITY_TYPE_PLAYER = 1
ENTITY_TYPE_NPC = 2
ENTITY_TYPE_COUNT = 3
CHUNK_WIDTH = 16
CHUNK_HEIGHT = 16
CHUNK_DEPTH = 4
# CHUNK_VERTEX_COUNT_MAX = QUAD_VERTEXES * CHUNK_WIDTH * CHUNK_HEIGHT * 4
CHUNK_VERTEX_COUNT_MAX=6144
CHUNK_MESH_COUNT_MAX = 14
CHUNK_ENTITY_COUNT_MAX = 8
TILE_WIDTH = 16.0
TILE_HEIGHT = 16.0

View File

@@ -188,4 +188,13 @@ entity_t * entityGetAt(const worldpos_t position) {
} while(++ent, ent < &ENTITIES[ENTITY_COUNT]);
return NULL;
}
uint8_t entityGetAvailable() {
entity_t *ent = ENTITIES;
do {
if(ent->type == ENTITY_TYPE_NULL) return ent - ENTITIES;
} while(++ent, ent < &ENTITIES[ENTITY_COUNT]);
return 0xFF;
}

View File

@@ -11,8 +11,6 @@
#include "entitytype.h"
#include "npc.h"
#define ENTITY_COUNT 256
typedef struct map_s map_t;
typedef struct entity_s {
@@ -69,4 +67,11 @@ void entityWalk(entity_t *entity, const entitydir_t direction);
* @param pos The world position to check.
* @return Pointer to the entity at the position, or NULL if none.
*/
entity_t *entityGetAt(const worldpos_t pos);
entity_t *entityGetAt(const worldpos_t pos);
/**
* Gets an available entity index.
*
* @return The index of an available entity, or 0xFF if none are available.
*/
uint8_t entityGetAvailable();

View File

@@ -6,17 +6,11 @@
*/
#pragma once
#include "duskdefs.h"
#include "rpg/entity/player.h"
#include "npc.h"
typedef enum {
ENTITY_TYPE_NULL = 0,
ENTITY_TYPE_PLAYER,
ENTITY_TYPE_NPC,
ENTITY_TYPE_COUNT
} entitytype_t;
typedef uint8_t entitytype_t;
typedef union {
player_t player;

View File

@@ -13,6 +13,7 @@
#include "rpgcamera.h"
#include "rpgtextbox.h"
#include "util/memory.h"
#include "assert/assert.h"
errorret_t rpgInit(void) {
memoryZero(ENTITIES, sizeof(ENTITIES));
@@ -26,17 +27,14 @@ errorret_t rpgInit(void) {
rpgTextboxInit();
// TEST: Create some entities.
entity_t *ent;
ent = &ENTITIES[0];
uint8_t entIndex = entityGetAvailable();
assertTrue(entIndex != 0xFF, "No available entity slots!.");
entity_t *ent = &ENTITIES[entIndex];
entityInit(ent, ENTITY_TYPE_PLAYER);
RPG_CAMERA.mode = RPG_CAMERA_MODE_FOLLOW_ENTITY;
RPG_CAMERA.followEntity.followEntityId = ent->id;
ent->position.x = 2, ent->position.y = 2;
ent = &ENTITIES[1];
entityInit(ent, ENTITY_TYPE_NPC);
ent->position.x = 4, ent->position.y = 0, ent->position.z = 1;
// All Good!
errorOk();
}

View File

@@ -17,6 +17,7 @@ typedef struct chunk_s {
uint8_t meshCount;
meshvertex_t vertices[CHUNK_VERTEX_COUNT_MAX];
mesh_t meshes[CHUNK_MESH_COUNT_MAX];
uint8_t entities[CHUNK_ENTITY_COUNT_MAX];
} chunk_t;
/**

View File

@@ -9,6 +9,7 @@
#include "util/memory.h"
#include "assert/assert.h"
#include "asset/asset.h"
#include "rpg/entity/entity.h"
map_t MAP;
@@ -126,6 +127,11 @@ void mapDispose() {
}
void mapChunkUnload(chunk_t* chunk) {
for(uint8_t i = 0; i < CHUNK_ENTITY_COUNT_MAX; i++) {
if(chunk->entities[i] == 0xFF) break;
entity_t *entity = &ENTITIES[chunk->entities[i]];
entity->type = ENTITY_TYPE_NULL;
}
for(uint8_t i = 0; i < chunk->meshCount; i++) {
if(chunk->meshes[i].vertexCount == 0) continue;
meshDispose(&chunk->meshes[i]);
@@ -137,6 +143,7 @@ errorret_t mapChunkLoad(chunk_t* chunk) {
chunk->meshCount = 0;
memoryZero(chunk->meshes, sizeof(chunk->meshes));
memorySet(chunk->entities, 0xFF, sizeof(chunk->entities));
snprintf(buffer, sizeof(buffer), "map/map/%d_%d_%d.dcf",
chunk->position.x,

View File

@@ -54,6 +54,7 @@ def processChunk(chunk):
buffer.extend(b'DCF')# Header
buffer.extend(len(chunk.tiles).to_bytes(4, 'little')) # Number of tiles
buffer.extend(len(models).to_bytes(1, 'little')) # Number of models
buffer.extend(len(chunk.entities).to_bytes(1, 'little')) # Number of entities
# Buffer tile data as array of uint8_t
for tileIndex, tile in chunk.tiles.items():
@@ -79,6 +80,14 @@ def processChunk(chunk):
buffer.extend(bytearray(struct.pack('<f', vertex[0])))
buffer.extend(bytearray(struct.pack('<f', vertex[1])))
buffer.extend(bytearray(struct.pack('<f', vertex[2])))
# For each entity
for entity in chunk.entities.values():
buffer.extend(entity.type.to_bytes(1, 'little'))
buffer.extend(entity.localX.to_bytes(1, 'little'))
buffer.extend(entity.localY.to_bytes(1, 'little'))
buffer.extend(entity.localZ.to_bytes(1, 'little'))
pass
# Write out map file
relative = getAssetRelativePath(chunk.getFilename())

View File

@@ -3,6 +3,7 @@ import os
from dusk.event import Event
from dusk.defs import CHUNK_WIDTH, CHUNK_HEIGHT, CHUNK_DEPTH, CHUNK_VERTEX_COUNT_MAX, TILE_SHAPE_NULL
from dusk.tile import Tile
from dusk.entity import Entity
from editortool.map.vertexbuffer import VertexBuffer
from OpenGL.GL import *
@@ -14,12 +15,16 @@ class Chunk:
self.z = z
self.current = {}
self.original = {}
self.entities = {}
self.onChunkData = Event()
self.dirty = False
self.tiles = {}
self.vertexBuffer = VertexBuffer()
# TEST
self.addEntity()
tileIndex = 0
for tz in range(CHUNK_DEPTH):
for ty in range(CHUNK_HEIGHT):
@@ -100,4 +105,18 @@ class Chunk:
return f"{dir_path}/{self.x}_{self.y}_{self.z}.json"
def draw(self):
self.vertexBuffer.draw()
self.vertexBuffer.draw()
def addEntity(self):
ent = Entity()
self.entities[len(self.entities)] = ent
self.map.onEntityData.invoke()
return ent
def removeEntity(self, entity):
for key, val in list(self.entities.items()):
if val == entity:
del self.entities[key]
self.map.onEntityData.invoke()
return True
return False

View File

@@ -37,4 +37,11 @@ TILE_SHAPES = {}
for key in defs.keys():
if key.startswith('TILE_SHAPE_'):
globals()[key] = int(defs.get(key))
TILE_SHAPES[key] = int(defs.get(key))
TILE_SHAPES[key] = int(defs.get(key))
ENTITY_TYPES = {}
for key in defs.keys():
if key.startswith('ENTITY_TYPE_'):
globals()[key] = int(defs.get(key))
if key != 'ENTITY_TYPE_COUNT':
ENTITY_TYPES[key] = int(defs.get(key))

9
tools/dusk/entity.py Normal file
View File

@@ -0,0 +1,9 @@
from dusk.defs import ENTITY_TYPE_NULL, ENTITY_TYPE_NPC
class Entity:
def __init__(self):
self.type = ENTITY_TYPE_NPC
self.localX = 0
self.localY = 8
self.localZ = 1
pass

View File

@@ -19,6 +19,7 @@ class Map:
self.chunks = {}
self.onMapData = Event()
self.onPositionChange = Event()
self.onEntityData = Event()
self.mapFileName = None
self.lastFile = None

View File

@@ -1,4 +1,4 @@
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel, QPushButton, QGridLayout, QTreeWidget, QTreeWidgetItem, QComboBox
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel, QGridLayout, QTreeWidget, QTreeWidgetItem, QComboBox
from dusk.defs import CHUNK_WIDTH, CHUNK_HEIGHT, CHUNK_DEPTH, TILE_SHAPES
class ChunkPanel(QWidget):
@@ -6,25 +6,6 @@ class ChunkPanel(QWidget):
super().__init__(parent)
self.parent = parent
layout = QVBoxLayout(self)
self.chunkInfoLabel = QLabel("Tile Information")
layout.addWidget(self.chunkInfoLabel)
# Nav buttons
grid = QGridLayout()
self.btnUp = QPushButton("U")
self.btnN = QPushButton("N")
self.btnDown = QPushButton("D")
self.btnW = QPushButton("W")
self.btnS = QPushButton("S")
self.btnE = QPushButton("E")
grid.addWidget(self.btnUp, 0, 0)
grid.addWidget(self.btnN, 0, 1)
grid.addWidget(self.btnDown, 0, 2)
grid.addWidget(self.btnW, 1, 0)
grid.addWidget(self.btnS, 1, 1)
grid.addWidget(self.btnE, 1, 2)
layout.addLayout(grid)
# Tile shape dropdown
self.tileShapeDropdown = QComboBox()
@@ -39,16 +20,9 @@ class ChunkPanel(QWidget):
layout.addWidget(self.tree) # Removed invalid stretch factor
# Add stretch so tree expands
layout.setStretchFactor(grid, 0)
layout.setStretchFactor(self.tree, 1)
# Event subscriptions
self.btnN.clicked.connect(lambda: self.parent.map.moveRelative(0, -1, 0))
self.btnS.clicked.connect(lambda: self.parent.map.moveRelative(0, 1, 0))
self.btnE.clicked.connect(lambda: self.parent.map.moveRelative(1, 0, 0))
self.btnW.clicked.connect(lambda: self.parent.map.moveRelative(-1, 0, 0))
self.btnUp.clicked.connect(lambda: self.parent.map.moveRelative(0, 0, 1))
self.btnDown.clicked.connect(lambda: self.parent.map.moveRelative(0, 0, -1))
self.parent.map.onMapData.sub(self.onMapData)
self.parent.map.onPositionChange.sub(self.onPositionChange)
self.tileShapeDropdown.currentTextChanged.connect(self.onTileShapeChanged)

View File

@@ -0,0 +1,70 @@
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel, QListWidget, QHBoxLayout, QPushButton, QLineEdit
from PyQt5.QtCore import Qt
from dusk.entity import Entity
class EntityPanel(QWidget):
def __init__(self, parent):
super().__init__(parent)
self.parent = parent
layout = QVBoxLayout(self)
self.setLayout(layout)
# Top panel placeholder
topWidget = QLabel("Entity Editor (top)")
layout.addWidget(topWidget)
# Name input
nameLayout = QHBoxLayout()
nameLabel = QLabel("Name:")
self.nameInput = QLineEdit()
nameLayout.addWidget(nameLabel)
nameLayout.addWidget(self.nameInput)
layout.addLayout(nameLayout)
# Entity list and buttons
self.entityList = QListWidget()
self.entityList.addItems(["Entity 1", "Entity 2", "Entity 3"])
layout.addWidget(self.entityList, stretch=1)
btnLayout = QHBoxLayout()
self.btnAdd = QPushButton("Add")
self.btnRemove = QPushButton("Remove")
btnLayout.addWidget(self.btnAdd)
btnLayout.addWidget(self.btnRemove)
layout.addLayout(btnLayout)
# Events
self.btnAdd.clicked.connect(self.onAddEntity)
self.btnRemove.clicked.connect(self.onRemoveEntity)
self.parent.map.onEntityData.sub(self.onEntityData)
self.entityList.itemClicked.connect(self.onEntityClicked)
self.entityList.itemDoubleClicked.connect(self.onEntityDoubleClicked)
# Call once to populate
self.onEntityData()
self.onEntityUnselect()
def onEntityUnselect(self):
self.nameInput.setText("")
def onEntityDoubleClicked(self, item):
print(f"Double clicked: {item.text()}")
def onEntityClicked(self, item):
print(f"Clicked: {item.text()}")
def onAddEntity(self):
chunk = self.parent.map.getChunkAtWorldPos(*self.parent.map.position)
if chunk is None:
return
chunk.addEntity()
def onRemoveEntity(self):
pass
def onEntityData(self):
self.onEntityUnselect()
self.entityList.clear()
for chunk in self.parent.map.chunks.values():
for entity_id, entity in chunk.entities.items():
self.entityList.addItem(f"Entity {entity_id}")

View File

@@ -0,0 +1,46 @@
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QGridLayout, QPushButton, QTabWidget, QLabel
from editortool.map.chunkpanel import ChunkPanel
from editortool.map.entitypanel import EntityPanel
class MapLeftPanel(QWidget):
def __init__(self, parent):
super().__init__(parent)
self.parent = parent
layout = QVBoxLayout(self)
self.setLayout(layout)
# Nav buttons
self.chunkInfoLabel = QLabel("Tile Information")
layout.addWidget(self.chunkInfoLabel)
grid = QGridLayout()
self.btnUp = QPushButton("U")
self.btnN = QPushButton("N")
self.btnDown = QPushButton("D")
self.btnW = QPushButton("W")
self.btnS = QPushButton("S")
self.btnE = QPushButton("E")
grid.addWidget(self.btnUp, 0, 0)
grid.addWidget(self.btnN, 0, 1)
grid.addWidget(self.btnDown, 0, 2)
grid.addWidget(self.btnW, 1, 0)
grid.addWidget(self.btnS, 1, 1)
grid.addWidget(self.btnE, 1, 2)
layout.addLayout(grid)
# Tabs
self.tabs = QTabWidget()
self.chunkPanel = ChunkPanel(self.parent)
self.chunkPanel.setFixedWidth(250)
self.tabs.addTab(self.chunkPanel, "Tiles")
self.entityPanel = EntityPanel(self.parent)
self.tabs.addTab(self.entityPanel, "Entities")
layout.addWidget(self.tabs)
# Event subscriptions
self.btnN.clicked.connect(lambda: self.parent.map.moveRelative(0, -1, 0))
self.btnS.clicked.connect(lambda: self.parent.map.moveRelative(0, 1, 0))
self.btnE.clicked.connect(lambda: self.parent.map.moveRelative(1, 0, 0))
self.btnW.clicked.connect(lambda: self.parent.map.moveRelative(-1, 0, 0))
self.btnUp.clicked.connect(lambda: self.parent.map.moveRelative(0, 0, 1))
self.btnDown.clicked.connect(lambda: self.parent.map.moveRelative(0, 0, -1))

View File

@@ -1,8 +1,8 @@
import os
from PyQt5.QtWidgets import QMainWindow, QWidget, QHBoxLayout, QMessageBox, QTabWidget
from PyQt5.QtWidgets import QMainWindow, QWidget, QHBoxLayout, QMessageBox
from PyQt5.QtCore import Qt
from editortool.map.glwidget import GLWidget
from editortool.map.chunkpanel import ChunkPanel
from editortool.map.mapleftpanel import MapLeftPanel
from editortool.map.mapinfopanel import MapInfoPanel
from editortool.map.menubar import MapMenubar
from editortool.map.statusbar import StatusBar
@@ -36,14 +36,10 @@ class MapWindow(QMainWindow):
self.setCentralWidget(central)
mainLayout = QHBoxLayout(central)
# Tabs (left)
self.tabs = QTabWidget()
self.chunkPanel = ChunkPanel(self)
self.chunkPanel.setFixedWidth(200)
self.tabs.addTab(self.chunkPanel, "Tile Editor")
mainLayout.addWidget(self.tabs)
# Left panel (tabs + nav buttons)
self.leftPanel = MapLeftPanel(self)
self.leftPanel.setFixedWidth(250)
mainLayout.addWidget(self.leftPanel)
# Center panel (GLWidget + controls)
self.glWidget = GLWidget(self)