[jasper/world/studio] Get a good start to world editor going
Some checks failed
Build / build (push) Failing after 46s

This commit is contained in:
Gary Talent 2024-04-21 10:25:44 -05:00
parent 7ceee8c84d
commit 513e3360b5
31 changed files with 810 additions and 339 deletions

View File

@ -57,6 +57,7 @@ misc-*,
readability-duplicate-include,
-misc-non-private-member-variables-in-classes,
-misc-no-recursion,
-misc-include-cleaner,
bugprone-*,
clang-analyzer-*,
modernize-*,

View File

@ -11,6 +11,10 @@ endif()
include(deps/nostalgia/deps/buildcore/base.cmake)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
if(BUILDCORE_TARGET STREQUAL "gba")
include(deps/nostalgia/deps/gbabuildcore/base.cmake)
else()

View File

@ -19,8 +19,8 @@ struct AnimPage {
};
oxModelBegin(AnimPage)
oxModelFieldRename(tilesheet_path, tilesheetPath)
oxModelFieldRename(subsheet_path, subsheetPath)
oxModelFieldRename(tilesheetPath, tilesheet_path)
oxModelFieldRename(subsheetPath, subsheet_path)
oxModelEnd()

View File

@ -1,35 +0,0 @@
/*
* Copyright 2023 - 2024 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <ox/std/size.hpp>
#include <ox/std/string.hpp>
#include <ox/model/def.hpp>
#include <nostalgia/core/core.hpp>
#include <jasper/core/animpage.hpp>
namespace jasper::world {
struct PrefabDoc {
constexpr static auto TypeName = "net.drinkingtea.jasper.world.PrefabDoc";
constexpr static auto TypeVersion = 1;
constexpr static auto Preloadable = true;
ox::Size footprint;
ox::Size visibleSz;
ox::String tilesheetPath;
ox::String subsheetPath;
};
oxModelBegin(PrefabDoc)
oxModelField(footprint)
oxModelFieldRename(visible_size, visibleSz)
oxModelFieldRename(tilesheet_path, tilesheetPath)
oxModelFieldRename(subsheet_path, subsheetPath)
oxModelEnd()
}

View File

@ -7,7 +7,6 @@
#include <nostalgia/core/context.hpp>
#include "consts.hpp"
#include "prefab.hpp"
#include "worldobject.hpp"
#include "worldstatic.hpp"
@ -25,7 +24,7 @@ class World {
ox::Error setupDisplay(ncore::Context &ctx) const noexcept;
private:
void setupLayer(ncore::Context&, ox::Vector<uint16_t> const&layer, unsigned layerNo) const noexcept;
void setupLayer(ncore::Context&, uint_t lyr, uint_t cbb) const noexcept;
};

View File

@ -4,6 +4,8 @@
#pragma once
#include <ox/std/array.hpp>
#include <nostalgia/core/context.hpp>
#include <nostalgia/core/tilesheet.hpp>
@ -14,7 +16,6 @@ namespace ncore = nostalgia::core;
struct PaletteCycle {
static constexpr auto TypeName = "net.drinkingtea.jasper.world.PaletteCycle";
static constexpr auto TypeVersion = 1;
static constexpr auto Preloadable = true;
ox::FileAddress palette;
uint16_t intervalMs = 1000;
};
@ -27,44 +28,68 @@ oxModelEnd()
using CollisionMap = uint32_t;
enum class ObjectType: uint8_t {
None = 0,
NormalBgTile = 1,
NormalSprite = 2,
Person = 3,
};
using ObjectId = uint32_t;
struct WorldObject {
static constexpr auto TypeName = "net.drinkingtea.jasper.world.WorldObject";
static constexpr auto TypeVersion = 1;
static constexpr auto Preloadable = true;
int id{};
ObjectId id{};
ox::String name;
uint16_t palBank{};
ncore::SubSheetId subsheetId{};
CollisionMap collisionMap{};
uint8_t objectType{};
uint8_t ext1{};
uint8_t ext2{};
uint8_t ext3{};
};
oxModelBegin(WorldObject)
oxModelField(id)
oxModelField(name)
oxModelField(palBank)
oxModelField(subsheetId)
oxModelField(collisionMap)
oxModelFieldRename(subsheetId, subsheet_id)
oxModelFieldRename(collisionMap, collision_map)
oxModelFieldRename(objectType, object_type)
oxModelField(ext1)
oxModelField(ext2)
oxModelField(ext3)
oxModelEnd()
enum class TerrainType: uint8_t {
NormalLand,
NormalWater,
};
void bgObjSetTerrainType(WorldObject &obj, TerrainType t) noexcept;
[[nodiscard]]
TerrainType bgObjGetTerrainType(WorldObject &obj) noexcept;
struct WorldObjectSet {
static constexpr auto TypeName = "net.drinkingtea.jasper.world.WorldObjectSet";
static constexpr auto TypeVersion = 1;
static constexpr auto Preloadable = true;
int objIdIdx = 0;
ObjectId objIdIdx = 0;
ox::FileAddress tilesheet;
ox::Vector<PaletteCycle> palettes;
ox::Vector<WorldObject> objects;
};
oxModelBegin(WorldObjectSet)
oxModelField(objIdIdx)
oxModelFieldRename(objIdIdx, obj_id_idx)
oxModelField(tilesheet)
oxModelField(palettes)
oxModelField(objects)
oxModelEnd()
ox::Error loadObjectSet(WorldObjectSet const&os) noexcept;
}

View File

@ -12,98 +12,15 @@
#include <nostalgia/core/tilesheet.hpp>
#include <jasper/world/objectcache.hpp>
#include "worlddoc.hpp"
#include "worldobject.hpp"
namespace jasper::world {
namespace ncore = nostalgia::core;
struct SpriteDoc {
constexpr static auto TypeName = "net.drinkingtea.jasper.world.SpriteDoc";
constexpr static auto TypeVersion = 1;
constexpr static auto Preloadable = true;
ox::String tilesheetPath;
ox::Vector<ncore::SubSheetId> subsheetId;
};
struct TileDoc {
constexpr static auto TypeName = "net.drinkingtea.jasper.world.TileDoc";
constexpr static auto TypeVersion = 1;
constexpr static auto Preloadable = true;
ncore::SubSheetId subsheetId = -1;
ox::String subsheetPath;
uint8_t type = 0;
ox::Array<uint8_t, 4> layerAttachments;
[[nodiscard]]
constexpr ox::Result<ncore::SubSheetId> getSubsheetId(ncore::TileSheet const&ts) const noexcept {
// prefer the already present ID
if (subsheetId > -1) {
return subsheetId;
}
return getIdFor(ts, subsheetPath);
}
[[nodiscard]]
constexpr ox::Result<ox::StringView> getSubsheetPath(
ncore::TileSheet const&ts) const noexcept {
// prefer the already present path
if (!subsheetPath.len()) {
return getNameFor(ts, subsheetId);
}
return ox::StringView(subsheetPath);
}
};
oxModelBegin(TileDoc)
oxModelFieldRename(subsheetId, subsheet_id)
oxModelFieldRename(subsheetPath, subsheet_path)
oxModelField(type)
oxModelFieldRename(layerAttachments, layer_attachments)
oxModelEnd()
struct WorldDoc {
using TileMapRow = ox::Vector<TileDoc>;
using TileMapLayer = ox::Vector<TileMapRow>;
using TileMap = ox::Vector<TileMapLayer>;
constexpr static auto TypeName = "net.drinkingtea.jasper.world.WorldDoc";
constexpr static auto TypeVersion = 1;
constexpr static auto Preloadable = true;
ox::String tilesheet; // path
ox::Vector<ox::String> palettes; // paths
TileMap tiles;
[[nodiscard]]
constexpr ox::Size size(std::size_t layerIdx) const noexcept {
const auto &layer = this->tiles[layerIdx];
const auto rowCnt = static_cast<int>(layer.size());
if (!rowCnt) {
return {};
}
auto colCnt = layer[0].size();
// find shortest row (they should all be the same, but you know this data
// could come from a file)
for (auto const&row : layer) {
colCnt = ox::min(colCnt, row.size());
}
return {static_cast<int>(colCnt), rowCnt};
}
};
oxModelBegin(WorldDoc)
oxModelField(tilesheet)
oxModelField(palettes)
oxModelField(tiles)
oxModelEnd()
constexpr void setTopEdge(uint8_t &layerAttachments, unsigned val) noexcept {
const auto val8 = static_cast<uint8_t>(val);
layerAttachments = (layerAttachments & 0b11111100) | val8;
@ -139,92 +56,69 @@ constexpr unsigned rightEdge(uint8_t layerAttachments) noexcept {
}
struct WorldStatic {
constexpr static auto TypeName = "net.drinkingtea.jasper.world.WorldStatic";
struct TileStatic {
constexpr static auto TypeName = "net.drinkingtea.jasper.world.TileStatic";
constexpr static auto TypeVersion = 1;
constexpr static auto Preloadable = true;
struct Tile {
uint16_t &tileMapIdx;
uint8_t &tileType;
uint8_t &layerAttachments;
constexpr Tile(uint16_t &pTileMapIdx, uint8_t &pTileType, uint8_t &pLayerAttachments) noexcept:
tileMapIdx(pTileMapIdx),
tileType(pTileType),
layerAttachments(pLayerAttachments) {
}
};
struct Layer {
uint16_t &columns;
uint16_t &rows;
ox::Vector<uint16_t> &tileMapIdx;
ox::Vector<uint8_t> &tileType;
ox::Vector<uint8_t> &layerAttachments;
constexpr Layer(
uint16_t &pColumns,
uint16_t &pRows,
ox::Vector<uint16_t> &pTileMapIdx,
ox::Vector<uint8_t> &pTileType,
ox::Vector<uint8_t> &pLayerAttachments) noexcept:
columns(pColumns),
rows(pRows),
tileMapIdx(pTileMapIdx),
tileType(pTileType),
layerAttachments(pLayerAttachments) {
}
[[nodiscard]]
constexpr Tile tile(std::size_t i) noexcept {
return {tileMapIdx[i], tileType[i], layerAttachments[i]};
}
constexpr auto setDimensions(ox::Size dim) noexcept {
columns = static_cast<uint16_t>(dim.width);
rows = static_cast<uint16_t>(dim.height);
const auto tileCnt = static_cast<unsigned>(columns * rows);
tileMapIdx.resize(tileCnt);
tileType.resize(tileCnt);
layerAttachments.resize(tileCnt);
}
};
ox::FileAddress tilesheet;
ox::Vector<ox::FileAddress> palettes;
// tile layer data
ox::Vector<uint16_t> columns;
ox::Vector<uint16_t> rows;
ox::Vector<ox::Vector<uint16_t>> tileMapIdx;
ox::Vector<ox::Vector<uint8_t>> tileType;
ox::Vector<ox::Vector<uint8_t>> layerAttachments;
[[nodiscard]]
constexpr Layer layer(std::size_t i) noexcept {
return {
columns[i],
rows[i],
tileMapIdx[i],
tileType[i],
layerAttachments[i],
};
}
constexpr auto setLayerCnt(std::size_t layerCnt) noexcept {
this->layerAttachments.resize(layerCnt);
this->columns.resize(layerCnt);
this->rows.resize(layerCnt);
this->tileMapIdx.resize(layerCnt);
this->tileType.resize(layerCnt);
}
uint16_t tileIdx{};
uint8_t palBank{};
uint8_t tileType{};
uint8_t layerAttachments{};
};
oxModelBegin(WorldStatic)
oxModelField(tilesheet)
oxModelField(palettes)
oxModelField(columns)
oxModelField(rows)
oxModelField(tileMapIdx)
oxModelBegin(TileStatic)
oxModelField(tileIdx)
oxModelField(palBank)
oxModelField(tileType)
oxModelField(layerAttachments)
oxModelEnd()
struct BgLayer {
constexpr static auto TypeName = "net.drinkingtea.jasper.world.BgLayer";
constexpr static auto TypeVersion = 1;
uint8_t cbb{};
ox::Vector<TileStatic> tiles;
};
oxModelBegin(BgLayer)
oxModelField(cbb)
oxModelField(tiles)
oxModelEnd()
struct WorldStatic {
constexpr static auto TypeName = "net.drinkingtea.jasper.world.WorldStatic";
constexpr static auto TypeVersion = 1;
constexpr static auto Preloadable = true;
ncore::TileSheetSet tilesheets;
ox::Vector<PaletteCycle> palettes;
int16_t columns{};
int16_t rows{};
ox::Array<BgLayer, 3> map;
};
oxModelBegin(WorldStatic)
oxModelField(tilesheets)
oxModelField(palettes)
oxModelField(columns)
oxModelField(rows)
oxModelField(map)
oxModelEnd()
ox::Result<WorldStatic> loadWorldStatic(ObjectCache const&objCache, WorldDoc const&doc) noexcept;
[[nodiscard]]
auto &tile(ox::CommonRefWith<WorldStatic> auto &ws, size_t lyr, size_t col, size_t row) noexcept {
auto const idx = row * static_cast<size_t>(ws.columns) + col;
return ws.map[lyr].tiles[idx];
}
[[nodiscard]]
auto &cbb(ox::CommonRefWith<WorldStatic> auto &ws, size_t lyr) noexcept {
return ws.map[lyr].cbb;
}
[[nodiscard]]
bool isValid(WorldStatic const&ws) noexcept;
}

View File

@ -1,7 +1,10 @@
add_library(
JasperWorld
objectcache.cpp
world.cpp
worlddoc.cpp
worldobject.cpp
worldstatic.cpp
)

View File

@ -5,66 +5,18 @@
#include <nostalgia/core/gfx.hpp>
#include <keel/media.hpp>
#include <jasper/world/objectcache.hpp>
#include "typeconv.hpp"
namespace jasper::world {
namespace ncore = nostalgia::core;
[[nodiscard]]
constexpr unsigned adjustLayerAttachment(unsigned layer, unsigned attachment) noexcept {
if (attachment == 0) {
return layer;
} else {
return attachment - 1;
}
}
constexpr void setLayerAttachments(unsigned layer, TileDoc const&srcTile, WorldStatic::Tile &dstTile) noexcept {
setTopEdge(
dstTile.layerAttachments,
adjustLayerAttachment(layer, srcTile.layerAttachments[0]));
setBottomEdge(
dstTile.layerAttachments,
adjustLayerAttachment(layer, srcTile.layerAttachments[1]));
setLeftEdge(
dstTile.layerAttachments,
adjustLayerAttachment(layer, srcTile.layerAttachments[2]));
setRightEdge(
dstTile.layerAttachments,
adjustLayerAttachment(layer, srcTile.layerAttachments[3]));
}
ox::Error WorldDocToWorldStaticConverter::convert(
keel::Context &ctx,
keel::Context &kctx,
WorldDoc &src,
WorldStatic &dst) const noexcept {
oxRequire(ts, keel::readObj<ncore::TileSheet>(ctx, src.tilesheet));
const auto layerCnt = src.tiles.size();
dst.setLayerCnt(layerCnt);
dst.tilesheet = ox::FileAddress(src.tilesheet);
dst.palettes.reserve(src.palettes.size());
for (const auto &pal : src.palettes) {
dst.palettes.emplace_back(pal);
}
for (auto layerIdx = 0u; const auto &layer : src.tiles) {
const auto layerDim = src.size(layerIdx);
auto dstLayer = dst.layer(layerIdx);
dstLayer.setDimensions(layerDim);
for (auto tileIdx = 0u; const auto &row : layer) {
for (const auto &srcTile : row) {
auto dstTile = dstLayer.tile(tileIdx);
dstTile.tileType = srcTile.type;
oxRequire(path, srcTile.getSubsheetPath(*ts));
oxRequire(mapIdx, getTileOffset(*ts, path));
dstTile.tileMapIdx = static_cast<uint16_t>(mapIdx);
setLayerAttachments(layerIdx, srcTile, dstTile);
++tileIdx;
}
}
++layerIdx;
}
return {};
oxRequire(oc, buildObjCache(kctx, src));
return loadWorldStatic(oc, src).moveTo(dst);
}
}

View File

@ -0,0 +1,91 @@
/*
* Copyright 2023 - 2024 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <keel/media.hpp>
#include <nostalgia/core/gfx.hpp>
#include <jasper/world/objectcache.hpp>
namespace jasper::world {
void ObjectCache::clear() noexcept {
m_tileIdx = 0;
m_palBank = 0;
m_tilesheets.clear();
m_palettes.clear();
m_objSets.clear();
}
ox::Error ObjectCache::indexSet(
keel::Context &kctx,
uint64_t setId,
WorldObjectSet const&objSet) noexcept {
auto &set = m_objSets.emplace_back(ObjSet{
.setId = setId,
.objects = {},
});
oxRequire(ts, readObj<ncore::TileSheet>(kctx, objSet.tilesheet));
for (auto const&obj : objSet.objects) {
set.objects.emplace_back(Obj{
.id = obj.id,
.palBank = static_cast<uint16_t>(obj.palBank + m_palBank),
.tileIdx = getTileIdx(*ts, obj.subsheetId) + m_tileIdx,
});
}
auto const tileCnt = getTileCnt(*ts);
m_tileIdx += tileCnt;
m_tilesheets.emplace_back(objSet.tilesheet);
m_tilesheetSet.bpp = 4;
addTileSheet(objSet.tilesheet, static_cast<int32_t>(tileCnt));
for (auto const&pal : objSet.palettes) {
m_palettes.emplace_back(pal);
oxRequire(p, readObj<ncore::Palette>(kctx, pal.palette));
m_palBank += largestPage(*p);
}
return {};
}
ox::Optional<ObjectCache::Obj> ObjectCache::obj(uint64_t setId, ObjectId objId) const noexcept {
for (auto &set : m_objSets) {
if (set.setId == setId) {
for (auto &obj : set.objects) {
if (obj.id == objId) {
return ox::Optional<ObjectCache::Obj>{ox::in_place, obj};
}
}
break;
}
}
return {};
}
ncore::TileSheetSet const&ObjectCache::tilesheets() const noexcept {
return m_tilesheetSet;
}
ox::Vector<PaletteCycle> const&ObjectCache::palettes() const noexcept {
return m_palettes;
}
void ObjectCache::addTileSheet(ox::FileAddress path, int32_t tiles) noexcept {
ncore::TileSheetSetEntry entry;
entry.tilesheet = std::move(path);
entry.sections.push_back({
.begin = 0,
.tiles = tiles,
});
m_tilesheetSet.entries.emplace_back(std::move(entry));
}
ox::Result<ObjectCache> buildObjCache(keel::Context &kctx, WorldDoc const&doc) noexcept {
ObjectCache cache;
for (auto &set : doc.objSets) {
oxRequire(s, readObj<WorldObjectSet>(kctx, set.path));
oxReturnError(cache.indexSet(kctx, set.id, *s));
}
return cache;
}
}

View File

@ -1,6 +1,7 @@
add_library(
JasperWorld-Studio
studiomodule.cpp
worldeditor/commands/commands.hpp
)
target_link_libraries(

View File

@ -1,6 +1,8 @@
target_sources(
JasperWorld-Studio PRIVATE
commands/addrmobjectset.cpp
commands/editsize.cpp
objectexplorer.cpp
worldeditor-imgui.cpp
worldeditor.cpp
worldeditorview.cpp
)

View File

@ -0,0 +1,59 @@
/*
* Copyright 2023 - 2024 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include "commands.hpp"
#include "addrmobjectset.hpp"
namespace jasper::world {
AddObjectSet::AddObjectSet(WorldDoc &doc, ox::StringView objSetPath):
m_doc(doc),
m_path(ox::String(objSetPath)) {
for (auto const&o : m_doc.objSets) {
if (o.path > objSetPath) {
break;
} else if (o.path == objSetPath) {
throw ox::Exception(OxError(1, "Path already exists in doc"));
}
++m_insertIdx;
}
}
void AddObjectSet::redo() noexcept {
m_doc.objSets.insert(m_insertIdx, {
.path = std::move(m_path),
.id = ++m_doc.objSetIdIdx,
});
}
void AddObjectSet::undo() noexcept {
m_path = std::move(m_doc.objSets[m_insertIdx].path);
--m_doc.objSetIdIdx;
oxIgnoreError(m_doc.objSets.erase(m_insertIdx));
}
int AddObjectSet::commandId() const noexcept {
return static_cast<int>(WorldEditorCommand::AddObjectSet);
}
RmObjectSet::RmObjectSet(WorldDoc &doc, size_t idx):
m_doc(doc),
m_idx(idx) {
}
void RmObjectSet::redo() noexcept {
m_entry = std::move(m_doc.objSets[m_idx]);
oxIgnoreError(m_doc.objSets.erase(m_idx));
}
void RmObjectSet::undo() noexcept {
m_doc.objSets.emplace(m_idx, std::move(m_entry));
}
int RmObjectSet::commandId() const noexcept {
return static_cast<int>(WorldEditorCommand::RmObjectSet);
}
}

View File

@ -0,0 +1,45 @@
/*
* Copyright 2023 - 2024 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <ox/fs/fs.hpp>
#include <studio/undostack.hpp>
#include <jasper/world/world.hpp>
namespace jasper::world {
class AddObjectSet: public studio::UndoCommand {
private:
WorldDoc &m_doc;
size_t m_insertIdx{};
ox::String m_path;
public:
AddObjectSet(WorldDoc &doc, ox::StringView objSetPath);
void redo() noexcept override;
void undo() noexcept override;
[[nodiscard]]
int commandId() const noexcept override;
[[nodiscard]]
inline size_t insertIdx() const noexcept {
return m_insertIdx;
}
};
class RmObjectSet: public studio::UndoCommand {
private:
WorldDoc &m_doc;
size_t m_idx{};
ObjectSetEntry m_entry;
public:
RmObjectSet(WorldDoc &doc, size_t idx);
void redo() noexcept override;
void undo() noexcept override;
[[nodiscard]]
int commandId() const noexcept override;
};
}

View File

@ -0,0 +1,16 @@
/*
* Copyright 2023 - 2024 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
namespace jasper::world {
enum class WorldEditorCommand {
None,
AddObjectSet,
RmObjectSet,
EditWorldSize,
};
}

View File

@ -0,0 +1,30 @@
/*
* Copyright 2023 - 2024 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include "commands.hpp"
#include "editsize.hpp"
namespace jasper::world {
EditWorldSize::EditWorldSize(WorldDoc &doc, ox::Size const&size):
m_doc(doc),
m_oldSize(m_doc.columns, m_doc.rows),
m_newVal(size) {
}
void EditWorldSize::redo() noexcept {
m_oldMap = m_doc.tiles;
resize(m_doc, m_newVal);
}
void EditWorldSize::undo() noexcept {
resize(m_doc, m_oldSize);
m_doc.tiles = std::move(m_oldMap);
}
int EditWorldSize::commandId() const noexcept {
return static_cast<int>(WorldEditorCommand::EditWorldSize);
}
}

View File

@ -0,0 +1,34 @@
/*
* Copyright 2023 - 2024 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <ox/fs/fs.hpp>
#include <studio/undostack.hpp>
#include <jasper/world/world.hpp>
namespace jasper::world {
class EditWorldSize: public studio::UndoCommand {
private:
WorldDoc &m_doc;
size_t m_insertIdx{};
ox::Size const m_oldSize;
WorldDoc::TileMap m_oldMap;
ox::Size const m_newVal;
public:
EditWorldSize(WorldDoc &doc, ox::Size const&size);
void redo() noexcept override;
void undo() noexcept override;
[[nodiscard]]
int commandId() const noexcept override;
[[nodiscard]]
inline size_t insertIdx() const noexcept {
return m_insertIdx;
}
};
}

View File

@ -0,0 +1,34 @@
/*
* Copyright 2023 - 2024 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include "objectexplorer.hpp"
namespace studio {
ObjectExplorerModel::ObjectExplorerModel(ox::StringView name, ObjectExplorerModel *parent) noexcept:
m_parent(parent),
m_name(name),
m_fullPath(m_parent ? (m_parent->m_fullPath + "/" + m_name) : ox::String{}) {
}
void ObjectExplorerModel::draw(turbine::Context &ctx) const noexcept {
constexpr auto dirFlags = ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick;
if (!m_children.empty()) {
if (ImGui::TreeNodeEx(m_name.c_str(), dirFlags)) {
for (auto const&child : m_children) {
child->draw(ctx);
}
ImGui::TreePop();
}
} else {
auto const name = ox::sfmt<ox::BasicString<255>>("{}##{}", m_name, m_fullPath);
if (ImGui::TreeNodeEx(name.c_str(), ImGuiTreeNodeFlags_Leaf)) {
if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(0)) {
}
ImGui::TreePop();
}
}
}
}

View File

@ -0,0 +1,23 @@
/*
* Copyright 2023 - 2024 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <turbine/turbine.hpp>
namespace studio {
class ObjectExplorerModel {
private:
ObjectExplorerModel *m_parent{};
ox::String m_name;
ox::String m_fullPath;
ox::Vector<ox::UPtr<ObjectExplorerModel>> m_children;
public:
explicit ObjectExplorerModel(ox::StringView name, ObjectExplorerModel *parent = {}) noexcept;
void draw(turbine::Context &ctx) const noexcept;
private:
};
}

View File

@ -4,23 +4,151 @@
#include <imgui.h>
#include <ox/std/ignore.hpp>
#include <keel/media.hpp>
#include <studio/imguiutil.hpp>
#include "commands/addrmobjectset.hpp"
#include "commands/editsize.hpp"
#include "worldeditor-imgui.hpp"
namespace jasper::world {
WorldEditorImGui::WorldEditorImGui(studio::StudioContext &ctx, ox::StringView path):
Editor(path),
m_ctx(ctx.tctx),
m_editor(m_ctx, path),
m_view(m_ctx, m_editor.world()) {
namespace ig = studio::ig;
constexpr auto SqrBtnSize = ImVec2(ig::BtnSz.y, ig::BtnSz.y);
struct WorldTileDragDrop {
static constexpr auto TypeName = "net.drinkingtea.jasper.world.studio.WorldTileDragDrop";
static constexpr auto TypeVersion = 1;
ox::String setPath;
ox::String objName;
};
oxModelBegin(WorldTileDragDrop)
oxModelField(setPath)
oxModelField(objName)
oxModelEnd()
static WorldDoc makeValid(WorldDoc doc) noexcept {
for (auto &lyr : doc.tiles) {
lyr.resize(static_cast<size_t>(doc.columns));
for (auto &row : lyr) {
row.resize(static_cast<size_t>(doc.rows));
}
}
return doc;
}
WorldEditorImGui::WorldEditorImGui(studio::StudioContext &sctx, ox::StringView path):
Editor(path),
m_sctx(sctx),
m_doc(makeValid(*readObj<WorldDoc>(keelCtx(m_sctx.tctx), path).unwrapThrow())),
m_objCache(buildObjCache(keelCtx(m_sctx.tctx), m_doc).unwrapThrow()),
m_worldStatic(loadWorldStatic(m_objCache, m_doc)),
m_view(m_sctx.tctx, m_worldStatic) {
oxThrowError(loadObjectSets());
setRequiresConstantRefresh(false);
m_objSetPicker.filePicked.connect(this, &WorldEditorImGui::addObjSet);
m_sctx.project->fileUpdated.connect(this, &WorldEditorImGui::handleObjectSetUpdate);
}
void WorldEditorImGui::draw(turbine::Context&) noexcept {
const auto paneSize = ImGui::GetContentRegionAvail();
constexpr auto resourcesWidth = 300.f;
{
ig::ChildStackItem const worldView{"WorldView", ImVec2(paneSize.x - resourcesWidth, 0)};
drawWorldView();
}
ImGui::SameLine();
{
ig::ChildStackItem const worldView{"RightPane"};
drawPropEditor();
ImGui::Separator();
drawResources();
}
m_objSetPicker.draw();
}
void WorldEditorImGui::onActivated() noexcept {
oxLogError(m_view.setupWorld());
}
ox::Error WorldEditorImGui::saveItem() noexcept {
return m_sctx.project->writeObj(itemPath(), m_doc, ox::ClawFormat::Organic);
}
void WorldEditorImGui::drawObjSetSelector() noexcept {
ig::IDStackItem const idStackItem("ObjSetSelector");
if (ig::PushButton("+", SqrBtnSize)) {
m_objSetPicker.show();
}
ImGui::SameLine();
if (ig::PushButton("-", SqrBtnSize)) {
rmObjSet();
}
ig::ListBox("Object Sets", [this](size_t i) -> ox::CStringView {
return m_doc.objSets[i].path;
}, m_doc.objSets.size(), m_palMgr.selectedIdx);
}
void WorldEditorImGui::drawObjSelector() noexcept {
ig::IDStackItem const idStackItem("ObjSelector");
for (auto const&set : m_objSets) {
constexpr auto flags = ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick;
if (ImGui::TreeNodeEx(set.name.c_str(), flags)) {
for (auto const&obj : set.set->objects) {
if (ImGui::TreeNodeEx(obj.name.c_str(), ImGuiTreeNodeFlags_Leaf)) {
ImGui::TreePop();
}
ig::dragDropSource([set, obj] {
std::ignore = ig::setDragDropPayload("WorldTile", WorldTileDragDrop{
.setPath = ox::String(set.name),
.objName = ox::String(obj.name)});
ImGui::Text("%s", obj.name.c_str());
});
}
ImGui::TreePop();
}
}
}
void WorldEditorImGui::drawPropEditor() noexcept {
ig::IDStackItem const idStackItem("PropEditor");
int width{m_doc.columns};
int height{m_doc.rows};
if (ImGui::InputInt("Map Width", &width, 1)) {
pushCommand<EditWorldSize>(m_doc, ox::Size{width, m_doc.rows});
}
if (ImGui::InputInt("Map Height", &height, 1)) {
pushCommand<EditWorldSize>(m_doc, ox::Size{m_doc.columns, height});
}
}
void WorldEditorImGui::drawResources() noexcept {
ig::IDStackItem const idStackItem("Resources");
drawObjSetSelector();
drawObjSelector();
}
[[nodiscard]]
static ox::Vec2 dropPos(ox::Vec2 dropPos) noexcept {
auto const winPos = ImGui::GetWindowPos();
dropPos.x -= winPos.x;
dropPos.y -= winPos.y;
return dropPos;
}
void WorldEditorImGui::drawWorldView() noexcept {
ig::IDStackItem const idStackItem("WorldView");
auto const paneSize = ImGui::GetContentRegionAvail();
m_view.draw(ox::Size{static_cast<int>(paneSize.x), static_cast<int>(paneSize.y)});
m_view.draw({static_cast<int>(paneSize.x), static_cast<int>(paneSize.y)});
auto &fb = m_view.framebuffer();
auto const fbWidth = static_cast<float>(fb.width);
auto const fbHeight = static_cast<float>(fb.height);
@ -37,22 +165,46 @@ void WorldEditorImGui::draw(turbine::Context&) noexcept {
xScale = 1;
yScale = srcW / dstW;
}
uintptr_t const buffId = fb.color.id;
ImGui::Image(
std::bit_cast<void*>(buffId),
paneSize,
ImVec2(0, 1),
ImVec2(xScale, 1 - yScale));
ig::toImTextureID(fb.color.id),
paneSize,
ImVec2(0, 1),
ImVec2(xScale, 1 - yScale));
std::ignore = ig::dragDropTarget([] {
oxRequire(obj, ig::getDragDropPayload<WorldTileDragDrop>("WorldTile"));
oxDebugf("{}: {}", obj.setPath, obj.objName);
auto const&io = ImGui::GetIO();
auto const mousePos = ox::Vec2(io.MousePos);
auto const dropPos = world::dropPos(mousePos);
std::cout << dropPos.x << ", " << dropPos.y << '\n';
return ox::Error{};
});
}
void WorldEditorImGui::onActivated() noexcept {
oxLogError(m_view.setupWorld());
ox::Error WorldEditorImGui::addObjSet(ox::StringView path) noexcept {
pushCommand<AddObjectSet>(m_doc, path);
return {};
}
ox::Error WorldEditorImGui::saveItem() noexcept {
const auto sctx = applicationData<studio::StudioContext>(m_ctx);
oxReturnError(sctx->project->writeObj(itemPath(), m_editor.world(), ox::ClawFormat::Organic));
oxReturnError(keelCtx(m_ctx).assetManager.setAsset(itemPath(), m_editor.world()));
void WorldEditorImGui::rmObjSet() noexcept {
pushCommand<RmObjectSet>(m_doc, m_palMgr.selectedIdx);
}
ox::Error WorldEditorImGui::handleObjectSetUpdate(ox::StringView path) noexcept {
if (ox::any_of(m_doc.objSets.begin(), m_doc.objSets.end(), [path](ObjectSetEntry const&set) {
return set.path == path;
})) {
oxReturnError(buildObjCache(keelCtx(m_sctx.tctx), m_doc).moveTo(m_objCache));
}
return loadObjectSets();
}
ox::Error WorldEditorImGui::loadObjectSets() noexcept {
m_objSets.clear();
for (auto const&set : m_doc.objSets) {
oxRequireM(os, readObj<WorldObjectSet>(keelCtx(m_sctx.tctx), set.path));
m_objSets.emplace_back(ObjSetRef{ox::String(set.path), std::move(os)});
}
return {};
}

View File

@ -8,7 +8,8 @@
#include <turbine/context.hpp>
#include "worldeditor.hpp"
#include <jasper/world/objectcache.hpp>
#include "worldeditorview.hpp"
namespace jasper::world {
@ -16,9 +17,21 @@ namespace jasper::world {
class WorldEditorImGui: public studio::Editor {
private:
turbine::Context &m_ctx;
WorldEditor m_editor;
studio::StudioContext &m_sctx;
studio::ig::FilePicker m_objSetPicker{
m_sctx, ox::String("Choose Object Set"), ox::String(FileExt_jwob)};
WorldDoc m_doc;
ObjectCache m_objCache;
struct ObjSetRef {
ox::String name;
keel::AssetRef<WorldObjectSet> set;
};
ox::Vector<ObjSetRef> m_objSets;
WorldStatic m_worldStatic;
WorldEditorView m_view;
struct {
size_t selectedIdx{};
} m_palMgr;
public:
WorldEditorImGui(studio::StudioContext &ctx, ox::StringView path);
@ -30,6 +43,26 @@ class WorldEditorImGui: public studio::Editor {
protected:
ox::Error saveItem() noexcept final;
private:
void drawObjSetSelector() noexcept;
void drawObjSelector() noexcept;
void drawPropEditor() noexcept;
void drawResources() noexcept;
void drawWorldView() noexcept;
ox::Error addObjSet(ox::StringView path) noexcept;
void rmObjSet() noexcept;
// handles the updating of an object set in case it is one used by this world
ox::Error handleObjectSetUpdate(ox::StringView path) noexcept;
ox::Error loadObjectSets() noexcept;
};
}

View File

@ -8,9 +8,9 @@
namespace jasper::world {
WorldEditor::WorldEditor(turbine::Context &ctx, ox::StringView path):
m_ctx(*applicationData<studio::StudioContext>(ctx)),
m_world(*readObj<WorldStatic>(keelCtx(ctx), path).unwrapThrow()) {
WorldEditor::WorldEditor(studio::StudioContext &ctx, ox::StringView path):
m_ctx(ctx),
m_world(*readObj<WorldStatic>(keelCtx(m_ctx.tctx), path).unwrapThrow()) {
}
ox::Error WorldEditor::saveItem() noexcept {

View File

@ -19,7 +19,7 @@ class WorldEditor {
WorldStatic m_world;
public:
WorldEditor(turbine::Context &ctx, ox::StringView path);
WorldEditor(studio::StudioContext &ctx, ox::StringView path);
[[nodiscard]]
WorldStatic const&world() const noexcept {

View File

@ -18,7 +18,7 @@ namespace ncore = nostalgia::core;
class CollisionView {
private:
static const glutils::ProgramSource s_programSrc;
static constexpr int s_scale = 10;
static constexpr int s_scale = 5;
ncore::ContextUPtr m_nctx;
glutils::FrameBuffer m_frameBuffer;
glutils::GLProgram m_shader;

View File

@ -109,18 +109,10 @@ void WorldObjectSetEditorImGui::drawObjSelector() noexcept {
if (ig::PushButton("-", btnSize)) {
undoStack()->push(ox::make_unique<RmObject>(m_doc, m_selectedObj));
}
if (ImGui::BeginListBox("Objects")) {
for (auto i = 0u; auto const&obj : m_doc.objects) {
ig::IDStackItem const idStackItem2(static_cast<int>(i));
if (ImGui::Selectable(obj.name.c_str(), m_selectedObj == i)) {
if (i != m_selectedObj) {
m_selectedObj = i;
loadObj();
}
}
++i;
}
ImGui::EndListBox();
if (ig::ListBox("Objects", [this](size_t i) -> ox::CStringView {
return m_doc.objects[i].name;
}, m_doc.objects.size(), m_selectedObj)) {
loadObj();
}
}

View File

@ -18,14 +18,14 @@ ox::Error World::setupDisplay(ncore::Context &ctx) const noexcept {
if (m_worldStatic.palettes.empty()) {
return OxError(1, "World has no palettes");
}
auto const&palette = m_worldStatic.palettes[0];
oxReturnError(ncore::loadBgTileSheet(
ctx, 0, m_worldStatic.tilesheet));
oxReturnError(ncore::loadBgPalette(ctx, 0, palette));
// disable all backgrounds
ncore::setBgStatus(ctx, 0);
for (auto layerNo = 0u; auto const&layer : m_worldStatic.tileMapIdx) {
setupLayer(ctx, layer, layerNo);
for (auto i = 0u; auto const&pal : m_worldStatic.palettes) {
oxReturnError(ncore::loadBgPalette(ctx, i, pal.palette));
++i;
}
oxReturnError(ncore::loadBgTileSheet(ctx, 0, m_worldStatic.tilesheets));
ncore::setBgStatus(ctx, 0); // disable all backgrounds
for (auto layerNo = 0u; auto const&layer : m_worldStatic.map) {
setupLayer(ctx, layerNo, layer.cbb);
++layerNo;
}
return {};
@ -33,23 +33,31 @@ ox::Error World::setupDisplay(ncore::Context &ctx) const noexcept {
void World::setupLayer(
ncore::Context &ctx,
ox::Vector<uint16_t> const&layer,
unsigned layerNo) const noexcept {
ncore::setBgStatus(ctx, layerNo, true);
ncore::setBgCbb(ctx, layerNo, 0);
auto x = 0;
auto y = 0;
const auto width = m_worldStatic.rows[layerNo];
for (auto const&tile : layer) {
const auto tile8 = static_cast<uint8_t>(tile);
ncore::setBgTile(ctx, layerNo, x + 0, y + 0, tile8 + 0);
ncore::setBgTile(ctx, layerNo, x + 1, y + 0, tile8 + 1);
ncore::setBgTile(ctx, layerNo, x + 0, y + 1, tile8 + 2);
ncore::setBgTile(ctx, layerNo, x + 1, y + 1, tile8 + 3);
x += 2;
if (x >= width * 2) {
x = 0;
y += 2;
uint_t lyr,
uint_t cbb) const noexcept {
ncore::setBgStatus(ctx, lyr, true);
ncore::setBgCbb(ctx, lyr, cbb);
for (auto y = 0; y < m_worldStatic.rows; ++y) {
for (auto x = 0; x < m_worldStatic.columns; ++x) {
auto &t = tile(m_worldStatic, lyr, static_cast<size_t>(x), static_cast<size_t>(y));
auto const tx = x * 2;
auto const ty = y * 2;
ncore::setBgTile(ctx, lyr, tx + 0, ty + 0, {
.tileIdx = static_cast<uint_t>(t.tileIdx + 0),
.palBank = t.palBank,
});
ncore::setBgTile(ctx, lyr, tx + 1, ty + 0, {
.tileIdx = static_cast<uint_t>(t.tileIdx + 1),
.palBank = t.palBank,
});
ncore::setBgTile(ctx, lyr, tx + 0, ty + 1, {
.tileIdx = static_cast<uint_t>(t.tileIdx + 2),
.palBank = t.palBank,
});
ncore::setBgTile(ctx, lyr, tx + 1, ty + 1, {
.tileIdx = static_cast<uint_t>(t.tileIdx + 3),
.palBank = t.palBank,
});
}
}
}

View File

@ -0,0 +1,29 @@
/*
* Copyright 2023 - 2024 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <jasper/world/worldstatic.hpp>
namespace jasper::world {
ObjectSetEntry const*objSetEntry(WorldDoc const&doc, size_t id) noexcept {
for (auto const&e : doc.objSets) {
if (e.id == id) {
return &e;
}
}
return nullptr;
}
void resize(WorldDoc &doc, ox::Size const&sz) noexcept {
doc.columns = sz.width;
doc.rows = sz.height;
for (auto &layer : doc.tiles) {
layer.resize(static_cast<size_t>(sz.height));
for (auto &row : layer) {
row.resize(static_cast<size_t>(sz.width));
}
}
}
}

View File

@ -0,0 +1,17 @@
/*
* Copyright 2023 - 2024 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <jasper/world/worldobject.hpp>
namespace jasper::world {
void bgObjSetTerrainType(WorldObject &obj, TerrainType t) noexcept {
obj.ext1 = static_cast<uint8_t>(t);
}
TerrainType bgObjGetTerrainType(WorldObject &obj) noexcept {
return static_cast<TerrainType>(obj.ext1);
}
}

View File

@ -2,10 +2,59 @@
* Copyright 2023 - 2024 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <nostalgia/core/gfx.hpp>
#include <jasper/world/worldstatic.hpp>
namespace jasper::world {
static void loadTile(ObjectCache const&objCache, TileStatic &dst, TileDoc const&src) noexcept {
auto const obj = objCache.obj(src.obj.worldObjectSetId, src.obj.worldObjectId).or_value({});
dst.palBank = src.palBank;
dst.tileIdx = static_cast<uint16_t>(obj.tileIdx);
dst.palBank = static_cast<uint8_t>(obj.palBank);
dst.tileType = src.type;
setTopEdge(dst.layerAttachments, src.topLayerAttachment);
setBottomEdge(dst.layerAttachments, src.bottomLayerAttachment);
setLeftEdge(dst.layerAttachments, src.leftLayerAttachment);
setRightEdge(dst.layerAttachments, src.rightLayerAttachment);
}
ox::Result<WorldStatic> loadWorldStatic(ObjectCache const&objCache, WorldDoc const&doc) noexcept {
auto const tileCnt =
static_cast<size_t>(doc.columns) * static_cast<size_t>(doc.rows);
WorldStatic out {
.tilesheets = {},
.palettes = {},
.columns = static_cast<int16_t>(doc.columns),
.rows = static_cast<int16_t>(doc.rows),
.map = {
{.tiles = ox::Vector<TileStatic>(tileCnt),},
{.tiles = ox::Vector<TileStatic>(tileCnt),},
{.tiles = ox::Vector<TileStatic>(tileCnt),},
},
};
// resources
out.tilesheets = objCache.tilesheets();
out.palettes = objCache.palettes();
// tiles
for (auto lyr = 0u; lyr < 3; ++lyr) {
for (auto x = 0u; x < static_cast<size_t>(out.columns); ++x) {
for (auto y = 0u; y < static_cast<size_t>(out.rows); ++y) {
auto &dst = tile(out, lyr, x, y);
auto &src = tile(doc, lyr, x, y);;
loadTile(objCache, dst, src);
}
}
}
return out;
}
bool isValid(WorldStatic const&ws) noexcept {
auto const tileCnt = static_cast<size_t>(ws.columns * ws.rows);
return ox::all_of(ws.map.begin(), ws.map.end(), [tileCnt](auto &v) {
return v.tiles.size() == tileCnt;
});
}
}

View File

@ -8,12 +8,22 @@
#include <keel/keel.hpp>
#include <turbine/turbine.hpp>
#include <nostalgia/core/core.hpp>
#include <jasper/core/bootfile.hpp>
#include <jasper/world/world.hpp>
namespace ncore = nostalgia::core;
namespace jasper {
ox::Error run(turbine::Context &ctx, ox::StringView, ox::SpanView<ox::String>) noexcept {
ox::Error run(turbine::Context &tctx, ox::StringView, ox::SpanView<ox::String>) noexcept {
oxOut("Jasper Player\n");
oxReturnError(turbine::run(ctx));
oxRequire(nctx, ncore::init(tctx));
auto constexpr worldPath = ox::FileAddress(ox::StringLiteral("/Worlds/Chester.jwld"));
oxRequire(worldStatic, readObj<world::WorldStatic>(keelCtx(tctx), worldPath));
world::World const world(*worldStatic);
oxReturnError(world.setupDisplay(*nctx));
oxReturnError(turbine::run(tctx));
oxOut("Exiting...\n");
return {};
}
@ -22,11 +32,14 @@ ox::Error run(turbine::Context &ctx, ox::StringView, ox::SpanView<ox::String>) n
namespace olympic {
ox::Error run(
[[maybe_unused]] ox::StringView project,
ox::StringView project,
[[maybe_unused]] ox::StringView appName,
[[maybe_unused]] ox::StringView projectDataDir,
[[maybe_unused]] int argc,
[[maybe_unused]] char const**argv) noexcept {
int argc,
char const**argv) noexcept {
if (argc < 2) {
return OxError(1, "Insufficient arguments given to olympic::run");
}
auto const path = ox::StringView(argv[1]);
oxRequireM(fs, keel::loadRomFs(path));
oxRequireM(tctx, turbine::init(std::move(fs), project));

View File

@ -18,7 +18,7 @@
<string>APPL</string>
<key>CFBundleVersion</key>
<string>d2023.12.0</string>
<string>dev build</string>
<key>LSMinimumSystemVersion</key>
<string>12.0.0</string>