Initial engine repository

This commit is contained in:
2026-06-08 19:58:35 +08:00
commit 7a925c3736
137 changed files with 162421 additions and 0 deletions
+486
View File
@@ -0,0 +1,486 @@
#include <frostbite2D/2d/actor.h>
#include <frostbite2D/base/object_registry.h>
#include <frostbite2D/event/key_event.h>
#include <frostbite2D/event/mouse_event.h>
#include <frostbite2D/event/touch_event.h>
#include <frostbite2D/event/joystick_event.h>
#include <frostbite2D/event/window_event.h>
#include <SDL2/SDL.h>
#include <algorithm>
namespace frostbite2D {
Actor::Actor()
: parent_(nullptr)
, scene_(nullptr)
, rotation_(0.0f)
, visible_(true)
, zOrder_(0)
, opacity_(1.0f)
, anchor_(Vec2(0.0f, 0.0f)) {
uuid_ = UUID::generate();
ObjectRegistry::get().registerObject(uuid_, this);
}
void Actor::SetOpacity(float opacity) {
opacity_ = std::clamp(opacity, 0.0f, 1.0f);
}
float Actor::GetWorldOpacity() const {
float worldOpacity = opacity_;
if (parent_) {
worldOpacity *= parent_->GetWorldOpacity();
}
return worldOpacity;
}
void Actor::SetPosition(const Vec2& pos) {
position_ = pos;
markTransformDirty();
}
void Actor::SetPosition(float x, float y) {
position_ = Vec2(x, y);
markTransformDirty();
}
void Actor::SetRotation(float rotation) {
rotation_ = rotation;
markTransformDirty();
}
void Actor::SetScale(const Vec2& scale) {
scale_ = scale;
markTransformDirty();
}
void Actor::SetScale(float scale) {
scale_ = Vec2(scale, scale);
markTransformDirty();
}
void Actor::SetSkew(const Vec2& skew) {
skew_ = skew;
markTransformDirty();
}
void Actor::SetSkew(float skewX, float skewY) {
skew_ = Vec2(skewX, skewY);
markTransformDirty();
}
void Actor::SetSize(const Vec2& size) {
size_ = size;
markTransformDirty();
}
void Actor::SetSize(float width, float height) {
size_ = Vec2(width, height);
markTransformDirty();
}
void Actor::SetAnchor(const Vec2& anchor) {
anchor_ = anchor;
markTransformDirty();
}
void Actor::SetAnchor(float x, float y) {
anchor_ = Vec2(x, y);
markTransformDirty();
}
Vec2 Actor::GetTopLeftPosition() const {
return Vec2(position_.x - anchor_.x * size_.x,
position_.y - anchor_.y * size_.y);
}
void Actor::SetTopLeftPosition(const Vec2& pos) {
position_ = Vec2(pos.x + anchor_.x * size_.x,
pos.y + anchor_.y * size_.y);
markTransformDirty();
}
void Actor::SetTopLeftPosition(float x, float y) {
SetTopLeftPosition(Vec2(x, y));
}
Vec2 Actor::LocalToWorld(const Vec2& point) const {
return GetWorldTransform().transformPoint(point);
}
Vec2 Actor::WorldToLocal(const Vec2& point) const {
return GetWorldTransform().inverse().transformPoint(point);
}
Rect Actor::GetWorldBounds() const {
Vec2 topLeft = LocalToWorld(Vec2(0.0f, 0.0f));
Vec2 topRight = LocalToWorld(Vec2(size_.x, 0.0f));
Vec2 bottomLeft = LocalToWorld(Vec2(0.0f, size_.y));
Vec2 bottomRight = LocalToWorld(Vec2(size_.x, size_.y));
float minX = std::min(std::min(topLeft.x, topRight.x),
std::min(bottomLeft.x, bottomRight.x));
float maxX = std::max(std::max(topLeft.x, topRight.x),
std::max(bottomLeft.x, bottomRight.x));
float minY = std::min(std::min(topLeft.y, topRight.y),
std::min(bottomLeft.y, bottomRight.y));
float maxY = std::max(std::max(topLeft.y, topRight.y),
std::max(bottomLeft.y, bottomRight.y));
return Rect(minX, minY, maxX - minX, maxY - minY);
}
bool Actor::ContainsWorldPoint(const Vec2& point) const {
return GetWorldBounds().containsPoint(point);
}
bool Actor::ContainsWorldPoint(float x, float y) const {
return ContainsWorldPoint(Vec2(x, y));
}
const Transform2D& Actor::GetLocalTransform() const {
if (transformDirty_) {
updateLocalTransform();
}
return localTransform_;
}
const Transform2D& Actor::GetWorldTransform() const {
if (transformDirty_) {
updateLocalTransform();
updateWorldTransform();
transformDirty_ = false;
}
return worldTransform_;
}
void Actor::markTransformDirty() {
transformDirty_ = true;
OnTransformChanged();
for (auto it = children_.begin(); it != children_.end(); ++it) {
if (*it) {
(*it)->markTransformDirty();
}
}
}
void Actor::updateLocalTransform() const {
Vec2 anchorOffset = Vec2(anchor_.x * size_.x, anchor_.y * size_.y);
localTransform_ = Transform2D::translation(position_.x, position_.y) *
Transform2D::rotation(rotation_) *
Transform2D::skewing(skew_.x, skew_.y) *
Transform2D::scaling(scale_.x, scale_.y) *
Transform2D::translation(-anchorOffset.x, -anchorOffset.y);
}
void Actor::updateWorldTransform() const {
if (parent_) {
worldTransform_ = parent_->GetWorldTransform() * localTransform_;
} else {
worldTransform_ = localTransform_;
}
}
Actor::~Actor() {
ObjectRegistry::get().unregisterObject(uuid_);
RemoveAllChildren();
}
void Actor::Update(float deltaTime) {
executeUpdateListeners(UpdatePhase::BeforeChildren, deltaTime);
OnUpdate(deltaTime);
UpdateChildren(deltaTime);
executeUpdateListeners(UpdatePhase::AfterChildren, deltaTime);
}
void Actor::Render() {
RenderChildren();
}
void Actor::insertChildByZOrder(Ptr<Actor> child) {
if (!child) {
return;
}
if (children_.IsEmpty()) {
children_.PushBack(child);
return;
}
auto it = children_.begin();
while (it != children_.end()) {
if (*it && (*it)->GetZOrder() > child->GetZOrder()) {
children_.InsertBefore(child, *it);
return;
}
++it;
}
children_.PushBack(child);
}
void Actor::AddChild(Ptr<Actor> child) {
if (!child) {
return;
}
if (child->GetParent() == this) {
return;
}
if (child->GetParent()) {
child->GetParent()->RemoveChild(child);
}
child->SetParent(this);
child->SetScene(scene_);
if (iteratingChildren_) {
// 正在遍历孩子时不能立即重新插入排序位置,否则会破坏链表迭代状态。
children_.PushBack(child);
markChildrenOrderDirty();
} else {
insertChildByZOrder(child);
}
child->OnAdded(this);
}
void Actor::SetZOrder(int zOrder) {
if (zOrder_ == zOrder) {
return;
}
zOrder_ = zOrder;
if (parent_) {
// 不再在这里直接 RemoveChild/AddChild,避免更新阶段改动正在遍历的链表。
parent_->markChildrenOrderDirty();
}
}
void Actor::RemoveChild(Ptr<Actor> child) {
if (!child || child->GetParent() != this) {
return;
}
child->SetParent(nullptr);
child->SetScene(nullptr);
children_.Remove(child);
}
void Actor::RemoveAllChildren() {
while (!children_.IsEmpty()) {
Ptr<Actor> child = children_.GetFirst();
RemoveChild(child);
}
}
void Actor::UpdateChildren(float deltaTime) {
iteratingChildren_ = true;
for (auto it = children_.begin(); it != children_.end(); ++it) {
if (*it && (*it)->IsVisible()) {
(*it)->Update(deltaTime);
}
}
iteratingChildren_ = false;
if (childrenOrderDirty_) {
reorderChildrenByZOrder();
}
}
void Actor::RenderChildren() {
if (childrenOrderDirty_ && !iteratingChildren_) {
reorderChildrenByZOrder();
}
iteratingChildren_ = true;
for (auto it = children_.begin(); it != children_.end(); ++it) {
if (*it && (*it)->IsVisible()) {
(*it)->Render();
}
}
iteratingChildren_ = false;
}
void Actor::markChildrenOrderDirty() {
childrenOrderDirty_ = true;
}
void Actor::reorderChildrenByZOrder() {
if (!childrenOrderDirty_) {
return;
}
// 统一在安全时机按 zOrder 重建顺序,解决上下移动时频繁改层级导致的卡死。
std::vector<Ptr<Actor>> orderedChildren;
for (auto it = children_.begin(); it != children_.end(); ++it) {
if (*it) {
orderedChildren.push_back(*it);
}
}
std::sort(orderedChildren.begin(), orderedChildren.end(),
[](const Ptr<Actor>& lhs, const Ptr<Actor>& rhs) {
if (!lhs || !rhs) {
return static_cast<bool>(lhs);
}
return lhs->GetZOrder() < rhs->GetZOrder();
});
while (!children_.IsEmpty()) {
Ptr<Actor> child = children_.GetFirst();
children_.Remove(child);
}
for (auto& child : orderedChildren) {
children_.PushBack(child);
}
childrenOrderDirty_ = false;
}
uint32 Actor::AddUpdateListener(UpdateCallback callback, UpdatePhase phase) {
if (!callback) {
return 0;
}
UpdateListener listener;
listener.id = nextUpdateListenerId_++;
listener.phase = phase;
listener.callback = std::move(callback);
updateListeners_.push_back(std::move(listener));
return updateListeners_.back().id;
}
bool Actor::RemoveUpdateListener(uint32 listenerId) {
for (auto& listener : updateListeners_) {
if (listener.id == listenerId && listener.active) {
listener.active = false;
if (iteratingUpdateListeners_) {
updateListenersDirty_ = true;
} else {
compactUpdateListeners();
}
return true;
}
}
return false;
}
void Actor::ClearUpdateListeners() {
if (iteratingUpdateListeners_) {
for (auto& listener : updateListeners_) {
listener.active = false;
}
updateListenersDirty_ = true;
return;
}
updateListeners_.clear();
}
uint32 Actor::AddEventListener(EventType type, EventCallback callback) {
EventListener listener;
listener.id = nextListenerId_++;
listener.type = type;
listener.callback = std::move(callback);
eventListeners_.push_back(listener);
return listener.id;
}
bool Actor::RemoveEventListener(uint32 listenerId) {
for (auto it = eventListeners_.begin(); it != eventListeners_.end(); ++it) {
if (it->id == listenerId) {
eventListeners_.erase(it);
return true;
}
}
return false;
}
void Actor::ClearEventListeners() {
eventListeners_.clear();
}
void Actor::executeUpdateListeners(UpdatePhase phase, float deltaTime) {
if (updateListeners_.empty()) {
return;
}
iteratingUpdateListeners_ = true;
size_t listenerCount = updateListeners_.size();
for (size_t i = 0; i < listenerCount; ++i) {
auto& listener = updateListeners_[i];
if (!listener.active || listener.phase != phase || !listener.callback) {
continue;
}
listener.callback(*this, deltaTime);
}
iteratingUpdateListeners_ = false;
if (updateListenersDirty_) {
compactUpdateListeners();
}
}
void Actor::compactUpdateListeners() {
updateListeners_.erase(
std::remove_if(updateListeners_.begin(), updateListeners_.end(),
[](const UpdateListener& listener) {
return !listener.active;
}),
updateListeners_.end());
updateListenersDirty_ = false;
}
bool Actor::OnEvent(const Event& event) {
if (!eventReceiveEnabled_) {
return false;
}
for (const auto& listener : eventListeners_) {
if (listener.type == event.getType()) {
if (listener.callback(event)) {
return true;
}
}
}
return false;
}
bool Actor::DispatchEvent(const Event& event) {
if (event.isPropagationStopped()) {
return false;
}
if (OnEvent(event)) {
return true;
}
for (auto it = children_.begin(); it != children_.end(); ++it) {
if (*it) {
if ((*it)->DispatchEvent(event)) {
return true;
}
}
}
return false;
}
bool Actor::BubbleEvent(const Event& event) {
if (!parent_) {
return false;
}
if (parent_->OnEvent(event)) {
return true;
}
return parent_->BubbleEvent(event);
}
}
@@ -0,0 +1,141 @@
#include <frostbite2D/2d/canvas_actor.h>
#include <frostbite2D/graphics/renderer.h>
#include <algorithm>
namespace frostbite2D {
CanvasActor::CanvasActor() {
canvasRoot_ = MakePtr<Actor>();
canvasRoot_->SetName("canvasRoot");
}
bool CanvasActor::Init(int width, int height) {
return SetCanvasSize(width, height);
}
bool CanvasActor::SetCanvasSize(int width, int height) {
width = std::max(width, 1);
height = std::max(height, 1);
if (!renderTexture_) {
renderTexture_ = MakePtr<RenderTexture>();
}
if (!renderTexture_->Resize(width, height)) {
return false;
}
canvasWidth_ = width;
canvasHeight_ = height;
canvasCamera_.setPosition(Vec2::Zero());
canvasCamera_.setZoom(1.0f);
canvasCamera_.setViewport(canvasWidth_, canvasHeight_);
syncCanvasResources();
dirty_ = true;
return true;
}
void CanvasActor::SetClearColor(const Color& color) {
clearColor_ = color;
dirty_ = true;
}
void CanvasActor::SetDirty() {
dirty_ = true;
}
bool CanvasActor::Redraw() {
dirty_ = true;
return redrawInternal();
}
void CanvasActor::SetCustomDrawCallback(std::function<void()> callback) {
customDrawCallback_ = std::move(callback);
dirty_ = true;
}
void CanvasActor::ClearCustomDrawCallback() {
if (!customDrawCallback_) {
return;
}
customDrawCallback_ = nullptr;
dirty_ = true;
}
void CanvasActor::AddCanvasChild(RefPtr<Actor> child) {
if (!child || child.Get() == this) {
return;
}
canvasRoot_->AddChild(child);
dirty_ = true;
}
void CanvasActor::RemoveCanvasChild(RefPtr<Actor> child) {
if (!child) {
return;
}
canvasRoot_->RemoveChild(child);
dirty_ = true;
}
void CanvasActor::RemoveAllCanvasChildren() {
canvasRoot_->RemoveAllChildren();
dirty_ = true;
}
Ptr<Texture> CanvasActor::GetOutputTexture() const {
return renderTexture_ ? renderTexture_->GetTexture() : nullptr;
}
void CanvasActor::OnUpdate(float deltaTime) {
if (canvasRoot_) {
canvasRoot_->Update(deltaTime);
}
}
void CanvasActor::Render() {
if (dirty_) {
redrawInternal();
}
Sprite::Render();
}
bool CanvasActor::redrawInternal() {
if (!renderTexture_ || !renderTexture_->IsValid()) {
return false;
}
Renderer& renderer = Renderer::get();
if (!renderer.beginRenderToTexture(*renderTexture_, &canvasCamera_, clearColor_)) {
dirty_ = true;
return false;
}
if (canvasRoot_) {
canvasRoot_->Render();
}
if (customDrawCallback_) {
customDrawCallback_();
}
renderer.endRenderToTexture();
dirty_ = false;
return true;
}
void CanvasActor::syncCanvasResources() {
Ptr<Texture> outputTexture = renderTexture_ ? renderTexture_->GetTexture() : nullptr;
if (outputTexture) {
outputTexture->setSampling(TextureSampling::PixelArt);
}
SetTexture(outputTexture);
SetSize(static_cast<float>(canvasWidth_), static_cast<float>(canvasHeight_));
}
} // namespace frostbite2D
+271
View File
@@ -0,0 +1,271 @@
#include <frostbite2D/2d/sprite.h>
#include <frostbite2D/core/application.h>
#include <frostbite2D/graphics/renderer.h>
#include <frostbite2D/resource/npk_archive.h>
#include <SDL2/SDL.h>
#include <algorithm>
namespace frostbite2D {
Sprite::Sprite()
: Actor() {
}
Sprite::Sprite(Ptr<Texture> texture)
: Actor()
, texture_(texture) {
}
Sprite::~Sprite() {
}
Ptr<Sprite> Sprite::createFromFile(const std::string &path) {
Ptr<Texture> texture = Texture::loadFromFile(path);
if (!texture) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to load texture: %s", path.c_str());
return nullptr;
}
texture->setSampling(TextureSampling::PixelArt);
auto sprite = MakePtr<Sprite>(texture);
sprite->SetSizeToTexture();
return sprite;
}
Ptr<Sprite> Sprite::createFromMemory(const uint8* data, int width, int height, int channels) {
Ptr<Texture> texture = Texture::createFromMemory(data, width, height, channels);
if (!texture) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to create texture from memory");
return nullptr;
}
texture->setSampling(TextureSampling::PixelArt);
auto sprite = MakePtr<Sprite>(texture);
sprite->SetSizeToTexture();
return sprite;
}
void Sprite::Render() {
if (!IsVisible()) {
RenderChildren();
return;
}
if (!texture_) {
RenderChildren();
return;
}
Renderer& renderer = Renderer::get();
Quad quad = createQuad();
auto* shader = getActiveShader();
if (shader) {
shader->use();
ConfigureShader(shader);
}
Transform2D worldTransform = GetWorldTransform();
if (offset_ != Vec2::Zero()) {
worldTransform = Transform2D::translation(offset_.x, offset_.y) * worldTransform;
}
renderer.getBatch().submitQuad(quad, worldTransform, texture_, shader, blendMode_);
RenderChildren();
}
void Sprite::SetTexture(Ptr<Texture> texture) {
texture_ = texture;
}
void Sprite::SetShader(const std::string& shaderName) {
shaderName_ = shaderName;
}
void Sprite::SetDefaultShader() {
shaderName_.clear();
}
void Sprite::SetSourceRect(const Rect& rect) {
srcRect_ = rect;
}
void Sprite::SetColor(const Color& color) {
color_ = color;
}
void Sprite::SetColor(float r, float g, float b, float a) {
color_ = Color(r, g, b, a);
}
void Sprite::SetFlippedX(bool flipped) {
flippedX_ = flipped;
}
void Sprite::SetFlippedY(bool flipped) {
flippedY_ = flipped;
}
void Sprite::SetBlendMode(BlendMode mode) {
blendMode_ = mode;
}
void Sprite::SetSizeToTexture() {
if (texture_) {
SetSize(texture_->getWidth(), texture_->getHeight());
}
}
void Sprite::SetOffset(const Vec2& offset) {
offset_ = offset;
}
void Sprite::SetOffset(float x, float y) {
offset_.x = x;
offset_.y = y;
}
void Sprite::ConfigureShader(Shader* shader) const {
(void)shader;
}
void Sprite::updateTransform() {
Vec2 pos = GetPosition();
float rotation = GetRotation();
Vec2 scale = GetScale();
transform_ = Transform2D::translation(pos.x, pos.y) *
Transform2D::rotation(rotation) *
Transform2D::scaling(scale.x, scale.y);
}
Shader* Sprite::getActiveShader() const {
Renderer& renderer = Renderer::get();
ShaderManager& shaderManager = renderer.getShaderManager();
if (!shaderName_.empty()) {
Shader* shader = shaderManager.getShader(shaderName_);
if (shader && shader->isValid()) {
return shader;
} else {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Shader '%s' not found or invalid, fallback to default",
shaderName_.c_str());
}
}
return shaderManager.getShader(DEFAULT_SHADER);
}
Quad Sprite::createQuad() const {
Renderer& renderer = Renderer::get();
Rect srcRect = srcRect_;
if (srcRect.empty()) {
srcRect = Rect(0, 0, texture_->getWidth(), texture_->getHeight());
}
Vec2 destPos(0, 0);
Vec2 destSizeVec = GetSize();
if (destSizeVec.x == 0) {
destSizeVec.x = srcRect.width();
}
if (destSizeVec.y == 0) {
destSizeVec.y = srcRect.height();
}
Rect destRect(0, 0, destSizeVec.x, destSizeVec.y);
Vec2 texSize(texture_->getWidth(), texture_->getHeight());
float worldOpacity = GetWorldOpacity();
Quad quad = Quad::createTextured(
destRect, srcRect, texSize,
color_.r, color_.g, color_.b, color_.a * worldOpacity,
renderer.shouldShrinkSubTextureUVs()
);
if (flippedX_) {
std::swap(quad.vertices[0].texCoord, quad.vertices[1].texCoord);
std::swap(quad.vertices[2].texCoord, quad.vertices[3].texCoord);
}
if (flippedY_) {
std::swap(quad.vertices[0].texCoord, quad.vertices[2].texCoord);
std::swap(quad.vertices[1].texCoord, quad.vertices[3].texCoord);
}
return quad;
}
Ptr<Sprite> Sprite::createFromNpk(const std::string& imgPath, size_t frameIndex) {
NpkArchive& npk = NpkArchive::get();
auto imgOpt = npk.getImg(imgPath);
if (imgOpt) {
auto frameOpt = npk.getImageFrame(*imgOpt, frameIndex);
if (frameOpt && !frameOpt->data.empty()) {
const ImageFrame& frame = *frameOpt;
std::vector<uint8> convertedData(frame.data.size());
for (size_t i = 0; i < frame.data.size(); i += 4) {
uint8 b = frame.data[i + 0];
uint8 g = frame.data[i + 1];
uint8 r = frame.data[i + 2];
uint8 a = frame.data[i + 3];
convertedData[i + 0] = r;
convertedData[i + 1] = g;
convertedData[i + 2] = b;
convertedData[i + 3] = a;
}
auto sprite = createFromMemory(
convertedData.data(),
frame.width,
frame.height,
4
);
if (sprite) {
sprite->SetOffset(static_cast<float>(frame.xPos), static_cast<float>(frame.yPos));
}
return sprite;
}
}
const std::string& defaultPath = npk.getDefaultImgPath();
if (!defaultPath.empty()) {
auto defaultImgOpt = npk.getImg(defaultPath);
if (defaultImgOpt) {
auto defaultFrameOpt = npk.getImageFrame(*defaultImgOpt, npk.getDefaultImgFrame());
if (defaultFrameOpt && !defaultFrameOpt->data.empty()) {
const ImageFrame& frame = *defaultFrameOpt;
std::vector<uint8> convertedData(frame.data.size());
for (size_t i = 0; i < frame.data.size(); i += 4) {
uint8 b = frame.data[i + 0];
uint8 g = frame.data[i + 1];
uint8 r = frame.data[i + 2];
uint8 a = frame.data[i + 3];
convertedData[i + 0] = r;
convertedData[i + 1] = g;
convertedData[i + 2] = b;
convertedData[i + 3] = a;
}
return createFromMemory(
convertedData.data(),
frame.width,
frame.height,
4
);
}
}
}
return nullptr;
}
}
@@ -0,0 +1,178 @@
#include <frostbite2D/2d/text_sprite.h>
#include <frostbite2D/graphics/font_manager.h>
#include <frostbite2D/graphics/texture.h>
#include <SDL2/SDL.h>
#include <SDL2/SDL_ttf.h>
#include <vector>
namespace frostbite2D {
namespace {
Uint32 readSurfacePixel(const SDL_Surface* surface, int x, int y) {
const uint8* row =
static_cast<const uint8*>(surface->pixels) + y * surface->pitch;
const uint8* pixel = row + x * surface->format->BytesPerPixel;
switch (surface->format->BytesPerPixel) {
case 1:
return *pixel;
case 2:
return *reinterpret_cast<const uint16*>(pixel);
case 3:
if (SDL_BYTEORDER == SDL_BIG_ENDIAN) {
return (pixel[0] << 16) | (pixel[1] << 8) | pixel[2];
}
return pixel[0] | (pixel[1] << 8) | (pixel[2] << 16);
case 4:
return *reinterpret_cast<const uint32*>(pixel);
default:
return 0;
}
}
} // namespace
TextSprite::TextSprite()
: Sprite() {
}
TextSprite::~TextSprite() {
}
Ptr<TextSprite> TextSprite::create() {
return MakePtr<TextSprite>();
}
Ptr<TextSprite> TextSprite::create(const std::string& text, const std::string& fontName) {
auto textSprite = MakePtr<TextSprite>();
textSprite->SetFont(fontName);
textSprite->SetText(text);
return textSprite;
}
void TextSprite::SetText(const std::string& text) {
if (text_ != text) {
text_ = text;
RenderText();
}
}
void TextSprite::SetFont(const std::string& fontName) {
if (fontName_ != fontName) {
fontName_ = fontName;
RenderText();
}
}
void TextSprite::SetTextColor(const Color& color) {
textColor_ = color;
RenderText();
}
void TextSprite::SetTextColor(float r, float g, float b, float a) {
textColor_ = Color(r, g, b, a);
RenderText();
}
void TextSprite::SetAlign(Align align) {
if (align_ != align) {
align_ = align;
RenderText();
}
}
void TextSprite::RenderText() {
if (text_.empty() || fontName_.empty()) {
return;
}
FontManager& fontMgr = FontManager::get();
TTF_Font* font = fontMgr.getFont(fontName_);
if (!font) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Font '%s' not found for text rendering", fontName_.c_str());
return;
}
SDL_Color sdlColor;
sdlColor.r = static_cast<Uint8>(textColor_.r * 255.0f);
sdlColor.g = static_cast<Uint8>(textColor_.g * 255.0f);
sdlColor.b = static_cast<Uint8>(textColor_.b * 255.0f);
sdlColor.a = static_cast<Uint8>(textColor_.a * 255.0f);
SDL_Surface* surface = TTF_RenderUTF8_Blended(font, text_.c_str(), sdlColor);
if (!surface) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to render text: %s", TTF_GetError());
return;
}
int width = surface->w;
int height = surface->h;
std::vector<uint8> rgbaData(width * height * 4);
if (SDL_MUSTLOCK(surface) && SDL_LockSurface(surface) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to lock text surface: %s",
SDL_GetError());
SDL_FreeSurface(surface);
return;
}
for (int y = 0; y < height; ++y) {
for (int x = 0; x < width; ++x) {
Uint8 r = 0;
Uint8 g = 0;
Uint8 b = 0;
Uint8 a = 0;
Uint32 pixel = readSurfacePixel(surface, x, y);
SDL_GetRGBA(pixel, surface->format, &r, &g, &b, &a);
size_t offset = static_cast<size_t>((y * width + x) * 4);
rgbaData[offset + 0] = r;
rgbaData[offset + 1] = g;
rgbaData[offset + 2] = b;
rgbaData[offset + 3] = a;
}
}
if (SDL_MUSTLOCK(surface)) {
SDL_UnlockSurface(surface);
}
SDL_FreeSurface(surface);
Ptr<Texture> texture = Texture::createFromMemory(
rgbaData.data(),
width,
height,
4
);
if (texture) {
texture->setSampling(TextureSampling::Linear);
SetTexture(texture);
SetSizeToTexture();
}
}
Vec2 TextSprite::GetTextSize() const {
if (text_.empty() || fontName_.empty()) {
return Vec2::Zero();
}
FontManager& fontMgr = FontManager::get();
TTF_Font* font = fontMgr.getFont(fontName_);
if (!font) {
return Vec2::Zero();
}
int w = 0;
int h = 0;
TTF_SizeUTF8(font, text_.c_str(), &w, &h);
return Vec2(static_cast<float>(w), static_cast<float>(h));
}
void TextSprite::updateTexture() {
RenderText();
}
}
@@ -0,0 +1,536 @@
#include <frostbite2D/animation/animation.h>
#include <frostbite2D/2d/sprite.h>
#include <frostbite2D/resource/npk_archive.h>
#include <frostbite2D/resource/pvf_archive.h>
#include <frostbite2D/types/type_math.h>
#include <SDL2/SDL.h>
#include <algorithm>
#include <cmath>
using namespace frostbite2D::animation;
namespace frostbite2D {
namespace {
struct BoundsAccumulator {
bool valid = false;
float minX = 0.0f;
float minY = 0.0f;
float maxX = 0.0f;
float maxY = 0.0f;
void includePoint(const Vec2& point) {
if (!valid) {
minX = maxX = point.x;
minY = maxY = point.y;
valid = true;
return;
}
minX = std::min(minX, point.x);
minY = std::min(minY, point.y);
maxX = std::max(maxX, point.x);
maxY = std::max(maxY, point.y);
}
void includeRect(const Rect& rect) {
if (rect.empty()) {
return;
}
includePoint(Vec2(rect.left(), rect.top()));
includePoint(Vec2(rect.right(), rect.top()));
includePoint(Vec2(rect.left(), rect.bottom()));
includePoint(Vec2(rect.right(), rect.bottom()));
}
Rect toRect() const {
if (!valid) {
return Rect();
}
return Rect(minX, minY, maxX - minX, maxY - minY);
}
};
Vec2 resolveFramePosition(const AniFrame& frameInfo, const Vec2& imageRate) {
return Vec2(frameInfo.imgPos.x * imageRate.x, frameInfo.imgPos.y * imageRate.y);
}
float resolveFrameRotation(const AniFrame& frameInfo) {
if (frameInfo.flag.count("IMAGE_ROTATE")) {
return std::get<float>(frameInfo.flag.at("IMAGE_ROTATE"));
}
return 0.0f;
}
Vec2 resolveImageRate(const AniFrame& frameInfo) {
if (frameInfo.flag.count("IMAGE_RATE")) {
return std::get<Vec2>(frameInfo.flag.at("IMAGE_RATE"));
}
return Vec2::One();
}
BlendMode resolveBlendMode(const AniFrame& frameInfo) {
if (frameInfo.flag.count("GRAPHIC_EFFECT_LINEARDODGE")) {
return BlendMode::Additive;
}
return BlendMode::Normal;
}
Rect transformRectBounds(const Rect& rect, const Transform2D& transform) {
BoundsAccumulator bounds;
bounds.includePoint(transform.transformPoint(Vec2(rect.left(), rect.top())));
bounds.includePoint(transform.transformPoint(Vec2(rect.right(), rect.top())));
bounds.includePoint(transform.transformPoint(Vec2(rect.left(), rect.bottom())));
bounds.includePoint(transform.transformPoint(Vec2(rect.right(), rect.bottom())));
return bounds.toRect();
}
bool computeAnimationFrameBounds(const Animation& animation,
int frameIndex,
int direction,
Rect& outBounds) {
if (frameIndex < 0 ||
frameIndex >= static_cast<int>(animation.frames_.size()) ||
frameIndex >= static_cast<int>(animation.spriteFrames_.size()) ||
frameIndex >= static_cast<int>(animation.spriteFrameOffsets_.size())) {
return false;
}
const AniFrame& frameInfo = animation.frames_[frameIndex];
const auto& sprite = animation.spriteFrames_[frameIndex];
if (!sprite) {
return false;
}
Vec2 spriteSize = sprite->GetSize();
if (spriteSize.x <= 0.0f || spriteSize.y <= 0.0f) {
return false;
}
Vec2 imageRate = resolveImageRate(frameInfo);
float rotation = resolveFrameRotation(frameInfo);
Vec2 framePos = resolveFramePosition(frameInfo, imageRate);
Vec2 offset = animation.spriteFrameOffsets_[frameIndex];
float mirroredRotation = rotation;
if (direction < 0) {
float visualWidth = spriteSize.x * std::abs(imageRate.x);
framePos.x = -framePos.x;
offset.x = -offset.x - visualWidth;
mirroredRotation = -rotation;
}
Transform2D transform =
Transform2D::translation(offset) *
Transform2D::translation(framePos) *
Transform2D::rotation(mirroredRotation) *
Transform2D::scaling(imageRate.x, imageRate.y);
outBounds = transformRectBounds(Rect(0.0f, 0.0f, spriteSize.x, spriteSize.y),
transform);
return !outBounds.empty();
}
void combineHash(uint64& seed, uint64 value) {
constexpr uint64 kOffset = 0x9e3779b97f4a7c15ULL;
seed ^= value + kOffset + (seed << 6) + (seed >> 2);
}
} // namespace
Animation::Animation() {
}
Animation::Animation(const std::string& aniPath) {
Init(aniPath);
}
Animation::Animation(const std::string& aniPath,
std::function<std::string(std::string, ReplaceData)> additionalOptions,
ReplaceData data)
: additionalOptions_(additionalOptions)
, additionalOptionsData_(data) {
Init(aniPath);
}
Animation::~Animation() {
}
void Animation::Init(const std::string& aniPath) {
auto info = animation::parseAniFromPvf(aniPath);
if (!info) {
SDL_Log("Failed to load animation: %s", aniPath.c_str());
usable_ = false;
return;
}
aniPath_ = aniPath;
animationFlag_ = info->flag;
frames_ = std::move(info->frames);
spriteFrames_.clear();
spriteFrameOffsets_.clear();
if (animationFlag_.count("LOOP")) {
isLooping_ = true;
} else {
isLooping_ = false;
}
for (size_t i = 0; i < frames_.size(); i++) {
AniFrame& frameObj = frames_[i];
Ptr<Sprite> spriteObj = nullptr;
if (!frameObj.imgPath.empty()) {
if (additionalOptions_) {
frameObj.imgPath = additionalOptions_(frameObj.imgPath, additionalOptionsData_);
}
auto sprite = Sprite::createFromNpk(frameObj.imgPath, frameObj.imgIndex);
if (sprite) {
sprite->SetPosition(frameObj.imgPos);
sprite->SetVisible(false);
spriteObj = sprite;
}
}
if (!spriteObj) {
spriteObj = MakePtr<Sprite>();
spriteObj->SetVisible(false);
}
spriteFrames_.push_back(spriteObj);
spriteFrameOffsets_.push_back(spriteObj->GetOffset());
auto spriteSize = spriteObj->GetSize();
if (maxSize_.x < spriteSize.x) maxSize_.x = spriteSize.x;
if (maxSize_.y < spriteSize.y) maxSize_.y = spriteSize.y;
AddChild(spriteObj);
}
if (currentFrameTime_ == 0.0f && !spriteFrames_.empty()) {
SetSize(spriteFrames_[0]->GetSize());
}
totalFrameCount_ = static_cast<int>(frames_.size());
auto alsPath = aniPath + ".als";
if (PvfArchive::get().hasFile(alsPath)) {
auto alsInfo = animation::parseAlsFromPvf(alsPath);
if (alsInfo && !alsInfo->aniList.empty()) {
size_t lastSlash = aniPath.find_last_of('/');
std::string dir = (lastSlash != std::string::npos) ? aniPath.substr(0, lastSlash + 1) : "";
for (auto& ani : alsInfo->aniList) {
std::string subAniPath = dir + ani.second.path;
auto subAni = MakePtr<Animation>(subAniPath);
if (!subAni || !subAni->IsUsable()) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Animation: failed to load ALS child animation %s",
subAniPath.c_str());
continue;
}
if (ani.second.layer.size() >= 2) {
subAni->SetZOrder(ani.second.layer[1]);
}
subAni->SetDirection(direction_);
AddChild(subAni);
}
}
}
FlushFrame(0);
}
void Animation::OnUpdate(float deltaTime) {
if (!IsVisible()) {
for (auto& sp : spriteFrames_) {
sp->SetVisible(false);
}
return;
}
if (!usable_) {
if (currentFrame_) {
currentFrame_->SetVisible(true);
}
return;
}
float dtMs = deltaTime * 1000.0f;
currentFrameTime_ += dtMs;
while (currentFrameTime_ >= nextFrameDelay_) {
currentFrameTime_ -= nextFrameDelay_;
if (currentFrameIndex_ < (totalFrameCount_ - 1)) {
FlushFrame(currentFrameIndex_ + 1);
} else {
if (isLooping_) {
FlushFrame(0);
} else {
usable_ = false;
currentFrameTime_ = 0.0f;
if (currentFrame_) {
currentFrame_->SetVisible(true);
}
if (endCallback_) {
endCallback_();
}
break;
}
}
}
}
void Animation::OnAdded(Actor* parent) {
FlushFrame(0);
}
void Animation::SetVisible(bool visible) {
if (visible) {
if (currentFrame_) {
currentFrame_->SetVisible(true);
}
}
Actor::SetVisible(visible);
}
void Animation::FlushFrame(int index) {
if (currentFrame_) {
currentFrame_->SetVisible(false);
}
currentFrameIndex_ = index;
currentFrame_ = spriteFrames_[currentFrameIndex_];
currentFrame_->SetVisible(true);
AniFrame& frameInfo = frames_[currentFrameIndex_];
nextFrameDelay_ = static_cast<float>(frameInfo.delay);
auto& flagBuf = frameInfo.flag;
if (flagBuf.count("SET_FLAG")) {
if (changeFrameCallback_) {
auto flagValue = std::get<int>(flagBuf.at("SET_FLAG"));
changeFrameCallback_(flagValue);
}
}
if (flagBuf.count("PLAY_SOUND")) {
}
Vec2 imageRate = resolveImageRate(frameInfo);
float rotation = resolveFrameRotation(frameInfo);
BlendMode blendMode = resolveBlendMode(frameInfo);
Vec2 framePos = resolveFramePosition(frameInfo, imageRate);
ApplyFramePresentation(framePos, imageRate, rotation, blendMode);
if (flagBuf.count("INTERPOLATION")) {
if (interpolationData_.empty()) {
interpolationData_.push_back(frames_[currentFrameIndex_]);
if (currentFrameIndex_ + 1 < static_cast<int>(frames_.size())) {
interpolationData_.push_back(frames_[currentFrameIndex_ + 1]);
}
}
} else {
if (!interpolationData_.empty()) {
interpolationData_.clear();
}
}
SetSize(currentFrame_->GetSize());
}
void Animation::Reset() {
usable_ = true;
currentFrameTime_ = 0.0f;
FlushFrame(0);
}
AniFrame Animation::GetCurrentFrameInfo() const {
return frames_[currentFrameIndex_];
}
void Animation::SetFrameIndex(int index) {
FlushFrame(index);
currentFrameTime_ = 0.0f;
}
void Animation::SetDirection(int direction) {
direction_ = direction >= 0 ? 1 : -1;
if (currentFrame_ && currentFrameIndex_ >= 0 &&
currentFrameIndex_ < static_cast<int>(frames_.size())) {
const AniFrame& frameInfo = frames_[currentFrameIndex_];
Vec2 imageRate = resolveImageRate(frameInfo);
float rotation = resolveFrameRotation(frameInfo);
BlendMode blendMode = resolveBlendMode(frameInfo);
Vec2 framePos = resolveFramePosition(frameInfo, imageRate);
ApplyFramePresentation(framePos, imageRate, rotation, blendMode);
}
for (const auto& child : GetChildren()) {
auto* subAnimation = dynamic_cast<Animation*>(child.Get());
if (subAnimation) {
subAnimation->SetDirection(direction_);
}
}
}
void Animation::InterpolationLogic() {
if (interpolationData_.empty()) {
return;
}
float interRate = math::lerp(0.0f, 1.0f, currentFrameTime_ / nextFrameDelay_);
AniFrame& oldData = interpolationData_[0];
AniFrame& newData = interpolationData_[1];
{
std::vector<int> oldRgbaData = {255, 255, 255, 250};
std::vector<int> newRgbaData = {255, 255, 255, 250};
if (oldData.flag.count("RGBA")) {
oldRgbaData = std::get<std::vector<int>>(oldData.flag.at("RGBA"));
}
if (newData.flag.count("RGBA")) {
newRgbaData = std::get<std::vector<int>>(newData.flag.at("RGBA"));
}
std::vector<int> rgbaData = {
static_cast<int>(oldRgbaData[0] + (newRgbaData[0] - oldRgbaData[0]) * interRate),
static_cast<int>(oldRgbaData[1] + (newRgbaData[1] - oldRgbaData[1]) * interRate),
static_cast<int>(oldRgbaData[2] + (newRgbaData[2] - oldRgbaData[2]) * interRate),
static_cast<int>(oldRgbaData[3] + (newRgbaData[3] - oldRgbaData[3]) * interRate)};
currentFrame_->SetColor(
static_cast<float>(rgbaData[0]) / 255.0f,
static_cast<float>(rgbaData[1]) / 255.0f,
static_cast<float>(rgbaData[2]) / 255.0f,
static_cast<float>(rgbaData[3]) / 255.0f);
}
{
Vec2 oldRateData = Vec2::One();
Vec2 newRateData = Vec2::One();
if (oldData.flag.count("IMAGE_RATE")) {
oldRateData = std::get<Vec2>(oldData.flag.at("IMAGE_RATE"));
}
if (newData.flag.count("IMAGE_RATE")) {
newRateData = std::get<Vec2>(newData.flag.at("IMAGE_RATE"));
}
Vec2 rateData = Vec2::lerp(oldRateData, newRateData, interRate);
float oldAngleData = 0.0f;
float newAngleData = 0.0f;
if (oldData.flag.count("IMAGE_ROTATE")) {
oldAngleData = std::get<float>(oldData.flag.at("IMAGE_ROTATE"));
}
if (newData.flag.count("IMAGE_ROTATE")) {
newAngleData = std::get<float>(newData.flag.at("IMAGE_ROTATE"));
}
float angleData = math::lerp(oldAngleData, newAngleData, interRate);
Vec2 posData = Vec2::lerp(oldData.imgPos, newData.imgPos, interRate);
Vec2 framePos(posData.x * rateData.x, posData.y * rateData.y);
ApplyFramePresentation(framePos, rateData, angleData, currentFrame_->GetBlendMode());
}
}
Vec2 Animation::GetMaxSize() const {
return maxSize_;
}
bool Animation::GetStaticLocalBounds(Rect& outBounds) const {
BoundsAccumulator bounds;
Rect directionalBounds;
if (GetStaticLocalBounds(1, directionalBounds)) {
bounds.includeRect(directionalBounds);
}
if (GetStaticLocalBounds(-1, directionalBounds)) {
bounds.includeRect(directionalBounds);
}
outBounds = bounds.toRect();
return bounds.valid;
}
bool Animation::GetStaticLocalBounds(int direction, Rect& outBounds) const {
BoundsAccumulator bounds;
for (int i = 0; i < static_cast<int>(frames_.size()); ++i) {
Rect frameBounds;
if (computeAnimationFrameBounds(*this, i, direction, frameBounds)) {
bounds.includeRect(frameBounds);
}
}
for (const auto& child : GetChildren()) {
auto* subAnimation = dynamic_cast<Animation*>(child.Get());
if (!subAnimation) {
continue;
}
Rect childBounds;
if (!subAnimation->GetStaticLocalBounds(direction, childBounds)) {
continue;
}
bounds.includeRect(transformRectBounds(childBounds, subAnimation->GetLocalTransform()));
}
outBounds = bounds.toRect();
return bounds.valid;
}
uint64 Animation::GetRenderSignature() const {
uint64 signature = 1469598103934665603ULL;
combineHash(signature, static_cast<uint64>(usable_));
combineHash(signature, static_cast<uint64>(direction_ >= 0 ? 1 : 0));
combineHash(signature, static_cast<uint64>(std::max(currentFrameIndex_, 0)));
combineHash(signature, static_cast<uint64>(std::max(totalFrameCount_, 0)));
for (const auto& child : GetChildren()) {
auto* subAnimation = dynamic_cast<Animation*>(child.Get());
if (!subAnimation) {
continue;
}
combineHash(signature, subAnimation->GetRenderSignature());
}
return signature;
}
void Animation::ApplyFramePresentation(const Vec2& framePos,
const Vec2& imageRate,
float rotation,
BlendMode blendMode) {
if (!currentFrame_ || currentFrameIndex_ < 0 ||
currentFrameIndex_ >= static_cast<int>(spriteFrameOffsets_.size())) {
return;
}
currentFrame_->SetScale(imageRate);
currentFrame_->SetBlendMode(blendMode);
currentFrame_->SetFlippedX(direction_ < 0);
Vec2 offset = spriteFrameOffsets_[currentFrameIndex_];
Vec2 position = framePos;
float mirroredRotation = rotation;
if (direction_ < 0) {
// 朝左时要同时镜像帧位置和素材锚点偏移,避免多层时装各自翻转后错位。
float visualWidth = currentFrame_->GetSize().x * std::abs(imageRate.x);
position.x = -position.x;
offset.x = -offset.x - visualWidth;
mirroredRotation = -rotation;
}
currentFrame_->SetOffset(offset);
currentFrame_->SetPosition(position);
currentFrame_->SetRotation(mirroredRotation);
}
} // namespace frostbite2D
@@ -0,0 +1,416 @@
#include <frostbite2D/animation/animation_data.h>
#include <frostbite2D/resource/binary_reader.h>
#include <frostbite2D/resource/pvf_archive.h>
#include <frostbite2D/resource/script_parser.h>
#include <algorithm>
#include <cctype>
#include <sstream>
namespace frostbite2D {
namespace animation {
// ---------------------------------------------------------------------------
// 工具函数实现
// ---------------------------------------------------------------------------
static std::string toLowerCase(const std::string& str) {
std::string result = str;
std::transform(result.begin(), result.end(), result.begin(),
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
return result;
}
std::string getAniFlag(int type) {
switch (type) {
case 0: return {"LOOP"};
case 1: return {"SHADOW"};
case 3: return {"COORD"};
case 7: return {"IMAGE_RATE"};
case 8: return {"IMAGE_ROTATE"};
case 9: return {"RGBA"};
case 10: return {"INTERPOLATION"};
case 11: return {"GRAPHIC_EFFECT"};
case 12: return {"DELAY"};
case 13: return {"DAMAGE_TYPE"};
case 14: return {"DAMAGE_BOX"};
case 15: return {"ATTACK_BOX"};
case 16: return {"PLAY_SOUND"};
case 17: return {"PRELOAD"};
case 18: return {"SPECTRUM"};
case 23: return {"SET_FLAG"};
case 24: return {"FLIP_TYPE"};
case 25: return {"LOOP_START"};
case 26: return {"LOOP_END"};
case 27: return {"CLIP"};
case 28: return {"OPERATION"};
default: return {};
}
}
std::string getAniEffectType(int type) {
switch (type) {
case 0: return {"NONE"};
case 1: return {"DODGE"};
case 2: return {"LINEARDODGE"};
case 3: return {"DARK"};
case 4: return {"XOR"};
case 5: return {"MONOCHROME"};
case 6: return {"SPACEDISTORT"};
default: return {};
}
}
std::string getAniFlipType(int type) {
switch (type) {
case 1: return {"HORIZON"};
case 2: return {"VERTICAL"};
case 3: return {"ALL"};
default: return {};
}
}
std::string getAniDamageType(int type) {
switch (type) {
case 0: return {"NORMAL"};
case 1: return {"SUPERARMOR"};
case 2: return {"UNBREAKABLE"};
default: return {};
}
}
// ---------------------------------------------------------------------------
// 辅助函数
// ---------------------------------------------------------------------------
static uint8 read256(BinaryReader& reader) {
uint8 value = reader.readUInt8();
return value == 255 ? 256 : value;
}
static uint16 readUShort(BinaryReader& reader) {
return reader.readUInt16();
}
static int16 readShort(BinaryReader& reader) {
return reader.readInt16();
}
// ---------------------------------------------------------------------------
// 解析函数实现
// ---------------------------------------------------------------------------
AniInfo parseAniInfo(const char* data, size_t size) {
AniInfo info;
BinaryReader reader(data, size);
if (!reader.isOpen() || reader.size() < 4) {
return info;
}
uint16 frameMax = reader.readUInt16();
uint16 imgCount = reader.readUInt16();
for (uint16 i = 0; i < imgCount; i++) {
int buf = reader.readInt32();
std::string imgPath = "sprite/" + reader.readString(buf);
info.imgList.push_back(toLowerCase(imgPath));
}
uint16 aniHeaderItemCount = reader.readUInt16();
for (uint16 i = 0; i < aniHeaderItemCount; i++) {
uint16 type = reader.readUInt16();
switch (type) {
case 0:
case 1: {
std::string key = getAniFlag(type);
int value = reader.readUInt8();
if (!key.empty()) {
info.flag.emplace(key, value);
}
break;
}
case 3:
case 28: {
std::string key = getAniFlag(type);
int value = reader.readUInt16();
if (!key.empty()) {
info.flag.emplace(key, value);
}
break;
}
case 18: {
reader.readUInt8();
reader.readInt32();
reader.readInt32();
reader.readInt32();
read256(reader);
read256(reader);
read256(reader);
read256(reader);
reader.readUInt16();
break;
}
default:
break;
}
}
for (uint16 i = 0; i < frameMax; i++) {
AniFrame frame;
uint16 boxItemCount = reader.readUInt16();
for (uint16 j = 0; j < boxItemCount; j++) {
uint16 boxType = reader.readUInt16();
std::vector<int> boxData;
for (int k = 0; k < 6; k++) {
boxData.push_back(reader.readInt32());
}
if (boxType == 15) {
frame.attackBox.push_back(boxData);
} else {
frame.damageBox.push_back(boxData);
}
}
int16 indexBuf = reader.readInt16();
if (indexBuf != -1) {
frame.imgPath = info.imgList[indexBuf];
frame.imgIndex = reader.readUInt16();
} else {
frame.imgPath = "";
frame.imgIndex = 0;
}
frame.imgPos.x = static_cast<float>(reader.readInt32());
frame.imgPos.y = static_cast<float>(reader.readInt32());
uint16 imgFlagCount = reader.readUInt16();
for (uint16 j = 0; j < imgFlagCount; j++) {
uint16 imgFlagType = reader.readUInt16();
std::string key;
int value;
switch (imgFlagType) {
case 0:
case 1:
case 10: {
key = getAniFlag(imgFlagType);
value = reader.readUInt8();
if (!key.empty()) {
frame.flag.emplace(key, value);
}
break;
}
case 3: {
key = "COORD";
value = reader.readUInt16();
frame.flag.emplace(key, value);
break;
}
case 17: {
key = "PRELOAD";
value = 1;
frame.flag.emplace(key, value);
break;
}
case 7: {
key = "IMAGE_RATE";
Vec2 rate{reader.readFloat(), reader.readFloat()};
frame.flag.emplace(key, rate);
break;
}
case 8: {
key = "IMAGE_ROTATE";
float rateBuffer = reader.readFloat();
frame.flag.emplace(key, rateBuffer);
break;
}
case 9: {
key = "RGBA";
std::vector<int> rgba = {
static_cast<int>(read256(reader)),
static_cast<int>(read256(reader)),
static_cast<int>(read256(reader)),
static_cast<int>(read256(reader))};
frame.flag.emplace(key, rgba);
break;
}
case 11: {
uint16 effectType = reader.readUInt16();
key = "GRAPHIC_EFFECT_" + getAniEffectType(effectType);
std::vector<float> effect;
switch (effectType) {
case 5:
effect.push_back(static_cast<float>(read256(reader)));
effect.push_back(static_cast<float>(read256(reader)));
effect.push_back(static_cast<float>(read256(reader)));
break;
case 6:
effect.push_back(static_cast<float>(read256(reader)));
effect.push_back(static_cast<float>(read256(reader)));
break;
}
frame.flag.emplace(key, effect);
break;
}
case 12: {
value = reader.readInt32();
frame.delay = value;
break;
}
case 13: {
key = "DAMAGE_TYPE";
std::string dtype = getAniDamageType(reader.readUInt16());
frame.flag.emplace(key, dtype);
break;
}
case 16: {
int soundSize = reader.readInt32();
key = "PLAY_SOUND";
std::string sound = reader.readString(soundSize);
frame.flag.emplace(key, sound);
break;
}
case 23: {
key = "SET_FLAG";
value = reader.readInt32();
frame.flag.emplace(key, value);
break;
}
case 24: {
key = "FLIP_TYPE";
std::string ftValue = getAniFlipType(reader.readUInt16());
frame.flag.emplace(key, ftValue);
break;
}
case 25: {
key = "LOOP_START";
frame.flag.emplace(key, 1);
break;
}
case 26: {
key = "LOOP_END";
value = reader.readInt32();
frame.flag.emplace(key, value);
break;
}
case 27: {
key = "CLIP";
std::vector<int> clipArr = {
reader.readInt16(),
reader.readInt16(),
reader.readInt16(),
reader.readInt16()};
frame.flag.emplace(key, clipArr);
break;
}
default:
break;
}
}
info.frames.push_back(frame);
}
return info;
}
std::optional<AniInfo> parseAniFromPvf(const std::string& path) {
auto& pvf = PvfArchive::get();
if (!pvf.isOpen()) {
return std::nullopt;
}
auto rawData = pvf.getFileRawData(path);
if (!rawData) {
return std::nullopt;
}
return parseAniInfo(rawData->data.get(), rawData->size);
}
AlsInfo parseAlsInfo(const std::string& data) {
AlsInfo info;
std::istringstream stream(data);
std::string line;
while (std::getline(stream, line)) {
if (line.empty()) continue;
std::istringstream lineStream(line);
std::string segment;
lineStream >> segment;
if (segment == "[use animation]") {
std::string aniPath, aniKey;
lineStream >> aniPath >> aniKey;
info.aniList[aniKey].path = toLowerCase(aniPath);
} else if (segment == "[none effect add]") {
int layer1, layer2;
std::string aniKey;
lineStream >> layer1 >> layer2 >> aniKey;
info.aniList[aniKey].layer = {layer1, layer2};
} else if (segment == "[add]") {
int layer1, layer2;
std::string aniKey;
lineStream >> layer1 >> layer2 >> aniKey;
info.aniList[aniKey].layer = {layer1, layer2};
}
}
return info;
}
std::optional<AlsInfo> parseAlsFromPvf(const std::string& path) {
auto& pvf = PvfArchive::get();
if (!pvf.isOpen()) {
return std::nullopt;
}
auto rawData = pvf.getFileRawData(path);
if (!rawData) {
return std::nullopt;
}
ScriptParser parser(*rawData, path);
if (!parser.isValid()) {
return std::nullopt;
}
AlsInfo info;
while (!parser.isEnd()) {
auto segmentValue = parser.next();
if (!segmentValue || segmentValue->type != ScriptValueType::String) {
continue;
}
std::string segment = toLowerCase(segmentValue->toString());
if (segment == "[use animation]") {
auto aniPath = parser.next();
auto aniKey = parser.next();
if (!aniPath || !aniKey) {
break;
}
info.aniList[aniKey->toString()].path = toLowerCase(aniPath->toString());
} else if (segment == "[none effect add]" || segment == "[add]") {
auto layer1 = parser.next();
auto layer2 = parser.next();
auto aniKey = parser.next();
if (!layer1 || !layer2 || !aniKey) {
break;
}
info.aniList[aniKey->toString()].layer = {
std::stoi(layer1->toString()),
std::stoi(layer2->toString())};
}
}
return info;
}
} // namespace animation
} // namespace frostbite2D
@@ -0,0 +1,116 @@
#include <frostbite2D/audio/audio_system.h>
#include <SDL2/SDL.h>
#include <SDL2/SDL_mixer.h>
#include <algorithm>
namespace frostbite2D {
AudioSystem& AudioSystem::get() {
static AudioSystem instance;
return instance;
}
// 移除析构函数中的自动销毁,改为在 Application::shutdown() 中手动调用
bool AudioSystem::init(const AudioConfig& config) {
if (initialized_) {
return true;
}
if (SDL_InitSubSystem(SDL_INIT_AUDIO) < 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to initialize SDL audio: %s", SDL_GetError());
return false;
}
int frequency = config.frequency;
uint16_t format = config.format;
int channels = config.channels;
if (Mix_OpenAudio(frequency, format, channels, config.chunksize) < 0) {
if (Mix_OpenAudio(44100, MIX_DEFAULT_FORMAT, 2, config.chunksize) < 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to open audio: %s", Mix_GetError());
SDL_QuitSubSystem(SDL_INIT_AUDIO);
return false;
}
}
maxChannels_ = config.maxSoundChannels;
Mix_AllocateChannels(maxChannels_);
initialized_ = true;
updateVolumes();
return true;
}
void AudioSystem::shutdown() {
if (!initialized_) {
return;
}
Mix_HaltMusic();
Mix_HaltChannel(-1);
Mix_CloseAudio();
SDL_QuitSubSystem(SDL_INIT_AUDIO);
initialized_ = false;
}
void AudioSystem::setMasterVolume(float volume) {
masterVolume_ = std::clamp(volume, 0.0f, 1.0f);
updateVolumes();
}
float AudioSystem::getMasterVolume() const {
return masterVolume_;
}
void AudioSystem::setSoundVolume(float volume) {
soundVolume_ = std::clamp(volume, 0.0f, 1.0f);
updateVolumes();
}
float AudioSystem::getSoundVolume() const {
return soundVolume_;
}
void AudioSystem::setMusicVolume(float volume) {
musicVolume_ = std::clamp(volume, 0.0f, 1.0f);
updateVolumes();
}
float AudioSystem::getMusicVolume() const {
return musicVolume_;
}
void AudioSystem::updateVolumes() {
if (!initialized_) {
return;
}
int musicVol = static_cast<int>(musicVolume_ * masterVolume_ * MIX_MAX_VOLUME);
Mix_VolumeMusic(musicVol);
}
void AudioSystem::pauseAllSounds() {
if (!initialized_) {
return;
}
Mix_Pause(-1);
}
void AudioSystem::resumeAllSounds() {
if (!initialized_) {
return;
}
Mix_Resume(-1);
}
void AudioSystem::stopAllSounds() {
if (!initialized_) {
return;
}
Mix_HaltChannel(-1);
}
}
+269
View File
@@ -0,0 +1,269 @@
#include <frostbite2D/audio/music.h>
#include <frostbite2D/audio/audio_system.h>
#include <frostbite2D/resource/asset.h>
#include <frostbite2D/resource/sound_pack_archive.h>
#include <SDL2/SDL.h>
#include <SDL2/SDL_mixer.h>
#include <algorithm>
namespace frostbite2D {
namespace {
Mix_Music* gCurrentMusicHandle = nullptr;
std::string gCurrentMusicPath;
std::string normalizeAudioLogicalPath(const std::string& path) {
std::string normalized = path;
std::replace(normalized.begin(), normalized.end(), '\\', '/');
std::transform(normalized.begin(), normalized.end(), normalized.begin(),
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
return normalized;
}
bool startsWithPrefix(const std::string& value, const char* prefix) {
const size_t prefixLength = std::char_traits<char>::length(prefix);
return value.size() >= prefixLength &&
value.compare(0, prefixLength, prefix) == 0;
}
std::string canonicalizeAudioPlaybackPath(const std::string& path) {
std::string normalized = normalizeAudioLogicalPath(path);
if (startsWithPrefix(normalized, "assets/music/") ||
startsWithPrefix(normalized, "assets/sounds/")) {
return normalized.substr(7);
}
return normalized;
}
bool hasActiveMusicPlayback() {
return Mix_PlayingMusic() == 1 || Mix_PausedMusic() == 1;
}
void clearCurrentMusicState() {
gCurrentMusicHandle = nullptr;
gCurrentMusicPath.clear();
}
void clearCurrentMusicStateIfStopped() {
if (!hasActiveMusicPlayback()) {
clearCurrentMusicState();
}
}
} // namespace
Ptr<Music> Music::loadFromPath(const std::string& path) {
std::string logicalPath = normalizeAudioLogicalPath(path);
if (startsWithPrefix(logicalPath, "music/")) {
return loadFromFile("assets/" + logicalPath);
}
if (startsWithPrefix(logicalPath, "sounds/")) {
return loadFromNpk(logicalPath);
}
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Unsupported audio path prefix for music: %s", path.c_str());
return nullptr;
}
Ptr<Music> Music::loadFromFile(const std::string& path) {
auto& asset = Asset::get();
std::string fullPath = asset.resolvePath(path);
Mix_Music* music = Mix_LoadMUS(fullPath.c_str());
if (!music) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to load music from file %s: %s", fullPath.c_str(), Mix_GetError());
return nullptr;
}
return Ptr<Music>(new Music(music, path));
}
Ptr<Music> Music::loadFromMemory(const uint8* data, size_t size) {
std::vector<uint8> dataCopy(data, data + size);
SDL_RWops* rw = SDL_RWFromConstMem(dataCopy.data(), static_cast<int>(dataCopy.size()));
if (!rw) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to create RWops from memory: %s", SDL_GetError());
return nullptr;
}
Mix_Music* music = Mix_LoadMUS_RW(rw, 1);
if (!music) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to load music from memory: %s", Mix_GetError());
return nullptr;
}
return Ptr<Music>(new Music(music, std::move(dataCopy)));
}
Ptr<Music> Music::loadFromNpk(const std::string& audioPath) {
SoundPackArchive& archive = SoundPackArchive::get();
std::string normalizedPath = normalizeAudioLogicalPath(audioPath);
if (!archive.isOpen()) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SoundPackArchive not initialized");
return nullptr;
}
auto audioOpt = archive.getAudio(normalizedPath);
if (!audioOpt) {
std::string defaultPath = archive.getDefaultAudioPath();
if (!defaultPath.empty()) {
audioOpt = archive.getAudio(defaultPath);
}
if (!audioOpt) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Audio not found in NPK: %s", audioPath.c_str());
return nullptr;
}
}
auto dataOpt = archive.getAudioData(*audioOpt);
if (!dataOpt || dataOpt->empty()) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to get audio data for: %s", audioPath.c_str());
return nullptr;
}
Ptr<Music> music = loadFromMemory(dataOpt->data(), dataOpt->size());
if (music) {
music->path_ = canonicalizeAudioPlaybackPath(normalizedPath);
}
return music;
}
bool Music::isPathPlaying(const std::string& path) {
clearCurrentMusicStateIfStopped();
return hasActiveMusicPlayback() &&
gCurrentMusicPath == canonicalizeAudioPlaybackPath(path);
}
std::string Music::getCurrentPlayingPath() {
clearCurrentMusicStateIfStopped();
return gCurrentMusicPath;
}
Music::Music(Mix_Music* music, const std::string& path)
: music_(music), path_(canonicalizeAudioPlaybackPath(path)) {
}
Music::Music(Mix_Music* music, std::vector<uint8> data, const std::string& path)
: music_(music), path_(canonicalizeAudioPlaybackPath(path)),
data_(std::move(data)) {
}
Music::~Music() {
if (gCurrentMusicHandle == music_) {
clearCurrentMusicState();
}
if (music_) {
Mix_FreeMusic(music_);
music_ = nullptr;
}
}
bool Music::play(int loops) {
if (!music_) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Music is null");
return false;
}
if (!AudioSystem::get().isInitialized()) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Audio system not initialized");
return false;
}
int vol = static_cast<int>(volume_ * AudioSystem::get().getMusicVolume() * AudioSystem::get().getMasterVolume() * MIX_MAX_VOLUME);
Mix_VolumeMusic(vol);
if (Mix_PlayMusic(music_, loops) == 0) {
gCurrentMusicHandle = music_;
gCurrentMusicPath = path_;
return true;
} else {
clearCurrentMusicStateIfStopped();
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to play music: %s", Mix_GetError());
return false;
}
}
bool Music::fadeIn(int ms, int loops) {
if (!music_) {
return false;
}
if (!AudioSystem::get().isInitialized()) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Audio system not initialized");
return false;
}
int vol = static_cast<int>(volume_ * AudioSystem::get().getMusicVolume() * AudioSystem::get().getMasterVolume() * MIX_MAX_VOLUME);
Mix_VolumeMusic(vol);
if (Mix_FadeInMusic(music_, loops, ms) == 0) {
gCurrentMusicHandle = music_;
gCurrentMusicPath = path_;
return true;
}
clearCurrentMusicStateIfStopped();
return false;
}
void Music::pause() {
if (!music_) {
return;
}
Mix_PauseMusic();
}
void Music::resume() {
if (!music_) {
return;
}
Mix_ResumeMusic();
}
void Music::stop() {
if (!music_) {
return;
}
Mix_HaltMusic();
if (gCurrentMusicHandle == music_) {
clearCurrentMusicState();
}
}
bool Music::fadeOut(int ms) {
if (!music_) {
return false;
}
bool result = Mix_FadeOutMusic(ms) == 0;
if (result && gCurrentMusicHandle == music_) {
clearCurrentMusicState();
}
return result;
}
void Music::setVolume(float volume) {
volume_ = std::clamp(volume, 0.0f, 1.0f);
if (AudioSystem::get().isInitialized() && isPlaying()) {
int vol = static_cast<int>(volume_ * AudioSystem::get().getMusicVolume() * AudioSystem::get().getMasterVolume() * MIX_MAX_VOLUME);
Mix_VolumeMusic(vol);
}
}
float Music::getVolume() const {
return volume_;
}
bool Music::isPlaying() const {
return Mix_PlayingMusic() == 1;
}
bool Music::isPaused() const {
return Mix_PausedMusic() == 1;
}
}
+189
View File
@@ -0,0 +1,189 @@
#include <frostbite2D/audio/sound.h>
#include <frostbite2D/audio/audio_system.h>
#include <frostbite2D/resource/asset.h>
#include <frostbite2D/resource/sound_pack_archive.h>
#include <SDL2/SDL.h>
#include <SDL2/SDL_mixer.h>
#include <algorithm>
namespace frostbite2D {
namespace {
std::string normalizeAudioLogicalPath(const std::string& path) {
std::string normalized = path;
std::replace(normalized.begin(), normalized.end(), '\\', '/');
std::transform(normalized.begin(), normalized.end(), normalized.begin(),
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
return normalized;
}
bool startsWithPrefix(const std::string& value, const char* prefix) {
const size_t prefixLength = std::char_traits<char>::length(prefix);
return value.size() >= prefixLength &&
value.compare(0, prefixLength, prefix) == 0;
}
} // namespace
Ptr<Sound> Sound::loadFromPath(const std::string& path) {
std::string logicalPath = normalizeAudioLogicalPath(path);
if (startsWithPrefix(logicalPath, "music/")) {
return loadFromFile("assets/" + logicalPath);
}
if (startsWithPrefix(logicalPath, "sounds/")) {
return loadFromNpk(logicalPath);
}
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Unsupported audio path prefix for sound: %s", path.c_str());
return nullptr;
}
Ptr<Sound> Sound::loadFromFile(const std::string& path) {
auto& asset = Asset::get();
std::string fullPath = asset.resolvePath(path);
Mix_Chunk* chunk = Mix_LoadWAV(fullPath.c_str());
if (!chunk) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to load sound from file %s: %s", fullPath.c_str(), Mix_GetError());
return nullptr;
}
return Ptr<Sound>(new Sound(chunk, path));
}
Ptr<Sound> Sound::loadFromMemory(const uint8* data, size_t size) {
SDL_RWops* rw = SDL_RWFromConstMem(data, static_cast<int>(size));
if (!rw) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to create RWops from memory: %s", SDL_GetError());
return nullptr;
}
Mix_Chunk* chunk = Mix_LoadWAV_RW(rw, 1);
if (!chunk) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to load sound from memory: %s", Mix_GetError());
return nullptr;
}
return Ptr<Sound>(new Sound(chunk));
}
Ptr<Sound> Sound::loadFromNpk(const std::string& audioPath) {
SoundPackArchive& archive = SoundPackArchive::get();
std::string normalizedPath = normalizeAudioLogicalPath(audioPath);
if (!archive.isOpen()) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SoundPackArchive not initialized");
return nullptr;
}
auto audioOpt = archive.getAudio(normalizedPath);
if (!audioOpt) {
std::string defaultPath = archive.getDefaultAudioPath();
if (!defaultPath.empty()) {
audioOpt = archive.getAudio(defaultPath);
}
if (!audioOpt) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Audio not found in NPK: %s", audioPath.c_str());
return nullptr;
}
}
auto dataOpt = archive.getAudioData(*audioOpt);
if (!dataOpt || dataOpt->empty()) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to get audio data for: %s", audioPath.c_str());
return nullptr;
}
return loadFromMemory(dataOpt->data(), dataOpt->size());
}
Sound::Sound(Mix_Chunk* chunk, const std::string& path)
: chunk_(chunk), path_(path) {
}
Sound::~Sound() {
if (chunk_) {
Mix_FreeChunk(chunk_);
chunk_ = nullptr;
}
}
int Sound::play(int loops, int channel) {
if (!chunk_) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Sound chunk is null");
return -1;
}
if (!AudioSystem::get().isInitialized()) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Audio system not initialized");
return -1;
}
int vol = static_cast<int>(volume_ * AudioSystem::get().getSoundVolume() * AudioSystem::get().getMasterVolume() * MIX_MAX_VOLUME);
Mix_VolumeChunk(chunk_, vol);
int result = Mix_PlayChannel(channel, chunk_, loops);
if (result == -1) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to play sound: %s", Mix_GetError());
} else {
lastChannel_ = result;
}
return result;
}
void Sound::setVolume(float volume) {
volume_ = std::clamp(volume, 0.0f, 1.0f);
}
float Sound::getVolume() const {
return volume_;
}
int Sound::getLastChannel() const {
return lastChannel_;
}
int Sound::playLoop(int channel) {
return play(-1, channel);
}
void Sound::stop(int channel) {
if (channel >= 0) {
Mix_HaltChannel(channel);
}
}
void Sound::stopAll() {
Mix_HaltChannel(-1);
}
bool Sound::isPlaying(int channel) {
if (channel < 0) {
return false;
}
return Mix_Playing(channel) == 1;
}
void Sound::pause(int channel) {
if (channel >= 0) {
Mix_Pause(channel);
}
}
void Sound::resume(int channel) {
if (channel >= 0) {
Mix_Resume(channel);
}
}
void Sound::pauseAll() {
Mix_Pause(-1);
}
void Sound::resumeAll() {
Mix_Resume(-1);
}
}
@@ -0,0 +1,24 @@
#include <frostbite2D/base/RefObject.h>
namespace frostbite2D {
RefObject::RefObject() : ref_count_(0) {}
RefObject::~RefObject() {}
void RefObject::Retain() {
++ref_count_;
}
void RefObject::Release() {
--ref_count_;
if (ref_count_ == 0) {
delete this;
}
}
uint32_t RefObject::GetRefCount() const {
return ref_count_.load();
}
}
@@ -0,0 +1,48 @@
#include <frostbite2D/base/object_registry.h>
#include <frostbite2D/2d/actor.h>
#include <frostbite2D/types/uuid.h>
namespace frostbite2D {
ObjectRegistry& ObjectRegistry::get() {
static ObjectRegistry instance;
return instance;
}
void ObjectRegistry::registerObject(const UUID& uuid, Actor* actor) {
registry_[uuid.toString()] = actor;
}
void ObjectRegistry::unregisterObject(const UUID& uuid) {
registry_.erase(uuid.toString());
}
Actor* ObjectRegistry::getObject(const UUID& uuid) {
return getObject(uuid.toString());
}
Actor* ObjectRegistry::getObject(const std::string& uuidStr) {
auto it = registry_.find(uuidStr);
if (it != registry_.end()) {
return it->second;
}
return nullptr;
}
bool ObjectRegistry::hasObject(const UUID& uuid) const {
return hasObject(uuid.toString());
}
bool ObjectRegistry::hasObject(const std::string& uuidStr) const {
return registry_.find(uuidStr) != registry_.end();
}
size_t ObjectRegistry::count() const {
return registry_.size();
}
void ObjectRegistry::clear() {
registry_.clear();
}
} // namespace frostbite2D
@@ -0,0 +1,634 @@
#include <SDL2/SDL.h>
#include <SDL2/SDL_events.h>
#include <algorithm>
#include <cstdio>
#include <limits>
#include <memory>
#include <frostbite2D/audio/audio_system.h>
#include <frostbite2D/base/object_registry.h>
#include <frostbite2D/core/application.h>
#include <frostbite2D/core/task_system.h>
#include <frostbite2D/event/event.h>
#include <frostbite2D/event/key_event.h>
#include <frostbite2D/event/mouse_event.h>
#include <frostbite2D/event/touch_event.h>
#include <frostbite2D/event/joystick_event.h>
#include <frostbite2D/event/window_event.h>
#include <frostbite2D/graphics/camera.h>
#include <frostbite2D/graphics/renderer.h>
#include <frostbite2D/platform/switch.h>
#include <frostbite2D/platform/windows.h>
#include <frostbite2D/resource/asset.h>
#include <frostbite2D/resource/npk_archive.h>
#include <frostbite2D/resource/pvf_archive.h>
#include <frostbite2D/resource/save_system.h>
#include <frostbite2D/resource/sound_pack_archive.h>
#include <frostbite2D/scene/scene.h>
#include <frostbite2D/scene/scene_manager.h>
#include <frostbite2D/types/type_math.h>
#include <frostbite2D/utils/startup_trace.h>
namespace frostbite2D {
Application &Application::get() {
static Application instance;
return instance;
}
bool Application::init() {
AppConfig cfg;
return init(cfg);
}
bool Application::init(const AppConfig& config) {
ScopedStartupTrace startupTrace("Application::init internals");
if (initialized_) {
return true;
}
config_ = config;
setMaxFps(config_.maxFps);
// 平台相关初始化
#ifdef __SWITCH__
{
ScopedStartupTrace stageTrace("switchInit");
switchInit();
}
#endif
#ifdef _WIN32
{
ScopedStartupTrace stageTrace("windowsInit");
windowsInit();
}
#endif
// 初始化核心模块
if (!initCoreModules()) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to initialize core modules");
shutdown();
return false;
}
initialized_ = true;
return true;
}
void Application::shutdown() {
if (!initialized_)
return;
running_ = false;
shouldQuit_ = true;
setTextInputEnabled(false);
TaskSystem::get().shutdown();
// 单例销毁(反向初始化顺序)
// 场景管理
SceneManager::get().ClearAll();
// 渲染器(内部会清理 ShaderManager 和 Batch
Renderer::get().shutdown();
// 音频系统
AudioSystem::get().shutdown();
// 资源归档
NpkArchive::get().close();
PvfArchive::get().close();
SoundPackArchive::get().close();
// 清理相机
if (camera_) {
delete camera_;
camera_ = nullptr;
}
if (window_) {
window_->destroy();
window_ = nullptr;
}
// 平台相关清理
#ifdef __SWITCH__
switchShutdown();
#endif
// 退出 SDL(添加重复调用保护)
static bool sdlQuitCalled = false;
if (!sdlQuitCalled) {
SDL_Quit();
sdlQuitCalled = true;
}
ObjectRegistry::get().clear();
initialized_ = false;
running_ = false;
}
Application::~Application() {
}
bool Application::initCoreModules() {
ScopedStartupTrace startupTrace("Application::initCoreModules");
// 初始化资产管理器
auto &asset = Asset::get();
// 平台相关 switch平台不可以获取当前工作目录
#ifndef __SWITCH__
// 获取程序工作目录
{
ScopedStartupTrace stageTrace("Asset working directory setup");
std::string workingDir = SDL_GetBasePath();
asset.setWorkingDirectory(workingDir);
SDL_Log("Asset working directory: %s", workingDir.c_str());
}
#else
asset.setWorkingDirectory("/switch/Frostbite2D/" + config_.appName);
SDL_Log("Asset working directory: %s", asset.getWorkingDirectory().c_str());
#endif
{
ScopedStartupTrace stageTrace("SaveSystem::init");
if (!SaveSystem::get().init()) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to initialize save system");
return false;
}
}
{
ScopedStartupTrace stageTrace("SDL_Init video/events/controller");
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS | SDL_INIT_GAMECONTROLLER) != 0) {
SDL_Log("Failed to initialize SDL: %s", SDL_GetError());
return false;
}
}
// Disable text input by default. UI widgets should enable it explicitly.
setTextInputEnabled(false);
// 打开第一个手柄(如果存在)
{
ScopedStartupTrace stageTrace("SDL_GameControllerOpen(0)");
if (SDL_GameController *controller = SDL_GameControllerOpen(0)) {
SDL_Log("GameController opened: %s", SDL_GameControllerName(controller));
} else {
SDL_Log("No GameController found");
}
}
// 使用SDL2创建窗口
this->window_ = new Window();
// 创建窗口会使用窗口配置
{
ScopedStartupTrace stageTrace("Window::create");
if (!window_->create(config_.windowConfig)) {
SDL_Log("Failed to create window");
return false;
}
}
window_->setImmEnabled(immEnabled_);
window_->onResize([this](int width, int height) {
if (!renderer_ || !window_) {
return;
}
renderer_->setWindowSize(width, height, window_->scaleX(), window_->scaleY());
SDL_Log("Window resized to %dx%d (drawable %dx%d)", width, height,
window_->drawableWidth(), window_->drawableHeight());
});
// 初始化渲染器
renderer_ = &Renderer::get();
{
ScopedStartupTrace stageTrace("Renderer::init");
if (!renderer_->init()) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to initialize renderer");
return false;
}
}
// 设置窗口清除颜色和视口
renderer_->setClearColor(0.0f, 0.0f, 0.0f);
renderer_->setDefaultRenderStyleProfile(config_.renderStyleProfile);
renderer_->setVirtualResolutionEnabled(config_.useVirtualResolution);
renderer_->setResolutionScaleMode(config_.resolutionMode);
renderer_->setSeparateUIVirtualResolutionEnabled(
config_.useSeparateUIVirtualResolution);
renderer_->setUIResolutionScaleMode(config_.uiResolutionMode);
if (config_.virtualWidth > 0 && config_.virtualHeight > 0) {
renderer_->setVirtualResolution(config_.virtualWidth, config_.virtualHeight);
}
if (config_.uiVirtualWidth > 0 && config_.uiVirtualHeight > 0) {
renderer_->setUIVirtualResolution(config_.uiVirtualWidth,
config_.uiVirtualHeight);
}
renderer_->setWindowSize(window_->width(), window_->height(), window_->scaleX(),
window_->scaleY());
// 创建并设置相机
{
ScopedStartupTrace stageTrace("Camera setup");
camera_ = new Camera();
camera_->setViewport(renderer_->getVirtualWidth(), renderer_->getVirtualHeight());
camera_->setFlipY(true); // Use top-left as (0,0).
renderer_->applyRenderStyleToCamera(
camera_, config_.renderStyleProfile, RenderStyleLayerRole::World);
renderer_->setCamera(camera_);
}
{
ScopedStartupTrace stageTrace("TaskSystem::init");
if (!TaskSystem::get().init()) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to initialize task system");
return false;
}
}
return true;
}
void Application::run(StartCallback callback) {
if (!initialized_) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Application not initialized!");
return;
}
if (running_) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Application already running!");
return;
}
running_ = true;
shouldQuit_ = false;
paused_ = false;
lastFrameTime_ = SDL_GetPerformanceCounter() / static_cast<double>(SDL_GetPerformanceFrequency());
totalTime_ = 0.0f;
deltaTime_ = 0.0f;
frameCount_ = 0;
fpsTimer_ = 0.0f;
currentFps_ = 0;
if (callback) {
callback();
}
StartupTrace::mark("before Application::mainLoop");
mainLoop();
running_ = false;
SDL_Log("Application stopped");
}
void Application::quit() {
SDL_Log("Quit requested");
shouldQuit_ = true;
}
void Application::pause() {
if (!paused_) {
SDL_Log("Application paused");
paused_ = true;
}
}
void Application::resume() {
if (paused_) {
SDL_Log("Application resumed");
paused_ = false;
lastFrameTime_ = SDL_GetPerformanceCounter() / static_cast<double>(SDL_GetPerformanceFrequency());
}
}
void Application::setMaxFps(int maxFps) {
maxFps_ = std::max(0, maxFps);
targetFrameDuration_ = maxFps_ > 0
? (1.0 / static_cast<double>(maxFps_))
: 0.0;
config_.maxFps = maxFps_;
}
void Application::setTextInputEnabled(bool enabled) {
textInputRequestCount_ = enabled ? 1 : 0;
const bool canToggleTextInput = SDL_WasInit(SDL_INIT_VIDEO) != 0;
if (enabled) {
if (canToggleTextInput) {
SDL_StartTextInput();
}
if (!textInputEnabled_) {
SDL_Log("Text input enabled");
}
textInputEnabled_ = true;
return;
}
if (canToggleTextInput) {
SDL_StopTextInput();
}
if (textInputEnabled_) {
SDL_Log("Text input disabled");
}
textInputEnabled_ = false;
}
void Application::requestTextInput() {
if (textInputRequestCount_ < std::numeric_limits<int>::max()) {
++textInputRequestCount_;
}
if (textInputEnabled_) {
return;
}
const bool canToggleTextInput = SDL_WasInit(SDL_INIT_VIDEO) != 0;
if (canToggleTextInput) {
SDL_StartTextInput();
}
textInputEnabled_ = true;
SDL_Log("Text input enabled");
}
void Application::releaseTextInput() {
if (textInputRequestCount_ <= 0) {
textInputRequestCount_ = 0;
return;
}
--textInputRequestCount_;
if (textInputRequestCount_ > 0 || !textInputEnabled_) {
return;
}
const bool canToggleTextInput = SDL_WasInit(SDL_INIT_VIDEO) != 0;
if (canToggleTextInput) {
SDL_StopTextInput();
}
textInputEnabled_ = false;
SDL_Log("Text input disabled");
}
void Application::setImmEnabled(bool enabled) {
immEnabled_ = enabled;
if (window_) {
window_->setImmEnabled(enabled);
}
}
bool Application::isImmEnabled() const {
if (window_) {
return window_->isImmEnabled();
}
return immEnabled_;
}
void Application::mainLoop() {
constexpr double kUnfocusedFrameDuration = 1.0 / 15.0;
const double performanceFrequency =
static_cast<double>(SDL_GetPerformanceFrequency());
while (!shouldQuit_) {
const Uint64 frameStartCounter = SDL_GetPerformanceCounter();
SDL_Event sdlEvent;
while (SDL_PollEvent(&sdlEvent)) {
if (sdlEvent.type == SDL_QUIT) {
shouldQuit_ = true;
break;
}
if (sdlEvent.type == SDL_WINDOWEVENT && window_) {
switch (sdlEvent.window.event) {
case SDL_WINDOWEVENT_RESIZED:
case SDL_WINDOWEVENT_SIZE_CHANGED:
window_->refreshMetrics(true);
break;
case SDL_WINDOWEVENT_FOCUS_GAINED:
case SDL_WINDOWEVENT_FOCUS_LOST:
case SDL_WINDOWEVENT_MINIMIZED:
case SDL_WINDOWEVENT_MAXIMIZED:
case SDL_WINDOWEVENT_RESTORED:
window_->refreshMetrics(false);
break;
default:
break;
}
}
auto event = convertSDLEvent(sdlEvent);
if (event) {
dispatchEvent(*event);
}
}
TaskSystem::get().drainMainThreadTasks();
update();
render();
double frameDurationLimit = targetFrameDuration_;
bool shouldDelay = frameDurationLimit > 0.0;
if (window_) {
bool shouldUseBackgroundCap = !window_->focused() || window_->minimized();
if (shouldUseBackgroundCap) {
frameDurationLimit = shouldDelay
? std::max(frameDurationLimit, kUnfocusedFrameDuration)
: kUnfocusedFrameDuration;
shouldDelay = true;
} else if (window_->vsync()) {
continue;
}
}
if (!shouldDelay || frameDurationLimit <= 0.0) {
continue;
}
const double frameDuration =
static_cast<double>(SDL_GetPerformanceCounter() - frameStartCounter) /
performanceFrequency;
const double remainingDelay = frameDurationLimit - frameDuration;
if (remainingDelay > 0.0) {
const Uint32 delayMs = static_cast<Uint32>(remainingDelay * 1000.0);
if (delayMs > 0) {
SDL_Delay(delayMs);
}
}
}
}
std::unique_ptr<Event> Application::convertSDLEvent(const SDL_Event& sdlEvent) {
switch (sdlEvent.type) {
case SDL_WINDOWEVENT: {
switch (sdlEvent.window.event) {
case SDL_WINDOWEVENT_CLOSE:
return std::make_unique<WindowCloseEvent>(sdlEvent.window.timestamp, sdlEvent.window.windowID);
case SDL_WINDOWEVENT_RESIZED:
case SDL_WINDOWEVENT_SIZE_CHANGED:
return std::make_unique<WindowResizeEvent>(sdlEvent.window.timestamp, sdlEvent.window.windowID,
sdlEvent.window.data1, sdlEvent.window.data2);
case SDL_WINDOWEVENT_FOCUS_GAINED:
case SDL_WINDOWEVENT_FOCUS_LOST:
return std::make_unique<WindowFocusEvent>(sdlEvent.window.timestamp, sdlEvent.window.windowID,
sdlEvent.window.event == SDL_WINDOWEVENT_FOCUS_GAINED);
case SDL_WINDOWEVENT_MINIMIZED:
return std::make_unique<WindowMinimizeEvent>(sdlEvent.window.timestamp, sdlEvent.window.windowID);
case SDL_WINDOWEVENT_MAXIMIZED:
return std::make_unique<WindowMaximizeEvent>(sdlEvent.window.timestamp, sdlEvent.window.windowID);
case SDL_WINDOWEVENT_RESTORED:
return std::make_unique<WindowRestoreEvent>(sdlEvent.window.timestamp, sdlEvent.window.windowID);
}
break;
}
case SDL_KEYDOWN: {
return std::make_unique<KeyEvent>(sdlEvent.key.timestamp,
static_cast<KeyCode>(sdlEvent.key.keysym.sym),
static_cast<KeyMod>(sdlEvent.key.keysym.mod),
sdlEvent.key.repeat != 0);
}
case SDL_KEYUP: {
return std::make_unique<KeyUpEvent>(sdlEvent.key.timestamp,
static_cast<KeyCode>(sdlEvent.key.keysym.sym),
static_cast<KeyMod>(sdlEvent.key.keysym.mod));
}
case SDL_TEXTINPUT: {
return std::make_unique<KeyTextEvent>(sdlEvent.text.timestamp, sdlEvent.text.text);
}
case SDL_MOUSEMOTION: {
return std::make_unique<MouseMoveEvent>(sdlEvent.motion.timestamp,
Vec2(static_cast<float>(sdlEvent.motion.x), static_cast<float>(sdlEvent.motion.y)),
Vec2(static_cast<float>(sdlEvent.motion.xrel), static_cast<float>(sdlEvent.motion.yrel)));
}
case SDL_MOUSEBUTTONDOWN: {
return std::make_unique<MouseButtonEvent>(sdlEvent.button.timestamp,
static_cast<MouseButton>(sdlEvent.button.button),
Vec2(static_cast<float>(sdlEvent.button.x), static_cast<float>(sdlEvent.button.y)),
true);
}
case SDL_MOUSEBUTTONUP: {
return std::make_unique<MouseButtonEvent>(sdlEvent.button.timestamp,
static_cast<MouseButton>(sdlEvent.button.button),
Vec2(static_cast<float>(sdlEvent.button.x), static_cast<float>(sdlEvent.button.y)),
false);
}
case SDL_MOUSEWHEEL: {
return std::make_unique<MouseWheelEvent>(sdlEvent.wheel.timestamp,
Vec2(static_cast<float>(sdlEvent.wheel.mouseX), static_cast<float>(sdlEvent.wheel.mouseY)),
Vec2(static_cast<float>(sdlEvent.wheel.x), static_cast<float>(sdlEvent.wheel.y)),
sdlEvent.wheel.direction == SDL_MOUSEWHEEL_FLIPPED);
}
case SDL_FINGERDOWN: {
return std::make_unique<TouchDownEvent>(sdlEvent.tfinger.timestamp,
sdlEvent.tfinger.touchId,
sdlEvent.tfinger.fingerId,
Vec2(static_cast<float>(sdlEvent.tfinger.x), static_cast<float>(sdlEvent.tfinger.y)),
sdlEvent.tfinger.pressure);
}
case SDL_FINGERUP: {
return std::make_unique<TouchUpEvent>(sdlEvent.tfinger.timestamp,
sdlEvent.tfinger.touchId,
sdlEvent.tfinger.fingerId,
Vec2(static_cast<float>(sdlEvent.tfinger.x), static_cast<float>(sdlEvent.tfinger.y)),
sdlEvent.tfinger.pressure);
}
case SDL_FINGERMOTION: {
return std::make_unique<TouchMoveEvent>(sdlEvent.tfinger.timestamp,
sdlEvent.tfinger.touchId,
sdlEvent.tfinger.fingerId,
Vec2(static_cast<float>(sdlEvent.tfinger.x), static_cast<float>(sdlEvent.tfinger.y)),
Vec2(static_cast<float>(sdlEvent.tfinger.dx), static_cast<float>(sdlEvent.tfinger.dy)),
sdlEvent.tfinger.pressure);
}
case SDL_CONTROLLERBUTTONDOWN: {
return std::make_unique<JoystickButtonDownEvent>(sdlEvent.cbutton.timestamp,
sdlEvent.cbutton.which,
sdlEvent.cbutton.button);
}
case SDL_CONTROLLERBUTTONUP: {
return std::make_unique<JoystickButtonUpEvent>(sdlEvent.cbutton.timestamp,
sdlEvent.cbutton.which,
sdlEvent.cbutton.button);
}
case SDL_CONTROLLERAXISMOTION: {
return std::make_unique<JoystickAxisEvent>(sdlEvent.caxis.timestamp,
sdlEvent.caxis.which,
sdlEvent.caxis.axis,
sdlEvent.caxis.value);
}
}
return nullptr;
}
void Application::dispatchEvent(const Event& event) {
if (paused_) {
SceneManager::get().DispatchUIEvent(event);
return;
}
SceneManager::get().DispatchEvent(event);
}
void Application::update() {
double currentTime = SDL_GetPerformanceCounter() / static_cast<double>(SDL_GetPerformanceFrequency());
deltaTime_ = static_cast<float>(currentTime - lastFrameTime_);
lastFrameTime_ = currentTime;
totalTime_ += deltaTime_;
if (!paused_) {
SceneManager::get().Update(deltaTime_);
}
SceneManager::get().UpdateUI(deltaTime_);
frameCount_++;
fpsTimer_ += deltaTime_;
if (fpsTimer_ >= fpsUpdateInterval_) {
currentFps_ = static_cast<int>(frameCount_ / fpsTimer_);
frameCount_ = 0;
fpsTimer_ = 0.0f;
}
}
void Application::render() {
static bool firstFramePresented = false;
Renderer& renderer = Renderer::get();
renderer.beginFrame();
SceneManager::get().Render();
renderer.endFrame();
if (window_) {
window_->swap();
}
if (!firstFramePresented) {
firstFramePresented = true;
StartupTrace::mark("first frame presented");
}
}
const AppConfig& Application::getConfig() const {
return config_;
}
} // namespace frostbite2D
@@ -0,0 +1,207 @@
#include <frostbite2D/core/task_system.h>
#include <SDL2/SDL_log.h>
#include <algorithm>
#include <stdexcept>
#include <utility>
namespace frostbite2D {
TaskSystem& TaskSystem::get() {
static TaskSystem instance;
return instance;
}
bool TaskSystem::init(const TaskSystemConfig& config) {
if (initialized_) {
return true;
}
try {
config_ = config;
if (config_.workerCount == 0) {
config_.workerCount = static_cast<uint32>(resolveWorkerCount(0));
}
{
std::lock_guard<std::mutex> workerLock(workerMutex_);
std::queue<Function<void()>> emptyWorkerTasks;
std::swap(workerTasks_, emptyWorkerTasks);
}
{
std::lock_guard<std::mutex> mainThreadLock(mainThreadMutex_);
std::queue<Function<void()>> emptyMainThreadTasks;
std::swap(mainThreadTasks_, emptyMainThreadTasks);
}
mainThreadId_ = std::this_thread::get_id();
acceptingTasks_ = true;
workers_.reserve(config_.workerCount);
for (uint32 i = 0; i < config_.workerCount; ++i) {
workers_.emplace_back([this]() {
while (true) {
Function<void()> task;
{
std::unique_lock<std::mutex> lock(workerMutex_);
workerCondition_.wait(lock, [this]() {
return !acceptingTasks_.load() || !workerTasks_.empty();
});
if (!acceptingTasks_.load() && workerTasks_.empty()) {
return;
}
task = std::move(workerTasks_.front());
workerTasks_.pop();
}
task();
}
});
}
initialized_ = true;
SDL_Log("TaskSystem initialized with %u worker threads", config_.workerCount);
return true;
} catch (const std::exception& ex) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to initialize TaskSystem: %s", ex.what());
shutdown();
return false;
}
}
void TaskSystem::shutdown() {
if (!initialized_) {
return;
}
acceptingTasks_ = false;
workerCondition_.notify_all();
for (std::thread& worker : workers_) {
if (worker.joinable()) {
worker.join();
}
}
workers_.clear();
{
std::lock_guard<std::mutex> workerLock(workerMutex_);
std::queue<Function<void()>> emptyWorkerTasks;
std::swap(workerTasks_, emptyWorkerTasks);
}
{
std::lock_guard<std::mutex> mainThreadLock(mainThreadMutex_);
std::queue<Function<void()>> emptyMainThreadTasks;
std::swap(mainThreadTasks_, emptyMainThreadTasks);
}
initialized_ = false;
mainThreadId_ = std::thread::id();
SDL_Log("TaskSystem shutdown");
}
bool TaskSystem::isMainThread() const {
return initialized_ && std::this_thread::get_id() == mainThreadId_;
}
size_t TaskSystem::workerCount() const {
return workers_.size();
}
uint32 TaskSystem::maxMainThreadCallbacksPerFrame() const {
return config_.maxMainThreadCallbacksPerFrame;
}
void TaskSystem::drainMainThreadTasks(uint32 maxTasks) {
if (!initialized_) {
return;
}
uint32 taskBudget = maxTasks != 0 ? maxTasks : config_.maxMainThreadCallbacksPerFrame;
uint32 processed = 0;
while (true) {
Function<void()> task;
{
std::lock_guard<std::mutex> lock(mainThreadMutex_);
if (mainThreadTasks_.empty()) {
break;
}
if (taskBudget != 0 && processed >= taskBudget) {
break;
}
task = std::move(mainThreadTasks_.front());
mainThreadTasks_.pop();
}
task();
++processed;
}
}
TaskSystem::~TaskSystem() {
shutdown();
}
void TaskSystem::enqueueWorkerTask(Function<void()> task) {
if (!acceptingTasks_) {
throw std::runtime_error("TaskSystem is not accepting new worker tasks");
}
{
std::lock_guard<std::mutex> lock(workerMutex_);
workerTasks_.push(std::move(task));
}
workerCondition_.notify_one();
}
void TaskSystem::enqueueMainThreadTask(Function<void()> task) {
if (!acceptingTasks_) {
return;
}
std::lock_guard<std::mutex> lock(mainThreadMutex_);
mainThreadTasks_.push(std::move(task));
}
size_t TaskSystem::resolveWorkerCount(uint32 requestedCount) const {
if (requestedCount > 0) {
return requestedCount;
}
#ifdef __SWITCH__
return 1;
#else
unsigned int hardwareCount = std::thread::hardware_concurrency();
if (hardwareCount <= 1) {
return 1;
}
return std::max(1u, std::min(hardwareCount - 1, 4u));
#endif
}
void TaskSystem::logUnhandledTaskException(std::exception_ptr error) {
if (!error) {
return;
}
try {
std::rethrow_exception(error);
} catch (const std::exception& ex) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "TaskSystem task failed: %s", ex.what());
} catch (...) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "TaskSystem task failed with unknown exception");
}
}
} // namespace frostbite2D
+395
View File
@@ -0,0 +1,395 @@
#include "SDL_log.h"
#include <SDL2/SDL.h>
#if defined(_WIN32)
#include <SDL2/SDL_syswm.h>
#include <imm.h>
#endif
#include <frostbite2D/core/window.h>
#include <frostbite2D/resource/asset.h>
#include <glad/glad.h>
namespace frostbite2D {
#if defined(_WIN32)
namespace {
HWND resolveWindowHandle(SDL_Window* sdlWindow) {
if (!sdlWindow) {
return nullptr;
}
SDL_SysWMinfo wmInfo;
SDL_VERSION(&wmInfo.version);
if (!SDL_GetWindowWMInfo(sdlWindow, &wmInfo)) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Window: SDL_GetWindowWMInfo failed: %s", SDL_GetError());
return nullptr;
}
if (wmInfo.subsystem != SDL_SYSWM_WINDOWS) {
return nullptr;
}
return wmInfo.info.win.window;
}
} // namespace
#endif
bool Window::create(const WindowConfig& cfg) {
Uint32 flags = SDL_WINDOW_OPENGL | SDL_WINDOW_SHOWN;
#ifndef __SWITCH__
if (cfg.fullscreen) {
flags |= SDL_WINDOW_FULLSCREEN_DESKTOP;
} else if (cfg.borderless) {
flags |= SDL_WINDOW_BORDERLESS;
}
if (cfg.resizable) {
flags |= SDL_WINDOW_RESIZABLE;
}
if (!cfg.decorated) {
flags |= SDL_WINDOW_BORDERLESS;
}
#endif
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 2);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES);
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24);
SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8);
if (cfg.multisamples > 0) {
SDL_GL_SetAttribute(SDL_GL_MULTISAMPLEBUFFERS, 1);
SDL_GL_SetAttribute(SDL_GL_MULTISAMPLESAMPLES, cfg.multisamples);
}
int x = cfg.centered ? SDL_WINDOWPOS_CENTERED : SDL_WINDOWPOS_UNDEFINED;
int y = cfg.centered ? SDL_WINDOWPOS_CENTERED : SDL_WINDOWPOS_UNDEFINED;
sdlWindow_ = SDL_CreateWindow(cfg.title.c_str(), x, y, cfg.width, cfg.height,
flags);
if (!sdlWindow_) {
SDL_Log("Failed to create window: %s", SDL_GetError());
return false;
}
glContext_ = SDL_GL_CreateContext(sdlWindow_);
if (!glContext_) {
SDL_Log("Failed to create OpenGL context: %s", SDL_GetError());
SDL_DestroyWindow(sdlWindow_);
sdlWindow_ = nullptr;
return false;
}
if (!gladLoadGLES2Loader((GLADloadproc)SDL_GL_GetProcAddress)) {
SDL_Log("Failed to initialize GLAD GLES2");
SDL_GL_DeleteContext(glContext_);
glContext_ = nullptr;
SDL_DestroyWindow(sdlWindow_);
sdlWindow_ = nullptr;
return false;
}
SDL_GL_SetSwapInterval(cfg.vsync ? 1 : 0);
refreshMetrics(false);
fullscreen_ = (flags & SDL_WINDOW_FULLSCREEN_DESKTOP) != 0;
vsync_ = cfg.vsync;
cursorVisible_ = cfg.showCursor;
showCursor(cfg.showCursor);
setImmEnabled(immEnabled_);
#ifndef __SWITCH__
if (!cfg.icon.file_path.empty()) {
Asset& asset = Asset::get();
std::string resolvedPath = asset.resolveAssetPath(cfg.icon.file_path);
SDL_Surface* icon = SDL_LoadBMP(resolvedPath.c_str());
if (icon) {
SDL_SetWindowIcon(sdlWindow_, icon);
SDL_FreeSurface(icon);
} else {
SDL_Log("Failed to load window icon: %s", resolvedPath.c_str());
}
}
#if defined(_WIN32)
if (cfg.icon.resource_id != 0) {
SDL_Log("Loading icon from resource ID: %u", cfg.icon.resource_id);
}
#endif
#endif
return true;
}
void Window::destroy() {
#if defined(_WIN32)
if (sdlWindow_ && immDisabledByApp_) {
HWND hwnd = resolveWindowHandle(sdlWindow_);
if (hwnd) {
ImmAssociateContext(hwnd, static_cast<HIMC>(immRestoreContext_));
}
immRestoreContext_ = nullptr;
immDisabledByApp_ = false;
}
#endif
if (glContext_) {
SDL_GL_DeleteContext(glContext_);
glContext_ = nullptr;
}
if (sdlWindow_) {
SDL_DestroyWindow(sdlWindow_);
sdlWindow_ = nullptr;
}
}
void Window::poll() {}
void Window::swap() {
if (sdlWindow_ && glContext_) {
SDL_GL_SwapWindow(sdlWindow_);
}
}
void Window::close() {
shouldClose_ = true;
if (closeCb_) {
closeCb_();
}
}
void Window::setTitle(const std::string& title) {
if (sdlWindow_) {
SDL_SetWindowTitle(sdlWindow_, title.c_str());
}
}
void Window::setSize(int w, int h) {
if (sdlWindow_) {
SDL_SetWindowSize(sdlWindow_, w, h);
refreshMetrics(true);
return;
}
width_ = w;
height_ = h;
drawableWidth_ = w;
drawableHeight_ = h;
scaleX_ = 1.0f;
scaleY_ = 1.0f;
if (resizeCb_) {
resizeCb_(w, h);
}
}
void Window::setPos(int x, int y) {
if (sdlWindow_) {
SDL_SetWindowPosition(sdlWindow_, x, y);
}
}
void Window::setFullscreen(bool fs) {
fullscreen_ = fs;
if (!sdlWindow_) {
return;
}
#ifndef __SWITCH__
Uint32 flags = fs ? SDL_WINDOW_FULLSCREEN_DESKTOP : 0;
SDL_SetWindowFullscreen(sdlWindow_, flags);
refreshMetrics(true);
#endif
}
void Window::setVSync(bool vsync) {
vsync_ = vsync;
if (glContext_) {
SDL_GL_SetSwapInterval(vsync ? 1 : 0);
}
}
void Window::setVisible(bool visible) {
if (!sdlWindow_) {
return;
}
if (visible) {
SDL_ShowWindow(sdlWindow_);
} else {
SDL_HideWindow(sdlWindow_);
}
}
int Window::width() const { return width_; }
int Window::height() const { return height_; }
int Window::drawableWidth() const { return drawableWidth_; }
int Window::drawableHeight() const { return drawableHeight_; }
Size Window::size() const {
return Size{static_cast<float>(width_), static_cast<float>(height_)};
}
Vec2 Window::pos() const {
if (sdlWindow_) {
int x = 0;
int y = 0;
SDL_GetWindowPosition(sdlWindow_, &x, &y);
return Vec2{static_cast<float>(x), static_cast<float>(y)};
}
return Vec2{0.0f, 0.0f};
}
bool Window::fullscreen() const { return fullscreen_; }
bool Window::vsync() const { return vsync_; }
bool Window::focused() const { return focused_; }
bool Window::minimized() const { return minimized_; }
void Window::setImmEnabled(bool enabled) {
immEnabled_ = enabled;
#if defined(_WIN32)
if (!sdlWindow_) {
return;
}
HWND hwnd = resolveWindowHandle(sdlWindow_);
if (!hwnd) {
return;
}
if (enabled) {
if (!immDisabledByApp_) {
return;
}
ImmAssociateContext(hwnd, static_cast<HIMC>(immRestoreContext_));
immRestoreContext_ = nullptr;
immDisabledByApp_ = false;
SDL_Log("Window IMM enabled");
return;
}
if (immDisabledByApp_) {
return;
}
immRestoreContext_ = reinterpret_cast<void*>(ImmAssociateContext(hwnd, nullptr));
immDisabledByApp_ = true;
SDL_Log("Window IMM disabled");
#else
(void)enabled;
#endif
}
bool Window::isImmEnabled() const { return immEnabled_; }
float Window::scaleX() const { return scaleX_; }
float Window::scaleY() const { return scaleY_; }
bool Window::refreshMetrics(bool emitResize) {
if (!sdlWindow_) {
return false;
}
int prevWidth = width_;
int prevHeight = height_;
int prevDrawableWidth = drawableWidth_;
int prevDrawableHeight = drawableHeight_;
bool prevFocused = focused_;
SDL_GetWindowSize(sdlWindow_, &width_, &height_);
SDL_GL_GetDrawableSize(sdlWindow_, &drawableWidth_, &drawableHeight_);
if (width_ > 0 && height_ > 0) {
scaleX_ = static_cast<float>(drawableWidth_) / static_cast<float>(width_);
scaleY_ = static_cast<float>(drawableHeight_) / static_cast<float>(height_);
} else {
scaleX_ = 1.0f;
scaleY_ = 1.0f;
}
Uint32 windowFlags = SDL_GetWindowFlags(sdlWindow_);
focused_ = (windowFlags & SDL_WINDOW_INPUT_FOCUS) != 0;
minimized_ = (windowFlags & SDL_WINDOW_MINIMIZED) != 0;
fullscreen_ = (windowFlags & SDL_WINDOW_FULLSCREEN_DESKTOP) != 0 ||
(windowFlags & SDL_WINDOW_FULLSCREEN) != 0;
bool changed = prevWidth != width_ || prevHeight != height_ ||
prevDrawableWidth != drawableWidth_ ||
prevDrawableHeight != drawableHeight_;
if (emitResize && changed && resizeCb_) {
resizeCb_(width_, height_);
}
if (prevFocused != focused_ && focusCb_) {
focusCb_(focused_);
}
return true;
}
void Window::setCursor(CursorType cursor) {
SDL_SystemCursor systemCursor = SDL_SYSTEM_CURSOR_ARROW;
switch (cursor) {
case CursorType::Arrow:
systemCursor = SDL_SYSTEM_CURSOR_ARROW;
break;
case CursorType::TextInput:
systemCursor = SDL_SYSTEM_CURSOR_IBEAM;
break;
case CursorType::Hand:
systemCursor = SDL_SYSTEM_CURSOR_HAND;
break;
case CursorType::SizeAll:
systemCursor = SDL_SYSTEM_CURSOR_SIZEALL;
break;
case CursorType::SizeWE:
systemCursor = SDL_SYSTEM_CURSOR_SIZEWE;
break;
case CursorType::SizeNS:
systemCursor = SDL_SYSTEM_CURSOR_SIZENS;
break;
case CursorType::SizeNESW:
systemCursor = SDL_SYSTEM_CURSOR_SIZENESW;
break;
case CursorType::SizeNWSE:
systemCursor = SDL_SYSTEM_CURSOR_SIZENWSE;
break;
case CursorType::No:
systemCursor = SDL_SYSTEM_CURSOR_ARROW;
break;
}
SDL_Cursor* sdlCursor = SDL_CreateSystemCursor(systemCursor);
if (sdlCursor) {
SDL_SetCursor(sdlCursor);
}
}
void Window::showCursor(bool show) {
cursorVisible_ = show;
SDL_ShowCursor(show ? SDL_ENABLE : SDL_DISABLE);
}
void Window::lockCursor(bool lock) {
cursorLocked_ = lock;
SDL_SetRelativeMouseMode(lock ? SDL_TRUE : SDL_FALSE);
}
void Window::onResize(ResizeCb cb) { resizeCb_ = cb; }
void Window::onClose(CloseCb cb) { closeCb_ = cb; }
void Window::onFocus(FocusCb cb) { focusCb_ = cb; }
void* Window::native() const { return nullptr; }
} // namespace frostbite2D
@@ -0,0 +1,254 @@
#include "SDL_log.h"
#include <SDL2/SDL.h>
#include <frostbite2D/graphics/batch.h>
#include <frostbite2D/graphics/renderer.h>
#include <glad/glad.h>
namespace frostbite2D {
Batch::Batch() {
currentBatch_.vertices.reserve(MAX_VERTICES);
currentBatch_.indices.reserve(MAX_INDICES);
}
Batch::~Batch() {
shutdown();
}
bool Batch::init() {
glGenVertexArrays(1, &vao_);
glGenBuffers(1, &vbo_);
glGenBuffers(1, &ibo_);
setupMesh();
SDL_Log("Batch system initialized");
return true;
}
void Batch::shutdown() {
if (vao_ != 0) {
glDeleteVertexArrays(1, &vao_);
vao_ = 0;
}
if (vbo_ != 0) {
glDeleteBuffers(1, &vbo_);
vbo_ = 0;
}
if (ibo_ != 0) {
glDeleteBuffers(1, &ibo_);
ibo_ = 0;
}
currentBatch_.vertices.clear();
currentBatch_.indices.clear();
batches_.clear();
}
void Batch::setupMesh() {
glBindVertexArray(vao_);
glBindBuffer(GL_ARRAY_BUFFER, vbo_);
glBufferData(GL_ARRAY_BUFFER, MAX_VERTICES * sizeof(Vertex), nullptr, GL_DYNAMIC_DRAW);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo_);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, MAX_INDICES * sizeof(uint16), nullptr, GL_DYNAMIC_DRAW);
int stride = sizeof(Vertex);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, stride, (void*)0);
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, stride, (void*)offsetof(Vertex, texCoord));
glEnableVertexAttribArray(2);
glVertexAttribPointer(2, 4, GL_FLOAT, GL_FALSE, stride, (void*)offsetof(Vertex, r));
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
}
void Batch::begin() {
begun_ = true;
drawCallCount_ = 0;
currentBatch_.vertices.clear();
currentBatch_.indices.clear();
currentBatch_.key = {0, 0, 0};
currentBatch_.shader = nullptr;
currentBatch_.texture.Reset();
}
void Batch::end() {
if (begun_) {
flush();
begun_ = false;
}
}
void Batch::submitQuad(const Quad& quad, const Transform2D& transform,
Ptr<Texture> texture, Shader* shader,
BlendMode blendMode) {
if (!begun_) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Batch not started, call begin() first");
return;
}
Renderer& renderer = Renderer::get();
glm::mat4 mat = transform.matrix;
Camera* camera = renderer.getCamera();
Vec2 snapOffset = Vec2::Zero();
if (camera && renderer.shouldSnapVertices()) {
glm::vec4 referencePos(quad.vertices[0].position.x,
quad.vertices[0].position.y, 0.0f, 1.0f);
glm::vec4 transformedReference = mat * referencePos;
Vec2 referenceWorld(transformedReference.x, transformedReference.y);
snapOffset = camera->snapWorldPosition(referenceWorld) - referenceWorld;
}
Vertex transformedVertices[4];
float minX = 0.0f;
float maxX = 0.0f;
float minY = 0.0f;
float maxY = 0.0f;
for (int i = 0; i < 4; i++) {
transformedVertices[i] = quad.vertices[i];
glm::vec4 pos(transformedVertices[i].position.x, transformedVertices[i].position.y,
0.0f, 1.0f);
glm::vec4 transformed = mat * pos;
transformedVertices[i].position.x = transformed.x + snapOffset.x;
transformedVertices[i].position.y = transformed.y + snapOffset.y;
if (i == 0) {
minX = maxX = transformedVertices[i].position.x;
minY = maxY = transformedVertices[i].position.y;
continue;
}
minX = std::min(minX, transformedVertices[i].position.x);
maxX = std::max(maxX, transformedVertices[i].position.x);
minY = std::min(minY, transformedVertices[i].position.y);
maxY = std::max(maxY, transformedVertices[i].position.y);
}
if (camera && renderer.isCullingEnabled()) {
Vec2 cameraPos = camera->getRenderPosition();
float safeZoom = std::max(camera->getZoom(), 0.01f);
float cullPadding = 0.5f / safeZoom;
Rect viewRect(cameraPos.x - cullPadding, cameraPos.y - cullPadding,
camera->getVisibleWidth() + cullPadding * 2.0f,
camera->getVisibleHeight() + cullPadding * 2.0f);
Rect quadBounds(minX, minY, maxX - minX, maxY - minY);
if (!viewRect.intersects(quadBounds)) {
return;
}
}
uint32_t textureID = texture ? texture->getID() : 0;
uint32_t shaderID = shader ? shader->getID() : 0;
BatchKey newKey = {shaderID, textureID, (uint32_t)blendMode};
flushIfNeeded(newKey, shader, texture);
constexpr size_t kVerticesPerQuad = 4;
constexpr size_t kIndicesPerQuad = 6;
if (currentBatch_.vertices.size() + kVerticesPerQuad > MAX_VERTICES ||
currentBatch_.indices.size() + kIndicesPerQuad > MAX_INDICES) {
flushCurrentBatch();
currentBatch_.key = newKey;
currentBatch_.shader = shader;
currentBatch_.texture = texture;
}
if (currentBatch_.vertices.size() + kVerticesPerQuad > MAX_VERTICES ||
currentBatch_.indices.size() + kIndicesPerQuad > MAX_INDICES) {
static bool overflowWarningLogged = false;
if (!overflowWarningLogged) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Batch overflow: dropping quad (max %d quads per draw batch)",
MAX_QUADS);
overflowWarningLogged = true;
}
return;
}
uint16_t indexOffset = static_cast<uint16_t>(currentBatch_.vertices.size());
for (int i = 0; i < 4; i++) {
currentBatch_.vertices.push_back(transformedVertices[i]);
}
currentBatch_.indices.push_back(indexOffset);
currentBatch_.indices.push_back(static_cast<uint16_t>(indexOffset + 1));
currentBatch_.indices.push_back(static_cast<uint16_t>(indexOffset + 2));
currentBatch_.indices.push_back(static_cast<uint16_t>(indexOffset + 1));
currentBatch_.indices.push_back(static_cast<uint16_t>(indexOffset + 3));
currentBatch_.indices.push_back(static_cast<uint16_t>(indexOffset + 2));
}
void Batch::flush() {
if (!currentBatch_.vertices.empty()) {
flushCurrentBatch();
}
}
void Batch::flushIfNeeded(const BatchKey& newKey, Shader* shader, Ptr<Texture> texture) {
if (currentBatch_.indices.empty()) {
currentBatch_.key = newKey;
currentBatch_.shader = shader;
currentBatch_.texture = texture;
return;
}
if (currentBatch_.key.shaderID != newKey.shaderID ||
currentBatch_.key.textureID != newKey.textureID ||
currentBatch_.key.blendMode != newKey.blendMode) {
flushCurrentBatch();
currentBatch_.key = newKey;
currentBatch_.shader = shader;
currentBatch_.texture = texture;
}
}
void Batch::flushCurrentBatch() {
if (currentBatch_.vertices.empty()) {
return;
}
// Apply the batch's blend state right before drawing so per-sprite
// `SetBlendMode()` changes actually affect the GL pipeline.
Renderer::get().setupBlendMode(static_cast<BlendMode>(currentBatch_.key.blendMode));
glBindVertexArray(vao_);
// 绑定着色器和纹理
if (currentBatch_.shader) {
currentBatch_.shader->use();
// 设置纹理采样器 uniform 变量
currentBatch_.shader->setTexture("u_texture", 0);
}
if (currentBatch_.texture) {
Renderer& renderer = Renderer::get();
bool preferPixelArtSampling = renderer.shouldUsePixelArtSampling();
currentBatch_.texture->bind(0, preferPixelArtSampling);
}
glBindBuffer(GL_ARRAY_BUFFER, vbo_);
glBufferSubData(GL_ARRAY_BUFFER, 0,
currentBatch_.vertices.size() * sizeof(Vertex),
currentBatch_.vertices.data());
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo_);
glBufferSubData(GL_ELEMENT_ARRAY_BUFFER, 0,
currentBatch_.indices.size() * sizeof(uint16),
currentBatch_.indices.data());
glDrawElements(GL_TRIANGLES,
static_cast<GLsizei>(currentBatch_.indices.size()),
GL_UNSIGNED_SHORT, 0);
drawCallCount_++;
currentBatch_.vertices.clear();
currentBatch_.indices.clear();
currentBatch_.shader = nullptr;
currentBatch_.texture.Reset();
}
}
@@ -0,0 +1,97 @@
#include <frostbite2D/graphics/camera.h>
#include <algorithm>
#include <cmath>
#include <glm/gtc/matrix_transform.hpp>
namespace frostbite2D {
namespace {
constexpr float kMinZoom = 0.01f;
float clampZoom(float zoom) {
return std::max(zoom, kMinZoom);
}
Vec2 snapPositionToPixelGrid(const Vec2& position, float zoom) {
float safeZoom = clampZoom(zoom);
return Vec2(std::round(position.x * safeZoom) / safeZoom,
std::round(position.y * safeZoom) / safeZoom);
}
} // namespace
Camera::Camera() : position_(0, 0), zoom_(1.0f) {}
void Camera::setPosition(const Vec2& pos) { position_ = pos; }
void Camera::setZoom(float zoom) { zoom_ = clampZoom(zoom); }
void Camera::setViewport(int width, int height) {
viewportWidth_ = width;
viewportHeight_ = height;
}
Vec2 Camera::getRenderPosition() const {
return snapWorldPosition(position_);
}
Vec2 Camera::snapWorldPosition(const Vec2& position) const {
if (!pixelSnapEnabled_) {
return position;
}
return snapPositionToPixelGrid(position, zoom_);
}
float Camera::getVisibleWidth() const {
float safeZoom = clampZoom(zoom_);
return static_cast<float>(viewportWidth_) / safeZoom;
}
float Camera::getVisibleHeight() const {
float safeZoom = clampZoom(zoom_);
return static_cast<float>(viewportHeight_) / safeZoom;
}
void Camera::lookAt(const Vec2& target) { position_ = target; }
void Camera::move(const Vec2& delta) { position_ = position_ + delta; }
void Camera::zoomAt(float factor, const Vec2& screenPos) {
float previousZoom = clampZoom(zoom_);
Vec2 worldBefore(position_.x + screenPos.x / previousZoom,
position_.y + screenPos.y / previousZoom);
zoom_ = clampZoom(zoom_ * factor);
Vec2 worldAfter(position_.x + screenPos.x / zoom_,
position_.y + screenPos.y / zoom_);
position_ = position_ + (worldBefore - worldAfter);
}
glm::mat4 Camera::getViewMatrix() const {
float safeZoom = clampZoom(zoom_);
Vec2 renderPosition = getRenderPosition();
glm::mat4 view = glm::mat4(1.0f);
view = glm::scale(view, glm::vec3(safeZoom, safeZoom, 1.0f));
view = glm::translate(view,
glm::vec3(-renderPosition.x, -renderPosition.y, 0.0f));
return view;
}
glm::mat4 Camera::getProjectionMatrix() const {
float left = 0.0f;
float right = static_cast<float>(viewportWidth_);
float bottom, top;
if (flipY_) {
bottom = static_cast<float>(viewportHeight_);
top = 0.0f;
} else {
bottom = 0.0f;
top = static_cast<float>(viewportHeight_);
}
return glm::ortho(left, right, bottom, top, -1.0f, 1.0f);
}
} // namespace frostbite2D
@@ -0,0 +1,98 @@
#include <frostbite2D/graphics/font_manager.h>
#include <frostbite2D/resource/asset.h>
#include <SDL2/SDL.h>
namespace frostbite2D {
FontManager::~FontManager() {
unloadAll();
}
FontManager& FontManager::get() {
static FontManager instance;
return instance;
}
bool FontManager::init() {
if (TTF_WasInit() == 0) {
if (TTF_Init() != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to initialize TTF: %s", TTF_GetError());
return false;
}
}
return true;
}
void FontManager::shutdown() {
unloadAll();
if (TTF_WasInit() != 0) {
TTF_Quit();
}
}
bool FontManager::registerFont(const std::string& name, const std::string& path, int fontSize) {
if (fonts_.find(name) != fonts_.end()) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Font '%s' already registered, replacing", name.c_str());
unregisterFont(name);
}
Asset& asset = Asset::get();
std::string fullPath = asset.resolveAssetPath(path);
TTF_Font* font = TTF_OpenFont(fullPath.c_str(), fontSize);
if (!font) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to load font '%s': %s", fullPath.c_str(), TTF_GetError());
return false;
}
fonts_[name] = font;
FontInfo info;
info.name = name;
info.path = path;
info.size = fontSize;
fontInfos_[name] = info;
SDL_Log("Font '%s' loaded: %s, size %d", name.c_str(), fullPath.c_str(), fontSize);
return true;
}
TTF_Font* FontManager::getFont(const std::string& name) {
auto it = fonts_.find(name);
if (it == fonts_.end()) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Font '%s' not found", name.c_str());
return nullptr;
}
return it->second;
}
bool FontManager::hasFont(const std::string& name) const {
return fonts_.find(name) != fonts_.end();
}
void FontManager::unregisterFont(const std::string& name) {
auto it = fonts_.find(name);
if (it != fonts_.end()) {
TTF_CloseFont(it->second);
fonts_.erase(it);
fontInfos_.erase(name);
}
}
void FontManager::unloadAll() {
for (auto& pair : fonts_) {
TTF_CloseFont(pair.second);
}
fonts_.clear();
fontInfos_.clear();
}
std::optional<FontManager::FontInfo> FontManager::getFontInfo(const std::string& name) const {
auto it = fontInfos_.find(name);
if (it != fontInfos_.end()) {
return it->second;
}
return std::nullopt;
}
}
@@ -0,0 +1,67 @@
#include <frostbite2D/graphics/render_texture.h>
#include <SDL2/SDL.h>
#include <algorithm>
#include <glad/glad.h>
namespace frostbite2D {
RenderTexture::~RenderTexture() {
Reset();
}
bool RenderTexture::Init(int width, int height) {
return Resize(width, height);
}
bool RenderTexture::Resize(int width, int height) {
width = std::max(width, 1);
height = std::max(height, 1);
if (framebufferID_ != 0 && texture_ && width_ == width && height_ == height) {
return true;
}
Reset();
texture_ = Texture::createEmpty(width, height);
if (!texture_) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"RenderTexture: failed to create color texture %dx%d",
width, height);
return false;
}
glGenFramebuffers(1, &framebufferID_);
glBindFramebuffer(GL_FRAMEBUFFER, framebufferID_);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
texture_->getID(), 0);
GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
if (status != GL_FRAMEBUFFER_COMPLETE) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"RenderTexture: framebuffer incomplete (status=0x%x)",
static_cast<unsigned int>(status));
Reset();
return false;
}
width_ = width;
height_ = height;
return true;
}
void RenderTexture::Reset() {
texture_.Reset();
if (framebufferID_ != 0) {
glDeleteFramebuffers(1, &framebufferID_);
framebufferID_ = 0;
}
width_ = 0;
height_ = 0;
}
} // namespace frostbite2D
@@ -0,0 +1,661 @@
#include "SDL_log.h"
#include <SDL2/SDL.h>
#include <algorithm>
#include <cmath>
#include <frostbite2D/graphics/renderer.h>
#include <frostbite2D/graphics/render_texture.h>
#include <glad/glad.h>
#include <glm/gtc/type_ptr.hpp>
namespace frostbite2D {
namespace {
constexpr float kMinContentScale = 0.01f;
int roundToInt(float value) {
return static_cast<int>(std::lround(value));
}
RenderStyleLayerSettings resolveRenderStyleLayer(RenderStyleProfileId profile,
RenderStyleLayerRole role) {
return RenderStyleSettings::FromProfile(profile).layer(role);
}
} // namespace
Renderer& Renderer::get() {
static Renderer instance;
return instance;
}
Renderer::Renderer() {
activeRenderStyle_ =
resolveRenderStyleLayer(defaultRenderStyleProfile_, activeRenderStyleRole_);
}
bool Renderer::init() {
if (initialized_) {
return true;
}
if (!batch_.init()) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to initialize batch system");
return false;
}
if (!shaderManager_.init("assets/shaders")) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to initialize shader manager");
return false;
}
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
SDL_Log("Renderer initialized");
initialized_ = true;
recalculateResolutionState();
return true;
}
void Renderer::shutdown() {
renderTargetStack_.clear();
batch_.shutdown();
shaderManager_.shutdown();
initialized_ = false;
frameActive_ = false;
cullingEnabled_ = true;
}
void Renderer::beginFrame() {
applyWorldViewport();
glClearColor(clearColor_[0] / 255.0f, clearColor_[1] / 255.0f,
clearColor_[2] / 255.0f, clearColor_[3] / 255.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
updateUniforms();
batch_.begin();
frameActive_ = true;
}
void Renderer::endFrame() {
while (!renderTargetStack_.empty()) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Renderer: endFrame() auto-closing unfinished render target scope");
endRenderToTexture();
}
batch_.end();
frameActive_ = false;
}
void Renderer::flush() {
batch_.flush();
}
bool Renderer::beginRenderToTexture(RenderTexture& target, Camera* camera,
const Color& clearColor, bool clear) {
if (!initialized_) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Renderer: beginRenderToTexture called before renderer init");
return false;
}
if (!target.IsValid()) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Renderer: beginRenderToTexture called with invalid target");
return false;
}
Camera* activeCamera = camera ? camera : camera_;
if (!activeCamera) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Renderer: beginRenderToTexture requires an active camera");
return false;
}
RenderTargetState state;
GLint framebufferBinding = 0;
GLint viewport[4] = {};
GLfloat clearColorValue[4] = {};
glGetIntegerv(GL_FRAMEBUFFER_BINDING, &framebufferBinding);
glGetIntegerv(GL_VIEWPORT, viewport);
glGetFloatv(GL_COLOR_CLEAR_VALUE, clearColorValue);
state.framebuffer = static_cast<uint32>(framebufferBinding);
state.viewportX = viewport[0];
state.viewportY = viewport[1];
state.viewportWidth = std::max(viewport[2], 1);
state.viewportHeight = std::max(viewport[3], 1);
state.clearColor[0] = clearColorValue[0];
state.clearColor[1] = clearColorValue[1];
state.clearColor[2] = clearColorValue[2];
state.clearColor[3] = clearColorValue[3];
state.camera = camera_;
state.cullingEnabled = cullingEnabled_;
if (frameActive_ || !renderTargetStack_.empty()) {
batch_.flush();
} else {
batch_.begin();
state.startedStandaloneBatch = true;
}
renderTargetStack_.push_back(state);
glBindFramebuffer(GL_FRAMEBUFFER, target.framebufferID_);
glViewport(0, 0, std::max(target.width_, 1), std::max(target.height_, 1));
if (clear) {
glClearColor(clearColor.r, clearColor.g, clearColor.b, clearColor.a);
glClear(GL_COLOR_BUFFER_BIT);
}
camera_ = activeCamera;
cullingEnabled_ = false;
updateUniforms();
return true;
}
bool Renderer::endRenderToTexture() {
if (renderTargetStack_.empty()) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Renderer: endRenderToTexture called without matching begin");
return false;
}
RenderTargetState state = renderTargetStack_.back();
renderTargetStack_.pop_back();
if (state.startedStandaloneBatch) {
batch_.end();
} else {
batch_.flush();
}
glBindFramebuffer(GL_FRAMEBUFFER, state.framebuffer);
glViewport(state.viewportX, state.viewportY, state.viewportWidth,
state.viewportHeight);
glClearColor(state.clearColor[0], state.clearColor[1], state.clearColor[2],
state.clearColor[3]);
camera_ = state.camera;
cullingEnabled_ = state.cullingEnabled;
updateUniforms();
return true;
}
void Renderer::setViewport(int x, int y, int width, int height) {
resolutionState_.viewportX = x;
resolutionState_.viewportY = y;
resolutionState_.viewportWidth = std::max(width, 1);
resolutionState_.viewportHeight = std::max(height, 1);
float safeScaleX = std::max(resolutionState_.contentScaleX, kMinContentScale);
float safeScaleY = std::max(resolutionState_.contentScaleY, kMinContentScale);
resolutionState_.windowViewportX = roundToInt(
static_cast<float>(resolutionState_.viewportX) / safeScaleX);
resolutionState_.windowViewportY = roundToInt(
static_cast<float>(resolutionState_.viewportY) / safeScaleY);
resolutionState_.windowViewportWidth = roundToInt(
static_cast<float>(resolutionState_.viewportWidth) / safeScaleX);
resolutionState_.windowViewportHeight = roundToInt(
static_cast<float>(resolutionState_.viewportHeight) / safeScaleY);
int logicalWidth = std::max(getVirtualWidth(), 1);
int logicalHeight = std::max(getVirtualHeight(), 1);
resolutionState_.scaleX =
static_cast<float>(resolutionState_.windowViewportWidth) /
static_cast<float>(logicalWidth);
resolutionState_.scaleY =
static_cast<float>(resolutionState_.windowViewportHeight) /
static_cast<float>(logicalHeight);
if (!useSeparateUIVirtualResolution_) {
uiResolutionState_ = resolutionState_;
}
syncCameraViewport();
}
void Renderer::setWindowSize(int width, int height, float contentScaleX,
float contentScaleY) {
resolutionState_.windowWidth = std::max(width, 1);
resolutionState_.windowHeight = std::max(height, 1);
resolutionState_.contentScaleX = std::max(contentScaleX, kMinContentScale);
resolutionState_.contentScaleY = std::max(contentScaleY, kMinContentScale);
resolutionState_.drawableWidth = std::max(
roundToInt(static_cast<float>(resolutionState_.windowWidth) *
resolutionState_.contentScaleX),
1);
resolutionState_.drawableHeight = std::max(
roundToInt(static_cast<float>(resolutionState_.windowHeight) *
resolutionState_.contentScaleY),
1);
recalculateResolutionState();
}
void Renderer::setVirtualResolutionEnabled(bool enabled) {
useVirtualResolution_ = enabled;
recalculateResolutionState();
}
void Renderer::setVirtualResolution(int width, int height) {
virtualWidth_ = std::max(width, 1);
virtualHeight_ = std::max(height, 1);
recalculateResolutionState();
}
void Renderer::setResolutionScaleMode(ResolutionScaleMode mode) {
resolutionMode_ = mode;
recalculateResolutionState();
}
void Renderer::setSeparateUIVirtualResolutionEnabled(bool enabled) {
useSeparateUIVirtualResolution_ = enabled;
recalculateResolutionState();
}
void Renderer::setUIVirtualResolution(int width, int height) {
uiVirtualWidth_ = width;
uiVirtualHeight_ = height;
recalculateResolutionState();
}
void Renderer::setUIResolutionScaleMode(ResolutionScaleMode mode) {
uiResolutionMode_ = mode;
recalculateResolutionState();
}
void Renderer::setClearColor(float r, float g, float b, float a) {
clearColor_[0] = static_cast<uint8>(r * 255);
clearColor_[1] = static_cast<uint8>(g * 255);
clearColor_[2] = static_cast<uint8>(b * 255);
clearColor_[3] = static_cast<uint8>(a * 255);
}
void Renderer::setClearColor(const Color& color) {
clearColor_[0] = color.r;
clearColor_[1] = color.g;
clearColor_[2] = color.b;
clearColor_[3] = color.a;
}
void Renderer::clear(uint32_t flags) {
glClear(flags);
}
void Renderer::setDefaultRenderStyleProfile(RenderStyleProfileId profile) {
defaultRenderStyleProfile_ = profile;
setActiveRenderStyleProfile(profile, activeRenderStyleRole_);
}
void Renderer::setActiveRenderStyleProfile(RenderStyleProfileId profile,
RenderStyleLayerRole role) {
if (initialized_ &&
(activeRenderStyleProfile_ != profile || activeRenderStyleRole_ != role)) {
batch_.flush();
}
activeRenderStyleProfile_ = profile;
activeRenderStyleRole_ = role;
activeRenderStyle_ = resolveRenderStyleLayer(profile, role);
}
void Renderer::applyRenderStyleToCamera(Camera* camera,
RenderStyleProfileId profile,
RenderStyleLayerRole role) const {
if (!camera) {
return;
}
RenderStyleLayerSettings settings = resolveRenderStyleLayer(profile, role);
camera->setPixelSnapEnabled(settings.cameraPixelSnap);
}
void Renderer::setCamera(Camera* camera) {
if (initialized_ && camera_ != camera) {
batch_.flush();
}
camera_ = camera;
syncCameraViewport();
if (initialized_) {
updateUniforms();
}
}
int Renderer::getVirtualWidth() const {
if (useVirtualResolution_) {
return std::max(virtualWidth_, 1);
}
return std::max(resolutionState_.windowWidth, 1);
}
int Renderer::getVirtualHeight() const {
if (useVirtualResolution_) {
return std::max(virtualHeight_, 1);
}
return std::max(resolutionState_.windowHeight, 1);
}
int Renderer::getUIVirtualWidth() const {
if (useSeparateUIVirtualResolution_ && uiVirtualWidth_ > 0 &&
uiVirtualHeight_ > 0) {
return std::max(uiVirtualWidth_, 1);
}
return getVirtualWidth();
}
int Renderer::getUIVirtualHeight() const {
if (useSeparateUIVirtualResolution_ && uiVirtualWidth_ > 0 &&
uiVirtualHeight_ > 0) {
return std::max(uiVirtualHeight_, 1);
}
return getVirtualHeight();
}
Rect Renderer::getViewportRect() const {
return resolutionState_.getWindowViewportRect();
}
Rect Renderer::getUIViewportRect() const {
return uiResolutionState_.getWindowViewportRect();
}
void Renderer::applyWorldViewport() const {
glViewport(resolutionState_.viewportX, resolutionState_.viewportY,
resolutionState_.viewportWidth, resolutionState_.viewportHeight);
}
void Renderer::applyUIViewport() const {
glViewport(uiResolutionState_.viewportX, uiResolutionState_.viewportY,
uiResolutionState_.viewportWidth, uiResolutionState_.viewportHeight);
}
Vec2 Renderer::screenToVirtual(const Vec2& screenPos) const {
Rect viewport = getViewportRect();
if (viewport.width() <= 0.0f || viewport.height() <= 0.0f) {
return screenPos;
}
float localX = screenPos.x - viewport.left();
float localY = screenPos.y - viewport.top();
return Vec2(localX * static_cast<float>(getVirtualWidth()) / viewport.width(),
localY * static_cast<float>(getVirtualHeight()) /
viewport.height());
}
Vec2 Renderer::screenToUIVirtual(const Vec2& screenPos) const {
Rect viewport = getUIViewportRect();
if (viewport.width() <= 0.0f || viewport.height() <= 0.0f) {
return screenPos;
}
float localX = screenPos.x - viewport.left();
float localY = screenPos.y - viewport.top();
return Vec2(localX * static_cast<float>(getUIVirtualWidth()) / viewport.width(),
localY * static_cast<float>(getUIVirtualHeight()) /
viewport.height());
}
Vec2 Renderer::virtualToScreen(const Vec2& virtualPos) const {
Rect viewport = getViewportRect();
float logicalWidth = static_cast<float>(std::max(getVirtualWidth(), 1));
float logicalHeight = static_cast<float>(std::max(getVirtualHeight(), 1));
return Vec2(viewport.left() + virtualPos.x * viewport.width() / logicalWidth,
viewport.top() + virtualPos.y * viewport.height() /
logicalHeight);
}
void Renderer::recalculateSingleResolutionState(RenderResolutionState& state,
bool useVirtualResolution,
int virtualWidth,
int virtualHeight,
ResolutionScaleMode mode) const {
state.windowWidth = std::max(state.windowWidth, 1);
state.windowHeight = std::max(state.windowHeight, 1);
state.contentScaleX = std::max(state.contentScaleX, kMinContentScale);
state.contentScaleY = std::max(state.contentScaleY, kMinContentScale);
state.drawableWidth = std::max(
roundToInt(static_cast<float>(state.windowWidth) * state.contentScaleX), 1);
state.drawableHeight = std::max(
roundToInt(static_cast<float>(state.windowHeight) * state.contentScaleY), 1);
int windowViewportX = 0;
int windowViewportY = 0;
int windowViewportWidth = state.windowWidth;
int windowViewportHeight = state.windowHeight;
int safeVirtualWidth = std::max(virtualWidth, 1);
int safeVirtualHeight = std::max(virtualHeight, 1);
if (useVirtualResolution) {
float widthScale = static_cast<float>(state.windowWidth) /
static_cast<float>(safeVirtualWidth);
float heightScale = static_cast<float>(state.windowHeight) /
static_cast<float>(safeVirtualHeight);
switch (mode) {
case ResolutionScaleMode::Disabled:
state.scaleX = widthScale;
state.scaleY = heightScale;
break;
case ResolutionScaleMode::FitHeight: {
float uniformScale = heightScale;
float fittedWidth = static_cast<float>(safeVirtualWidth) * uniformScale;
if (fittedWidth > static_cast<float>(state.windowWidth)) {
uniformScale = std::min(widthScale, heightScale);
}
windowViewportWidth = std::clamp(
roundToInt(static_cast<float>(safeVirtualWidth) * uniformScale), 1,
state.windowWidth);
windowViewportHeight = std::clamp(
roundToInt(static_cast<float>(safeVirtualHeight) * uniformScale), 1,
state.windowHeight);
windowViewportX = (state.windowWidth - windowViewportWidth) / 2;
windowViewportY = (state.windowHeight - windowViewportHeight) / 2;
state.scaleX = static_cast<float>(windowViewportWidth) /
static_cast<float>(safeVirtualWidth);
state.scaleY = static_cast<float>(windowViewportHeight) /
static_cast<float>(safeVirtualHeight);
break;
}
case ResolutionScaleMode::Fit:
default: {
float uniformScale = std::min(widthScale, heightScale);
windowViewportWidth = std::clamp(
roundToInt(static_cast<float>(safeVirtualWidth) * uniformScale), 1,
state.windowWidth);
windowViewportHeight = std::clamp(
roundToInt(static_cast<float>(safeVirtualHeight) * uniformScale), 1,
state.windowHeight);
windowViewportX = (state.windowWidth - windowViewportWidth) / 2;
windowViewportY = (state.windowHeight - windowViewportHeight) / 2;
state.scaleX = static_cast<float>(windowViewportWidth) /
static_cast<float>(safeVirtualWidth);
state.scaleY = static_cast<float>(windowViewportHeight) /
static_cast<float>(safeVirtualHeight);
break;
}
}
} else {
state.scaleX = 1.0f;
state.scaleY = 1.0f;
}
state.windowViewportX = windowViewportX;
state.windowViewportY = windowViewportY;
state.windowViewportWidth = windowViewportWidth;
state.windowViewportHeight = windowViewportHeight;
state.viewportX =
roundToInt(static_cast<float>(windowViewportX) * state.contentScaleX);
state.viewportY =
roundToInt(static_cast<float>(windowViewportY) * state.contentScaleY);
state.viewportWidth = std::max(
roundToInt(static_cast<float>(windowViewportWidth) * state.contentScaleX),
1);
state.viewportHeight = std::max(
roundToInt(static_cast<float>(windowViewportHeight) * state.contentScaleY),
1);
}
void Renderer::recalculateResolutionState() {
resolutionState_.windowWidth = std::max(resolutionState_.windowWidth, 1);
resolutionState_.windowHeight = std::max(resolutionState_.windowHeight, 1);
resolutionState_.contentScaleX =
std::max(resolutionState_.contentScaleX, kMinContentScale);
resolutionState_.contentScaleY =
std::max(resolutionState_.contentScaleY, kMinContentScale);
recalculateSingleResolutionState(resolutionState_, useVirtualResolution_,
virtualWidth_, virtualHeight_,
resolutionMode_);
uiResolutionState_ = resolutionState_;
if (useSeparateUIVirtualResolution_ && uiVirtualWidth_ > 0 &&
uiVirtualHeight_ > 0) {
recalculateSingleResolutionState(uiResolutionState_, true, uiVirtualWidth_,
uiVirtualHeight_, uiResolutionMode_);
}
syncCameraViewport();
if (initialized_) {
updateUniforms();
}
}
void Renderer::syncCameraViewport() {
if (!camera_) {
return;
}
if (activeRenderStyleRole_ == RenderStyleLayerRole::UI) {
camera_->setViewport(getUIVirtualWidth(), getUIVirtualHeight());
return;
}
camera_->setViewport(getVirtualWidth(), getVirtualHeight());
}
void Renderer::setupBlendMode(BlendMode mode) {
switch (mode) {
case BlendMode::None:
glDisable(GL_BLEND);
glBlendEquation(GL_FUNC_ADD);
break;
case BlendMode::Normal:
glEnable(GL_BLEND);
glBlendEquation(GL_FUNC_ADD);
glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE,
GL_ONE_MINUS_SRC_ALPHA);
break;
case BlendMode::Additive:
glEnable(GL_BLEND);
glBlendEquation(GL_FUNC_ADD);
glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE, GL_ONE, GL_ONE);
break;
case BlendMode::Screen:
glEnable(GL_BLEND);
glBlendEquation(GL_FUNC_ADD);
glBlendFuncSeparate(GL_ONE, GL_ONE_MINUS_SRC_COLOR, GL_ONE,
GL_ONE_MINUS_SRC_ALPHA);
break;
case BlendMode::Premultiplied:
glEnable(GL_BLEND);
glBlendEquation(GL_FUNC_ADD);
glBlendFuncSeparate(GL_ONE, GL_ONE_MINUS_SRC_ALPHA, GL_ONE,
GL_ONE_MINUS_SRC_ALPHA);
break;
case BlendMode::Subtractive:
glEnable(GL_BLEND);
glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE,
GL_ONE_MINUS_SRC_ALPHA);
glBlendEquation(GL_FUNC_REVERSE_SUBTRACT);
break;
case BlendMode::Multiply:
glEnable(GL_BLEND);
glBlendEquation(GL_FUNC_ADD);
glBlendFuncSeparate(GL_DST_COLOR, GL_ONE_MINUS_SRC_ALPHA, GL_ONE,
GL_ONE_MINUS_SRC_ALPHA);
break;
}
}
void Renderer::updateUniforms() {
if (!camera_) {
return;
}
for (const auto& [name, shaderPtr] : shaderManager_.getLoadedShaders()) {
(void)name;
Shader* shader = shaderPtr.Get();
if (!shader) {
continue;
}
shader->use();
shader->setMat4("u_view", camera_->getViewMatrix());
shader->setMat4("u_projection", camera_->getProjectionMatrix());
}
}
void Renderer::drawQuad(const Vec2& pos, const Size& size, float cr, float cg,
float cb, float ca) {
Rect rect(pos.x, pos.y, size.width, size.height);
Quad quad = Quad::create(rect, cr, cg, cb, ca);
Transform2D transform = Transform2D::identity();
auto* shader = shaderManager_.getShader("colored_quad");
if (shader) {
shader->use();
}
batch_.submitQuad(quad, transform, nullptr, shader, BlendMode::Normal);
}
void Renderer::drawQuad(const Vec2& pos, const Size& size, Ptr<Texture> texture) {
Rect rect(pos.x, pos.y, size.width, size.height);
Quad quad = Quad::createTextured(rect, Rect(0, 0, size.width, size.height),
Vec2(size.width, size.height),
1.0f, 1.0f, 1.0f, 1.0f,
shouldShrinkSubTextureUVs());
Transform2D transform = Transform2D::identity();
auto* shader = shaderManager_.getShader("sprite");
if (shader) {
shader->use();
}
batch_.submitQuad(quad, transform, texture, shader, BlendMode::Normal);
}
void Renderer::drawQuad(const Rect& rect, const Color& color) {
drawQuad(Vec2(rect.left(), rect.top()), Size(rect.width(), rect.height()),
color.r, color.g, color.b, color.a);
}
void Renderer::drawSprite(const Vec2& pos, const Size& size, Ptr<Texture> texture) {
drawQuad(pos, size, texture);
}
void Renderer::drawSprite(const Vec2& pos, const Rect& srcRect,
const Vec2& texSize, Ptr<Texture> texture,
const Color& color) {
Rect destRect(pos.x, pos.y, srcRect.width(), srcRect.height());
Quad quad = Quad::createTextured(destRect, srcRect, texSize, color.r, color.g,
color.b, color.a,
shouldShrinkSubTextureUVs());
Transform2D transform = Transform2D::identity();
auto* shader = shaderManager_.getShader("sprite");
if (shader) {
shader->use();
}
batch_.submitQuad(quad, transform, texture, shader, BlendMode::Normal);
}
} // namespace frostbite2D
@@ -0,0 +1,144 @@
#include "SDL_log.h"
#include <SDL2/SDL.h>
#include <frostbite2D/graphics/shader.h>
#include <frostbite2D/resource/asset.h>
#include <glad/glad.h>
#include <sstream>
namespace frostbite2D {
Shader::Shader(const std::string& name, uint32_t programID)
: name_(name), programID_(programID) {}
Shader::~Shader() {
if (programID_ != 0) {
glDeleteProgram(programID_);
programID_ = 0;
}
uniformLocations_.clear();
}
void Shader::use() {
glUseProgram(programID_);
}
void Shader::unuse() {
glUseProgram(0);
}
bool Shader::compile(uint32_t type, const std::string& source) {
uint32_t shaderID = glCreateShader(type);
const char* src = source.c_str();
glShaderSource(shaderID, 1, &src, nullptr);
glCompileShader(shaderID);
int success;
glGetShaderiv(shaderID, GL_COMPILE_STATUS, &success);
if (!success) {
int length;
glGetShaderiv(shaderID, GL_INFO_LOG_LENGTH, &length);
std::vector<char> log(length);
glGetShaderInfoLog(shaderID, length, &length, log.data());
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Shader compilation failed: %s", log.data());
glDeleteShader(shaderID);
return false;
}
return true;
}
bool Shader::link(uint32_t vertexID, uint32_t fragmentID) {
programID_ = glCreateProgram();
glAttachShader(programID_, vertexID);
glAttachShader(programID_, fragmentID);
glLinkProgram(programID_);
int success;
glGetProgramiv(programID_, GL_LINK_STATUS, &success);
if (!success) {
int length;
glGetProgramiv(programID_, GL_INFO_LOG_LENGTH, &length);
std::vector<char> log(length);
glGetProgramInfoLog(programID_, length, &length, log.data());
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Shader linking failed: %s", log.data());
glDeleteProgram(programID_);
programID_ = 0;
return false;
}
glDeleteShader(vertexID);
glDeleteShader(fragmentID);
return true;
}
int Shader::getUniformLocation(const std::string& name) {
auto it = uniformLocations_.find(name);
if (it != uniformLocations_.end()) {
return it->second;
}
int location = glGetUniformLocation(programID_, name.c_str());
uniformLocations_[name] = location;
return location;
}
void Shader::setInt(const std::string& name, int value) {
int location = getUniformLocation(name);
if (location >= 0) {
glUniform1i(location, value);
}
}
void Shader::setFloat(const std::string& name, float value) {
int location = getUniformLocation(name);
if (location >= 0) {
glUniform1f(location, value);
}
}
void Shader::setVec2(const std::string& name, const Vec2& value) {
int location = getUniformLocation(name);
if (location >= 0) {
glUniform2f(location, value.x, value.y);
}
}
void Shader::setVec3(const std::string& name, const glm::vec3& value) {
int location = getUniformLocation(name);
if (location >= 0) {
glUniform3f(location, value.x, value.y, value.z);
}
}
void Shader::setVec4(const std::string& name, const glm::vec4& value) {
int location = getUniformLocation(name);
if (location >= 0) {
glUniform4f(location, value.x, value.y, value.z, value.w);
}
}
void Shader::setMat3(const std::string& name, const glm::mat3& value) {
int location = getUniformLocation(name);
if (location >= 0) {
glUniformMatrix3fv(location, 1, GL_FALSE, &value[0][0]);
}
}
void Shader::setMat4(const std::string& name, const glm::mat4& value) {
int location = getUniformLocation(name);
if (location >= 0) {
glUniformMatrix4fv(location, 1, GL_FALSE, &value[0][0]);
}
}
void Shader::setTexture(const std::string& name, int slot) {
int location = getUniformLocation(name);
if (location >= 0) {
glUniform1i(location, slot);
}
}
}
@@ -0,0 +1,184 @@
#include "SDL_log.h"
#include <SDL2/SDL.h>
#include <frostbite2D/graphics/shader_manager.h>
#include <frostbite2D/graphics/shader.h>
#include <frostbite2D/resource/asset.h>
#include <json/json.hpp>
#include <fstream>
#include <sstream>
#include <glad/glad.h>
using json = nlohmann::json;
using json = nlohmann::json;
namespace frostbite2D {
ShaderManager& ShaderManager::get() {
static ShaderManager instance;
return instance;
}
// 移除析构函数中的自动销毁,改为在 Application::shutdown() 中手动调用
bool ShaderManager::init(const std::string& shadersDir) {
shadersDir_ = shadersDir;
shaders_.clear();
std::string configPath = shadersDir_ + "/shaders.json";
return loadShadersFromConfig(configPath);
}
void ShaderManager::shutdown() {
shaders_.clear();
}
Shader* ShaderManager::getShader(const std::string& name) {
auto it = shaders_.find(name);
if (it != shaders_.end()) {
return it->second.Get();
}
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Shader not found: %s", name.c_str());
return nullptr;
}
bool ShaderManager::hasShader(const std::string& name) const {
return shaders_.find(name) != shaders_.end();
}
bool ShaderManager::loadShader(const std::string& name, const std::string& vertPath,
const std::string& fragPath) {
Asset& asset = Asset::get();
std::string vertSource, fragSource;
if (!asset.readTextFile(vertPath, vertSource)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to load vertex shader: %s", vertPath.c_str());
return false;
}
if (!asset.readTextFile(fragPath, fragSource)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to load fragment shader: %s", fragPath.c_str());
return false;
}
uint32_t vertexID = glCreateShader(GL_VERTEX_SHADER);
uint32_t fragmentID = glCreateShader(GL_FRAGMENT_SHADER);
const char* vertSrc = vertSource.c_str();
glShaderSource(vertexID, 1, &vertSrc, nullptr);
glCompileShader(vertexID);
const char* fragSrc = fragSource.c_str();
glShaderSource(fragmentID, 1, &fragSrc, nullptr);
glCompileShader(fragmentID);
int success;
glGetShaderiv(vertexID, GL_COMPILE_STATUS, &success);
if (!success) {
int length;
glGetShaderiv(vertexID, GL_INFO_LOG_LENGTH, &length);
std::vector<char> log(length);
glGetShaderInfoLog(vertexID, length, &length, log.data());
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Vertex shader compilation failed: %s", log.data());
glDeleteShader(vertexID);
glDeleteShader(fragmentID);
return false;
}
glGetShaderiv(fragmentID, GL_COMPILE_STATUS, &success);
if (!success) {
int length;
glGetShaderiv(fragmentID, GL_INFO_LOG_LENGTH, &length);
std::vector<char> log(length);
glGetShaderInfoLog(fragmentID, length, &length, log.data());
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Fragment shader compilation failed: %s", log.data());
glDeleteShader(vertexID);
glDeleteShader(fragmentID);
return false;
}
uint32_t programID = glCreateProgram();
glAttachShader(programID, vertexID);
glAttachShader(programID, fragmentID);
// 绑定顶点属性位置,确保和 Batch 类的设置一致
glBindAttribLocation(programID, 0, "a_position");
glBindAttribLocation(programID, 1, "a_texCoord");
glBindAttribLocation(programID, 2, "a_color");
glLinkProgram(programID);
glGetProgramiv(programID, GL_LINK_STATUS, &success);
if (!success) {
int length;
glGetProgramiv(programID, GL_INFO_LOG_LENGTH, &length);
std::vector<char> log(length);
glGetProgramInfoLog(programID, length, &length, log.data());
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Shader linking failed: %s", log.data());
glDeleteProgram(programID);
glDeleteShader(vertexID);
glDeleteShader(fragmentID);
return false;
}
glDeleteShader(vertexID);
glDeleteShader(fragmentID);
auto shader = Ptr<Shader>(new Shader(name, programID));
shaders_[name] = shader;
SDL_Log("Loaded shader: %s", name.c_str());
return true;
}
bool ShaderManager::loadShadersFromConfig(const std::string& configPath) {
Asset& asset = Asset::get();
std::string content;
if (!asset.readTextFile(configPath, content)) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "No shaders.json found, will compile shaders on demand");
return true;
}
try {
json config = json::parse(content);
if (!config.contains("shaders")) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Invalid shaders.json: missing 'shaders' key");
return false;
}
auto shaders = config["shaders"];
for (auto& [key, value] : shaders.items()) {
if (!value.contains("vertex") || !value.contains("fragment")) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Invalid shader entry: %s", key.c_str());
continue;
}
std::string vertFile = value["vertex"];
std::string fragFile = value["fragment"];
std::string vertPath = shadersDir_ + "/" + vertFile;
std::string fragPath = shadersDir_ + "/" + fragFile;
if (!loadShader(key, vertPath, fragPath)) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Failed to load shader: %s", key.c_str());
}
}
SDL_Log("Loaded %zu shaders from config", shaders_.size());
return true;
} catch (const json::parse_error& e) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to parse shaders.json: %s", e.what());
return false;
} catch (const std::exception& e) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Error loading shaders: %s", e.what());
return false;
}
}
}
@@ -0,0 +1,192 @@
#include "SDL_log.h"
#include <SDL2/SDL.h>
#include <SDL2/SDL_image.h>
#include <cstdio>
#include <cstdlib>
#include <frostbite2D/graphics/texture.h>
#include <frostbite2D/resource/asset.h>
#include <glad/glad.h>
namespace frostbite2D {
Texture::Texture(int width, int height, uint32_t id)
: width_(width), height_(height), textureID_(id) {}
Texture::~Texture() {
if (textureID_ != 0) {
glDeleteTextures(1, &textureID_);
textureID_ = 0;
}
}
Ptr<Texture> Texture::loadFromFile(const std::string& path) {
Asset& asset = Asset::get();
std::string resolvedPath = asset.resolveAssetPath(path);
std::vector<uint8> fileData;
if (!asset.readBinaryFile(path, fileData)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to read texture file: %s", resolvedPath.c_str());
return nullptr;
}
if (fileData.empty()) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Texture file is empty: %s",
resolvedPath.c_str());
return nullptr;
}
SDL_RWops *rw =
SDL_RWFromConstMem(fileData.data(), static_cast<int>(fileData.size()));
if (!rw) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to create RWops: %s",
SDL_GetError());
return nullptr;
}
SDL_Surface *surface = IMG_Load_RW(rw, 1);
if (!surface) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to load image '%s': %s", resolvedPath.c_str(),
IMG_GetError());
return nullptr;
}
GLenum format;
int channels;
if (surface->format->BytesPerPixel == 4) {
format = GL_RGBA;
channels = 4;
} else {
format = GL_RGB;
channels = 3;
}
int width = surface->w;
int height = surface->h;
uint32_t textureID;
glGenTextures(1, &textureID);
glBindTexture(GL_TEXTURE_2D, textureID);
glTexImage2D(GL_TEXTURE_2D, 0, format, width, height, 0, format,
GL_UNSIGNED_BYTE, surface->pixels);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glBindTexture(GL_TEXTURE_2D, 0);
SDL_FreeSurface(surface);
auto texture = Ptr<Texture>(new Texture(width, height, textureID));
texture->path_ = resolvedPath;
texture->channels_ = channels;
texture->appliedMinFilter_ = GL_LINEAR;
texture->appliedMagFilter_ = GL_LINEAR;
return texture;
}
Ptr<Texture> Texture::createFromMemory(const uint8* data, int width, int height, int channels) {
if (!data) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to create texture from memory: null data");
return nullptr;
}
uint32_t format = channels == 4 ? GL_RGBA : GL_RGB;
uint32_t textureID;
glGenTextures(1, &textureID);
glBindTexture(GL_TEXTURE_2D, textureID);
glTexImage2D(GL_TEXTURE_2D, 0, format, width, height, 0, format, GL_UNSIGNED_BYTE, data);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glBindTexture(GL_TEXTURE_2D, 0);
auto texture = Ptr<Texture>(new Texture(width, height, textureID));
texture->channels_ = channels;
texture->appliedMinFilter_ = GL_LINEAR;
texture->appliedMagFilter_ = GL_LINEAR;
return texture;
}
Ptr<Texture> Texture::createEmpty(int width, int height) {
uint32_t textureID;
glGenTextures(1, &textureID);
glBindTexture(GL_TEXTURE_2D, textureID);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glBindTexture(GL_TEXTURE_2D, 0);
auto texture = Ptr<Texture>(new Texture(width, height, textureID));
texture->appliedMinFilter_ = GL_LINEAR;
texture->appliedMagFilter_ = GL_LINEAR;
return texture;
}
void Texture::bind(uint32_t slot, bool preferPixelArtSampling) {
glActiveTexture(GL_TEXTURE0 + slot);
glBindTexture(GL_TEXTURE_2D, textureID_);
applySampling(preferPixelArtSampling);
}
void Texture::unbind() {
glBindTexture(GL_TEXTURE_2D, 0);
}
void Texture::setWrapMode(uint32_t wrapS, uint32_t wrapT) {
bind();
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, wrapS);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, wrapT);
unbind();
}
void Texture::setFilterMode(uint32_t minFilter, uint32_t magFilter) {
bind();
applyFilter(minFilter, magFilter);
sampling_ = (minFilter == GL_NEAREST && magFilter == GL_NEAREST)
? TextureSampling::PixelArt
: TextureSampling::Linear;
unbind();
}
void Texture::setSampling(TextureSampling sampling) {
sampling_ = sampling;
appliedMinFilter_ = 0;
appliedMagFilter_ = 0;
}
void Texture::applyFilter(uint32_t minFilter, uint32_t magFilter) {
if (appliedMinFilter_ == minFilter && appliedMagFilter_ == magFilter) {
return;
}
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, minFilter);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, magFilter);
appliedMinFilter_ = minFilter;
appliedMagFilter_ = magFilter;
}
void Texture::applySampling(bool preferPixelArtSampling) {
bool useNearest =
sampling_ == TextureSampling::PixelArt && preferPixelArtSampling;
applyFilter(useNearest ? GL_NEAREST : GL_LINEAR,
useNearest ? GL_NEAREST : GL_LINEAR);
}
}
@@ -0,0 +1,17 @@
#include <frostbite2D/platform/switch.h>
#ifdef __SWITCH__
namespace frostbite2D {
void switchInit() {
// 初始化Switch平台相关的资源
socketInitializeDefault();
nxlinkStdio();
}
void switchShutdown() {
// 清理Switch平台相关的资源
socketExit();
}
} // namespace frostbite2D
#endif
@@ -0,0 +1,14 @@
#include <frostbite2D/platform/windows.h>
#ifdef _WIN32
namespace frostbite2D {
void windowsInit() {
SetConsoleOutputCP(CP_UTF8);
SetConsoleCP(CP_UTF8);
}
} // namespace frostbite2D
#endif
@@ -0,0 +1,447 @@
#include <frostbite2D/resource/asset.h>
#include <fstream>
#include <sstream>
#include <system_error>
#include <algorithm>
#include <cctype>
namespace frostbite2D {
Asset &Asset::get() {
static Asset instance;
return instance;
}
fs::path Asset::toPath(const std::string &path) const {
#ifdef _WIN32
return fs::u8path(path);
#else
return fs::path(path);
#endif
}
std::string Asset::fromPath(const fs::path &path) const {
#ifdef _WIN32
return path.u8string();
#else
return path.string();
#endif
}
std::string Asset::resolveFullPath(const std::string &path) const {
if (path.empty()) {
return path;
}
fs::path p = toPath(path);
if (p.is_absolute()) {
return path;
}
if (!workingDirectory_.empty()) {
return fromPath(toPath(workingDirectory_) / p);
}
return path;
}
bool Asset::readTextFile(const std::string &path, std::string &outContent) {
std::string fullPath = resolveFullPath(path);
if (!exists(fullPath)) {
return false;
}
std::ifstream file(toPath(fullPath), std::ios::in | std::ios::binary);
if (!file.is_open()) {
return false;
}
std::ostringstream ss;
ss << file.rdbuf();
outContent = ss.str();
file.close();
return true;
}
bool Asset::writeTextFile(const std::string &path, const std::string &content,
bool append) {
std::string fullPath = resolveFullPath(path);
auto mode = std::ios::out;
if (append) {
mode |= std::ios::app;
}
std::ofstream file(toPath(fullPath), mode);
if (!file.is_open()) {
return false;
}
file << content;
file.close();
return true;
}
bool Asset::readBinaryFile(const std::string &path,
std::vector<uint8> &outData) {
std::string fullPath = resolveFullPath(path);
if (!exists(fullPath)) {
return false;
}
std::ifstream file(toPath(fullPath), std::ios::in | std::ios::binary);
if (!file.is_open()) {
return false;
}
file.seekg(0, std::ios::end);
auto size = file.tellg();
file.seekg(0, std::ios::beg);
outData.resize(static_cast<size_t>(size));
file.read(reinterpret_cast<char *>(outData.data()), size);
file.close();
return true;
}
bool Asset::writeBinaryFile(const std::string &path,
const std::vector<uint8> &data, bool append) {
std::string fullPath = resolveFullPath(path);
auto mode = std::ios::out | std::ios::binary;
if (append) {
mode |= std::ios::app;
}
std::ofstream file(toPath(fullPath), mode);
if (!file.is_open()) {
return false;
}
file.write(reinterpret_cast<const char *>(data.data()), data.size());
file.close();
return true;
}
bool Asset::exists(const std::string &path) const {
std::error_code ec;
return fs::exists(toPath(resolveFullPath(path)), ec);
}
bool Asset::isDirectory(const std::string &path) const {
std::error_code ec;
return fs::is_directory(toPath(resolveFullPath(path)), ec);
}
bool Asset::isRegularFile(const std::string &path) const {
std::error_code ec;
return fs::is_regular_file(toPath(resolveFullPath(path)), ec);
}
bool Asset::createDirectory(const std::string &path) {
std::error_code ec;
return fs::create_directory(toPath(resolveFullPath(path)), ec);
}
bool Asset::createDirectories(const std::string &path) {
std::error_code ec;
return fs::create_directories(toPath(resolveFullPath(path)), ec);
}
bool Asset::removeFile(const std::string &path) {
std::error_code ec;
return fs::remove(toPath(resolveFullPath(path)), ec);
}
bool Asset::removeDirectory(const std::string &path) {
std::error_code ec;
return fs::remove_all(toPath(resolveFullPath(path)), ec) > 0;
}
bool Asset::copyFile(const std::string &from, const std::string &to,
bool overwrite) {
std::error_code ec;
auto options =
overwrite ? fs::copy_options::overwrite_existing : fs::copy_options::none;
return fs::copy_file(toPath(resolveFullPath(from)),
toPath(resolveFullPath(to)), options, ec);
}
bool Asset::moveFile(const std::string &from, const std::string &to) {
std::error_code ec;
fs::rename(toPath(resolveFullPath(from)), toPath(resolveFullPath(to)), ec);
return !ec;
}
bool Asset::rename(const std::string &oldPath, const std::string &newPath) {
std::error_code ec;
fs::rename(toPath(resolveFullPath(oldPath)), toPath(resolveFullPath(newPath)),
ec);
return !ec;
}
std::vector<std::string> Asset::listFiles(const std::string &directoryPath,
bool recursive) {
std::vector<std::string> files;
fs::path fullPath = toPath(resolveFullPath(directoryPath));
std::error_code ec;
if (!fs::exists(fullPath, ec) || !fs::is_directory(fullPath, ec)) {
return files;
}
try {
if (recursive) {
for (const auto &entry : fs::recursive_directory_iterator(fullPath, ec)) {
if (entry.is_regular_file()) {
files.push_back(fromPath(entry.path()));
}
}
} else {
for (const auto &entry : fs::directory_iterator(fullPath, ec)) {
if (entry.is_regular_file()) {
files.push_back(fromPath(entry.path()));
}
}
}
} catch (...) {
}
return files;
}
std::vector<std::string>
Asset::listDirectories(const std::string &directoryPath, bool recursive) {
std::vector<std::string> directories;
fs::path fullPath = toPath(resolveFullPath(directoryPath));
std::error_code ec;
if (!fs::exists(fullPath, ec) || !fs::is_directory(fullPath, ec)) {
return directories;
}
try {
if (recursive) {
for (const auto &entry : fs::recursive_directory_iterator(fullPath, ec)) {
if (entry.is_directory()) {
directories.push_back(fromPath(entry.path()));
}
}
} else {
for (const auto &entry : fs::directory_iterator(fullPath, ec)) {
if (entry.is_directory()) {
directories.push_back(fromPath(entry.path()));
}
}
}
} catch (...) {
}
return directories;
}
std::vector<std::string> Asset::listAll(const std::string &directoryPath,
bool recursive) {
std::vector<std::string> items;
fs::path fullPath = toPath(resolveFullPath(directoryPath));
std::error_code ec;
if (!fs::exists(fullPath, ec) || !fs::is_directory(fullPath, ec)) {
return items;
}
try {
if (recursive) {
for (const auto &entry : fs::recursive_directory_iterator(fullPath, ec)) {
items.push_back(fromPath(entry.path()));
}
} else {
for (const auto &entry : fs::directory_iterator(fullPath, ec)) {
items.push_back(fromPath(entry.path()));
}
}
} catch (...) {
}
return items;
}
std::vector<std::string>
Asset::listFilesWithExtension(const std::string &directoryPath,
const std::string &extension, bool recursive) {
std::vector<std::string> files;
fs::path fullPath = toPath(resolveFullPath(directoryPath));
std::string extLower = extension;
std::transform(extLower.begin(), extLower.end(), extLower.begin(),
[](unsigned char c) { return std::tolower(c); });
std::error_code ec;
if (!fs::exists(fullPath, ec) || !fs::is_directory(fullPath, ec)) {
return files;
}
try {
auto checkExtension = [&](const fs::path& filePath) {
std::string fileExt = fromPath(filePath.extension());
std::transform(fileExt.begin(), fileExt.end(), fileExt.begin(),
[](unsigned char c) { return std::tolower(c); });
return fileExt == extLower;
};
if (recursive) {
for (const auto &entry : fs::recursive_directory_iterator(fullPath, ec)) {
if (entry.is_regular_file() && checkExtension(entry.path())) {
files.push_back(fromPath(entry.path()));
}
}
} else {
for (const auto &entry : fs::directory_iterator(fullPath, ec)) {
if (entry.is_regular_file() && checkExtension(entry.path())) {
files.push_back(fromPath(entry.path()));
}
}
}
} catch (...) {
}
return files;
}
FileInfo Asset::getFileInfo(const std::string &path) {
FileInfo info;
std::string fullPath = resolveFullPath(path);
info.fullPath = fullPath;
if (!exists(fullPath)) {
info.exists = false;
return info;
}
info.exists = true;
info.isDirectory = isDirectory(fullPath);
info.isRegularFile = isRegularFile(fullPath);
fs::path p = toPath(fullPath);
info.name = fromPath(p.filename());
info.extension = fromPath(p.extension());
if (info.isRegularFile) {
std::error_code ec;
info.size = fs::file_size(p, ec);
if (ec) {
info.size = 0;
}
}
return info;
}
std::string Asset::getFileName(const std::string &path) const {
return fromPath(toPath(path).filename());
}
std::string Asset::getFileNameWithoutExtension(const std::string &path) const {
return fromPath(toPath(path).stem());
}
std::string Asset::getExtension(const std::string &path) const {
return fromPath(toPath(path).extension());
}
std::string Asset::getParentPath(const std::string &path) const {
return fromPath(toPath(path).parent_path());
}
std::string Asset::getAbsolutePath(const std::string &path) const {
std::error_code ec;
return fromPath(fs::absolute(toPath(resolveFullPath(path)), ec));
}
std::string Asset::getCanonicalPath(const std::string &path) const {
std::error_code ec;
return fromPath(fs::canonical(toPath(resolveFullPath(path)), ec));
}
std::string Asset::getCurrentPath() const {
std::error_code ec;
return fromPath(fs::current_path(ec));
}
bool Asset::setCurrentPath(const std::string &path) {
std::error_code ec;
fs::current_path(toPath(path), ec);
return !ec;
}
std::string Asset::combinePath(const std::string &left,
const std::string &right) const {
return fromPath(toPath(left) / toPath(right));
}
std::string Asset::normalizePath(const std::string &path) const {
std::error_code ec;
auto p = fs::absolute(toPath(resolveFullPath(path)), ec);
if (ec) {
return path;
}
return fromPath(p.make_preferred());
}
uint64 Asset::getFileSize(const std::string &path) const {
std::error_code ec;
auto size = fs::file_size(toPath(resolveFullPath(path)), ec);
if (ec) {
return 0;
}
return size;
}
bool Asset::isEmpty(const std::string &path) const {
std::error_code ec;
return fs::is_empty(toPath(resolveFullPath(path)), ec);
}
std::optional<std::string> Asset::readFileToString(const std::string &path) {
std::string content;
if (readTextFile(path, content)) {
return content;
}
return std::nullopt;
}
std::optional<std::vector<uint8>>
Asset::readFileToBytes(const std::string &path) {
std::vector<uint8> data;
if (readBinaryFile(path, data)) {
return data;
}
return std::nullopt;
}
void Asset::setWorkingDirectory(const std::string &path) {
workingDirectory_ = path;
}
const std::string &Asset::getWorkingDirectory() const {
return workingDirectory_;
}
std::string Asset::resolvePath(const std::string &relativePath) const {
return resolveFullPath(relativePath);
}
void Asset::setAssetRoot(const std::string &root) { assetRoot_ = root; }
const std::string &Asset::getAssetRoot() const { return assetRoot_; }
std::string Asset::resolveAssetPath(const std::string &relativePath) const {
if (assetRoot_.empty()) {
return resolveFullPath(relativePath);
}
return resolveFullPath(combinePath(assetRoot_, relativePath));
}
} // namespace frostbite2D
@@ -0,0 +1,494 @@
#include <frostbite2D/resource/audio_database.h>
#include <frostbite2D/resource/asset.h>
#include <rapidxml/rapidxml.hpp>
#include <SDL.h>
#include <algorithm>
#include <chrono>
#include <cstring>
#include <ctime>
#include <numeric>
#include <random>
namespace frostbite2D {
AudioDatabase& AudioDatabase::get() {
static AudioDatabase instance;
return instance;
}
bool AudioDatabase::loadFromFile(const std::string& path) {
clear();
Asset& asset = Asset::get();
std::string content;
if (!asset.readTextFile(path, content)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "AudioDatabase: 无法读取文件: %s", path.c_str());
return false;
}
return loadFromString(content);
}
bool AudioDatabase::loadFromString(const std::string& xmlContent) {
clear();
if (xmlContent.empty()) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "AudioDatabase: XML 内容为空");
return false;
}
std::string mutableContent = xmlContent;
rapidxml::xml_document<> doc;
try {
doc.parse<rapidxml::parse_default>(&mutableContent[0]);
} catch (const rapidxml::parse_error& e) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "AudioDatabase: XML 解析失败: %s", e.what());
return false;
}
rapidxml::xml_node<>* root = doc.first_node("AudioTagDatabase");
if (!root) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "AudioDatabase: 未找到 AudioTagDatabase 根元素");
return false;
}
for (rapidxml::xml_node<>* node = root->first_node(); node; node = node->next_sibling()) {
const char* name = node->name();
if (strcmp(name, "EFFECT") == 0) {
rapidxml::xml_attribute<>* idAttr = node->first_attribute("ID");
rapidxml::xml_attribute<>* fileAttr = node->first_attribute("FILE");
if (!idAttr || !fileAttr) {
continue;
}
if (strlen(idAttr->value()) == 0) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "AudioDatabase: 跳过无效的 EFFECT 条目(ID 为空)");
continue;
}
if (strlen(fileAttr->value()) == 0) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "AudioDatabase: 跳过 EFFECT 条目(FILE 为空): %s", idAttr->value());
continue;
}
AudioEffect effect;
effect.id = idAttr->value();
effect.file = fileAttr->value();
rapidxml::xml_attribute<>* subgroupAttr = node->first_attribute("SUBGROUP");
if (subgroupAttr) {
effect.subgroup = subgroupAttr->value();
}
rapidxml::xml_attribute<>* loopDelayAttr = node->first_attribute("LOOP_DELAY");
if (loopDelayAttr) {
effect.loopDelay = std::stoi(loopDelayAttr->value());
}
rapidxml::xml_attribute<>* loopDelayRangeAttr = node->first_attribute("LOOP_DELAY_RANGE");
if (loopDelayRangeAttr) {
effect.loopDelayRange = std::stoi(loopDelayRangeAttr->value());
}
effectMap_[effect.id] = std::move(effect);
} else if (strcmp(name, "AMBIENT") == 0) {
rapidxml::xml_attribute<>* idAttr = node->first_attribute("ID");
rapidxml::xml_attribute<>* fileAttr = node->first_attribute("FILE");
if (!idAttr || !fileAttr) {
continue;
}
if (strlen(idAttr->value()) == 0) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"AudioDatabase: skip invalid AMBIENT entry (empty ID)");
continue;
}
if (strlen(fileAttr->value()) == 0) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"AudioDatabase: skip AMBIENT entry with empty FILE: %s",
idAttr->value());
continue;
}
AudioAmbient ambient;
ambient.id = idAttr->value();
ambient.file = fileAttr->value();
rapidxml::xml_attribute<>* loopDelayAttr = node->first_attribute("LOOP_DELAY");
if (loopDelayAttr) {
ambient.loopDelay = std::stoi(loopDelayAttr->value());
}
rapidxml::xml_attribute<>* loopDelayRangeAttr = node->first_attribute("LOOP_DELAY_RANGE");
if (loopDelayRangeAttr) {
ambient.loopDelayRange = std::stoi(loopDelayRangeAttr->value());
}
ambientMap_[ambient.id] = std::move(ambient);
} else if (strcmp(name, "MUSIC") == 0) {
rapidxml::xml_attribute<>* idAttr = node->first_attribute("ID");
rapidxml::xml_attribute<>* fileAttr = node->first_attribute("FILE");
if (!idAttr || !fileAttr) {
continue;
}
if (strlen(idAttr->value()) == 0) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "AudioDatabase: 跳过无效的 MUSIC 条目(ID 为空)");
continue;
}
if (strlen(fileAttr->value()) == 0) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "AudioDatabase: 跳过 MUSIC 条目(FILE 为空): %s", idAttr->value());
continue;
}
AudioMusic music;
music.id = idAttr->value();
music.file = fileAttr->value();
rapidxml::xml_attribute<>* loopDelayAttr = node->first_attribute("LOOP_DELAY");
if (loopDelayAttr) {
music.loopDelay = std::stoi(loopDelayAttr->value());
}
musicMap_[music.id] = std::move(music);
} else if (strcmp(name, "RANDOM") == 0) {
rapidxml::xml_attribute<>* idAttr = node->first_attribute("ID");
if (!idAttr) {
continue;
}
if (strlen(idAttr->value()) == 0) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "AudioDatabase: 跳过无效的 RANDOM 条目(ID 为空)");
continue;
}
AudioRandom random;
random.id = idAttr->value();
for (rapidxml::xml_node<>* itemNode = node->first_node("ITEM"); itemNode; itemNode = itemNode->next_sibling("ITEM")) {
rapidxml::xml_attribute<>* tagAttr = itemNode->first_attribute("TAG");
rapidxml::xml_attribute<>* probAttr = itemNode->first_attribute("PROB");
if (!tagAttr || !probAttr) {
continue;
}
if (strlen(tagAttr->value()) == 0) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "AudioDatabase: 跳过 RANDOM 条目下的无效 ITEMTAG 为空): %s", random.id.c_str());
continue;
}
RandomItem ri;
ri.tag = tagAttr->value();
ri.probability = std::stoi(probAttr->value());
random.items.push_back(std::move(ri));
}
randomMap_[random.id] = std::move(random);
}
}
loaded_ = true;
SDL_Log("AudioDatabase: loaded - %zu effects, %zu ambients, %zu musics, %zu random groups",
effectMap_.size(), ambientMap_.size(), musicMap_.size(),
randomMap_.size());
return true;
}
void AudioDatabase::clear() {
effectMap_.clear();
ambientMap_.clear();
musicMap_.clear();
randomMap_.clear();
loaded_ = false;
}
bool AudioDatabase::isLoaded() const {
return loaded_;
}
std::optional<AudioEntry> AudioDatabase::get(const std::string& id) {
if (!loaded_) {
return std::nullopt;
}
AudioEntry entry;
auto effectIt = effectMap_.find(id);
if (effectIt != effectMap_.end()) {
entry.type = AudioEntryType::Effect;
entry.effect = &effectIt->second;
return entry;
}
auto ambientIt = ambientMap_.find(id);
if (ambientIt != ambientMap_.end()) {
entry.type = AudioEntryType::Ambient;
entry.ambient = &ambientIt->second;
return entry;
}
auto musicIt = musicMap_.find(id);
if (musicIt != musicMap_.end()) {
entry.type = AudioEntryType::Music;
entry.music = &musicIt->second;
return entry;
}
auto randomIt = randomMap_.find(id);
if (randomIt != randomMap_.end()) {
entry.type = AudioEntryType::Random;
entry.random = &randomIt->second;
return entry;
}
return std::nullopt;
}
std::optional<std::string> AudioDatabase::getSound(const std::string& id) {
if (!loaded_) {
return std::nullopt;
}
auto randomIt = randomMap_.find(id);
if (randomIt != randomMap_.end()) {
return selectFromRandom(randomIt->second);
}
if (effectMap_.find(id) != effectMap_.end()) {
return id;
}
return std::nullopt;
}
std::optional<std::string> AudioDatabase::getFilePath(const std::string& id) {
if (!loaded_) {
return std::nullopt;
}
auto soundIdOpt = getSound(id);
if (!soundIdOpt) {
auto ambientIt = ambientMap_.find(id);
if (ambientIt != ambientMap_.end()) {
return ambientIt->second.file;
}
auto musicIt = musicMap_.find(id);
if (musicIt != musicMap_.end()) {
return musicIt->second.file;
}
return std::nullopt;
}
const std::string& soundId = *soundIdOpt;
auto effectIt = effectMap_.find(soundId);
if (effectIt != effectMap_.end()) {
return effectIt->second.file;
}
return std::nullopt;
}
std::optional<const AudioEffect*> AudioDatabase::getEffect(const std::string& id) {
if (!loaded_) {
return std::nullopt;
}
auto it = effectMap_.find(id);
if (it != effectMap_.end()) {
return &it->second;
}
return std::nullopt;
}
std::optional<const AudioAmbient*> AudioDatabase::getAmbient(const std::string& id) {
if (!loaded_) {
return std::nullopt;
}
auto it = ambientMap_.find(id);
if (it != ambientMap_.end()) {
return &it->second;
}
return std::nullopt;
}
std::optional<const AudioMusic*> AudioDatabase::getMusic(const std::string& id) {
if (!loaded_) {
return std::nullopt;
}
auto it = musicMap_.find(id);
if (it != musicMap_.end()) {
return &it->second;
}
return std::nullopt;
}
std::optional<const AudioRandom*> AudioDatabase::getRandom(const std::string& id) {
if (!loaded_) {
return std::nullopt;
}
auto it = randomMap_.find(id);
if (it != randomMap_.end()) {
return &it->second;
}
return std::nullopt;
}
size_t AudioDatabase::effectCount() const {
return effectMap_.size();
}
size_t AudioDatabase::ambientCount() const {
return ambientMap_.size();
}
size_t AudioDatabase::musicCount() const {
return musicMap_.size();
}
size_t AudioDatabase::randomCount() const {
return randomMap_.size();
}
void AudioDatabase::initializeRNG() {
if (rngInitialized_) {
return;
}
std::random_device rd;
if (rd.entropy() > 0) {
rng_.seed(rd());
} else {
auto now = std::chrono::high_resolution_clock::now();
auto duration = now.time_since_epoch();
auto millis = std::chrono::duration_cast<std::chrono::milliseconds>(duration).count();
rng_.seed(static_cast<unsigned int>(millis));
}
rngInitialized_ = true;
SDL_Log("AudioDatabase: 随机数生成器已初始化");
}
std::string AudioDatabase::selectFromRandom(const AudioRandom& randomGroup) {
initializeRNG();
std::vector<const RandomItem*> validItems;
for (const auto& item : randomGroup.items) {
if (!item.tag.empty()) {
validItems.push_back(&item);
}
}
if (validItems.empty()) {
return "";
}
if (validItems.size() == 1) {
return validItems[0]->tag;
}
int32 totalWeight = 0;
for (const auto* item : validItems) {
totalWeight += item->probability;
}
if (totalWeight <= 0) {
std::uniform_int_distribution<size_t> dist(0, validItems.size() - 1);
return validItems[dist(rng_)]->tag;
}
std::uniform_int_distribution<int32> dist(1, totalWeight);
int32 randomValue = dist(rng_);
int32 currentWeight = 0;
for (const auto* item : validItems) {
currentWeight += item->probability;
if (randomValue <= currentWeight) {
return item->tag;
}
}
return validItems.back()->tag;
}
bool AudioDatabase::has(const std::string& id) {
if (!loaded_) {
return false;
}
return effectMap_.count(id) > 0 || ambientMap_.count(id) > 0 ||
musicMap_.count(id) > 0 || randomMap_.count(id) > 0;
}
std::string AudioDatabase::filePath(const std::string& id) {
auto opt = getFilePath(id);
return opt ? *opt : "";
}
std::string AudioDatabase::soundId(const std::string& idOrRandomId) {
auto opt = getSound(idOrRandomId);
return opt ? *opt : "";
}
std::string AudioDatabase::subgroup(const std::string& id) {
if (!loaded_) {
return "";
}
auto it = effectMap_.find(id);
if (it != effectMap_.end()) {
return it->second.subgroup;
}
return "";
}
AudioEntryType AudioDatabase::typeOf(const std::string& id) {
if (!loaded_) {
return AudioEntryType::None;
}
if (effectMap_.count(id) > 0) {
return AudioEntryType::Effect;
}
if (ambientMap_.count(id) > 0) {
return AudioEntryType::Ambient;
}
if (musicMap_.count(id) > 0) {
return AudioEntryType::Music;
}
if (randomMap_.count(id) > 0) {
return AudioEntryType::Random;
}
return AudioEntryType::None;
}
bool AudioDatabase::tryGetFilePath(const std::string& id, std::string& outPath) {
auto opt = getFilePath(id);
if (opt) {
outPath = *opt;
return true;
}
return false;
}
bool AudioDatabase::tryGetSoundId(const std::string& id, std::string& outSoundId) {
auto opt = getSound(id);
if (opt) {
outSoundId = *opt;
return true;
}
return false;
}
} // namespace frostbite2D
@@ -0,0 +1,236 @@
#include <frostbite2D/resource/binary_file_stream_reader.h>
#include <SDL.h>
#include <algorithm>
#include <filesystem>
#include <frostbite2D/resource/asset.h>
namespace frostbite2D {
namespace {
namespace fs = std::filesystem;
fs::path toFsPath(const std::string& path) {
#ifdef _WIN32
return fs::u8path(path);
#else
return fs::path(path);
#endif
}
} // namespace
BinaryFileStreamReader::BinaryFileStreamReader(const std::string& filePath) {
open(filePath);
}
BinaryFileStreamReader::~BinaryFileStreamReader() {
close();
}
bool BinaryFileStreamReader::open(const std::string& filePath) {
close();
Asset& asset = Asset::get();
filePath_ = asset.resolvePath(filePath);
stream_.open(toFsPath(filePath_), std::ios::binary);
if (!stream_.is_open()) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"BinaryFileStreamReader: unable to open file: %s",
filePath.c_str());
filePath_.clear();
return false;
}
stream_.seekg(0, std::ios::end);
std::streamoff endPos = stream_.tellg();
if (endPos < 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"BinaryFileStreamReader: failed to determine file size: %s",
filePath_.c_str());
close();
return false;
}
size_ = static_cast<size_t>(endPos);
stream_.seekg(0, std::ios::beg);
position_ = 0;
lastReadCount_ = 0;
return true;
}
void BinaryFileStreamReader::close() {
if (stream_.is_open()) {
stream_.close();
}
filePath_.clear();
size_ = 0;
position_ = 0;
lastReadCount_ = 0;
}
bool BinaryFileStreamReader::isOpen() const {
return stream_.is_open();
}
size_t BinaryFileStreamReader::tell() const {
return position_;
}
void BinaryFileStreamReader::seek(size_t pos) {
if (!isOpen()) {
position_ = 0;
lastReadCount_ = 0;
return;
}
position_ = std::min(pos, size_);
lastReadCount_ = 0;
stream_.clear();
stream_.seekg(static_cast<std::streamoff>(position_), std::ios::beg);
}
void BinaryFileStreamReader::skip(size_t count) {
seek(position_ + count);
}
bool BinaryFileStreamReader::eof() const {
return position_ >= size_;
}
size_t BinaryFileStreamReader::size() const {
return size_;
}
size_t BinaryFileStreamReader::remaining() const {
return position_ >= size_ ? 0 : size_ - position_;
}
size_t BinaryFileStreamReader::lastReadCount() const {
return lastReadCount_;
}
size_t BinaryFileStreamReader::read(char* buffer, size_t size) {
if (!buffer || size == 0 || eof() || !isOpen()) {
lastReadCount_ = 0;
return 0;
}
size_t bytesToRead = std::min(size, remaining());
stream_.clear();
stream_.read(buffer, static_cast<std::streamsize>(bytesToRead));
size_t bytesRead = static_cast<size_t>(stream_.gcount());
position_ += bytesRead;
lastReadCount_ = bytesRead;
if (bytesRead != bytesToRead) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"BinaryFileStreamReader: incomplete read, expected %zu bytes, got %zu bytes from %s",
bytesToRead, bytesRead, filePath_.c_str());
}
return bytesRead;
}
std::vector<uint8> BinaryFileStreamReader::readBytes(size_t size) {
std::vector<uint8> result;
size_t bytesToRead = std::min(size, remaining());
if (bytesToRead == 0) {
lastReadCount_ = 0;
return result;
}
result.resize(bytesToRead);
size_t bytesRead = read(reinterpret_cast<char*>(result.data()), bytesToRead);
result.resize(bytesRead);
return result;
}
int8 BinaryFileStreamReader::readInt8() {
return read<int8>();
}
int16 BinaryFileStreamReader::readInt16() {
return read<int16>();
}
int32 BinaryFileStreamReader::readInt32() {
return read<int32>();
}
int64 BinaryFileStreamReader::readInt64() {
return read<int64>();
}
uint8 BinaryFileStreamReader::readUInt8() {
return read<uint8>();
}
uint16 BinaryFileStreamReader::readUInt16() {
return read<uint16>();
}
uint32 BinaryFileStreamReader::readUInt32() {
return read<uint32>();
}
uint64 BinaryFileStreamReader::readUInt64() {
return read<uint64>();
}
float BinaryFileStreamReader::readFloat() {
return read<float>();
}
double BinaryFileStreamReader::readDouble() {
return read<double>();
}
std::string BinaryFileStreamReader::readString(size_t length) {
if (length == 0 || eof() || !isOpen()) {
lastReadCount_ = 0;
return {};
}
size_t bytesToRead = std::min(length, remaining());
std::string result(bytesToRead, '\0');
size_t bytesRead = read(result.data(), bytesToRead);
result.resize(bytesRead);
return result;
}
std::string BinaryFileStreamReader::readNullTerminatedString(size_t maxLength) {
if (maxLength == 0 || eof() || !isOpen()) {
lastReadCount_ = 0;
return {};
}
std::string result;
result.reserve(std::min(maxLength, remaining()));
size_t bytesRead = 0;
char ch = '\0';
while (bytesRead < maxLength && position_ < size_) {
stream_.clear();
if (!stream_.get(ch)) {
break;
}
++position_;
++bytesRead;
if (ch == '\0') {
lastReadCount_ = bytesRead;
return result;
}
result.push_back(ch);
}
lastReadCount_ = bytesRead;
return result;
}
} // namespace frostbite2D
@@ -0,0 +1,235 @@
#include <frostbite2D/resource/binary_reader.h>
#include <frostbite2D/resource/asset.h>
#include <fstream>
#include <SDL.h>
namespace frostbite2D {
BinaryReader::BinaryReader(const std::string& filePath) {
open(filePath);
}
BinaryReader::BinaryReader(const char* data, size_t size) {
loadFromMemory(data, size);
}
bool BinaryReader::open(const std::string& filePath) {
close();
Asset& asset = Asset::get();
auto dataOpt = asset.readFileToBytes(filePath);
if (!dataOpt.has_value()) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "BinaryReader: 无法打开文件: %s", filePath.c_str());
return false;
}
const std::vector<uint8>& bytes = dataOpt.value();
data_.resize(bytes.size());
std::memcpy(data_.data(), bytes.data(), bytes.size());
position_ = 0;
lastReadCount_ = 0;
return true;
}
void BinaryReader::loadFromMemory(const char* data, size_t size) {
close();
if (data && size > 0) {
data_.resize(size);
std::memcpy(data_.data(), data, size);
}
position_ = 0;
lastReadCount_ = 0;
}
void BinaryReader::close() {
data_.clear();
position_ = 0;
lastReadCount_ = 0;
}
bool BinaryReader::isOpen() const {
return !data_.empty();
}
size_t BinaryReader::tell() const {
return position_;
}
void BinaryReader::seek(size_t pos) {
position_ = std::clamp(pos, static_cast<size_t>(0), data_.size());
}
void BinaryReader::skip(size_t count) {
seek(position_ + count);
}
bool BinaryReader::eof() const {
return position_ >= data_.size();
}
size_t BinaryReader::size() const {
return data_.size();
}
size_t BinaryReader::remaining() const {
if (position_ >= data_.size()) {
return 0;
}
return data_.size() - position_;
}
size_t BinaryReader::lastReadCount() const {
return lastReadCount_;
}
size_t BinaryReader::read(char* buffer, size_t size) {
if (!buffer || size == 0 || eof()) {
lastReadCount_ = 0;
return 0;
}
size_t bytesToRead = std::min(size, remaining());
std::memcpy(buffer, data_.data() + position_, bytesToRead);
position_ += bytesToRead;
lastReadCount_ = bytesToRead;
if (bytesToRead != size) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "BinaryReader: 读取数据不完整,期望 %zu 字节,实际读取 %zu 字节", size, bytesToRead);
}
return bytesToRead;
}
std::vector<uint8> BinaryReader::readBytes(size_t size) {
std::vector<uint8> result;
size_t bytesToRead = std::min(size, remaining());
if (bytesToRead > 0) {
result.resize(bytesToRead);
read(reinterpret_cast<char*>(result.data()), bytesToRead);
}
return result;
}
const char* BinaryReader::data() const {
return data_.data();
}
const char* BinaryReader::currentData() const {
if (eof()) {
return nullptr;
}
return data_.data() + position_;
}
int8 BinaryReader::readInt8() {
return read<int8>();
}
int16 BinaryReader::readInt16() {
return read<int16>();
}
int32 BinaryReader::readInt32() {
return read<int32>();
}
int64 BinaryReader::readInt64() {
return read<int64>();
}
uint8 BinaryReader::readUInt8() {
return read<uint8>();
}
uint16 BinaryReader::readUInt16() {
return read<uint16>();
}
uint32 BinaryReader::readUInt32() {
return read<uint32>();
}
uint64 BinaryReader::readUInt64() {
return read<uint64>();
}
float BinaryReader::readFloat() {
return read<float>();
}
double BinaryReader::readDouble() {
return read<double>();
}
std::string BinaryReader::readString(size_t length) {
if (length == 0 || eof()) {
return "";
}
size_t bytesToRead = std::min(length, remaining());
std::string result(data_.data() + position_, bytesToRead);
position_ += bytesToRead;
lastReadCount_ = bytesToRead;
if (bytesToRead != length) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "BinaryReader: 读取字符串不完整,期望 %zu 字节,实际读取 %zu 字节", length, bytesToRead);
}
return result;
}
std::string BinaryReader::readNullTerminatedString() {
if (eof()) {
return "";
}
size_t startPos = position_;
while (position_ < data_.size() && data_[position_] != '\0') {
++position_;
}
size_t length = position_ - startPos;
std::string result(data_.data() + startPos, length);
if (position_ < data_.size()) {
++position_;
}
lastReadCount_ = length + 1;
return result;
}
void BinaryReader::crcDecode(size_t length, uint32 crc32) {
if (length == 0 || eof()) {
return;
}
const uint32 key = 0x81A79011;
size_t originalPos = position_;
size_t bytesToProcess = std::min(length, remaining());
for (size_t i = 0; i < bytesToProcess; i += 4) {
size_t pos = position_;
uint32 value = read<uint32>();
uint32 decoded = (value ^ key ^ crc32);
decoded = (decoded >> 6) | ((decoded << (32 - 6)) & 0xFFFFFFFF);
if (pos + 3 < data_.size()) {
data_[pos] = static_cast<char>((decoded >> 0) & 0xFF);
data_[pos + 1] = static_cast<char>((decoded >> 8) & 0xFF);
data_[pos + 2] = static_cast<char>((decoded >> 16) & 0xFF);
data_[pos + 3] = static_cast<char>((decoded >> 24) & 0xFF);
}
}
seek(originalPos + bytesToProcess);
}
} // namespace frostbite2D
@@ -0,0 +1,881 @@
#include <frostbite2D/resource/npk_archive.h>
#include <frostbite2D/resource/binary_file_stream_reader.h>
#include <algorithm>
#include <cctype>
#include <chrono>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <system_error>
#include <SDL.h>
#include <frostbite2D/utils/startup_trace.h>
namespace frostbite2D {
namespace {
namespace fs = std::filesystem;
constexpr char kIndexCacheFileName[] = ".frostbite_npk_index_cache.bin";
constexpr char kIndexCacheMagic[8] = {'F', 'B', 'N', 'P', 'K', 'I', 'D', 'X'};
constexpr uint32 kIndexCacheVersion = 2;
constexpr uint32 kMaxCacheStringLength = 4096;
constexpr uint32 kMaxCacheSourceFileCount = 20000;
constexpr uint32 kMaxCacheImageCount = 20000000;
fs::path toFsPath(const std::string& path) {
#ifdef _WIN32
return fs::u8path(path);
#else
return fs::path(path);
#endif
}
int64 toCacheTimestamp(const fs::file_time_type& value) {
auto duration =
std::chrono::duration_cast<std::chrono::nanoseconds>(
value.time_since_epoch())
.count();
return static_cast<int64>(duration);
}
bool appendBytes(std::vector<uint8>& buffer, const void* data, size_t size) {
if (size == 0) {
return true;
}
if (buffer.size() > buffer.max_size() - size) {
return false;
}
size_t oldSize = buffer.size();
buffer.resize(oldSize + size);
std::memcpy(buffer.data() + oldSize, data, size);
return true;
}
template <typename T>
bool appendPod(std::vector<uint8>& buffer, const T& value) {
return appendBytes(buffer, &value, sizeof(T));
}
template <typename T>
bool readPod(std::ifstream& stream, T& value) {
stream.read(reinterpret_cast<char*>(&value), sizeof(T));
return stream.good();
}
bool appendString(std::vector<uint8>& buffer, const std::string& value) {
if (value.size() > kMaxCacheStringLength) {
return false;
}
uint32 length = static_cast<uint32>(value.size());
if (!appendPod(buffer, length)) {
return false;
}
if (length == 0) {
return true;
}
return appendBytes(buffer, value.data(), length);
}
bool readString(std::ifstream& stream, std::string& value) {
uint32 length = 0;
if (!readPod(stream, length)) {
return false;
}
if (length > kMaxCacheStringLength) {
return false;
}
value.clear();
if (length == 0) {
return true;
}
value.resize(length);
stream.read(value.data(), length);
return stream.good();
}
const char* getZlibErrorName(int code) {
switch (code) {
case Z_OK:
return "Z_OK";
case Z_STREAM_END:
return "Z_STREAM_END";
case Z_NEED_DICT:
return "Z_NEED_DICT";
case Z_ERRNO:
return "Z_ERRNO";
case Z_STREAM_ERROR:
return "Z_STREAM_ERROR";
case Z_DATA_ERROR:
return "Z_DATA_ERROR";
case Z_MEM_ERROR:
return "Z_MEM_ERROR";
case Z_BUF_ERROR:
return "Z_BUF_ERROR";
case Z_VERSION_ERROR:
return "Z_VERSION_ERROR";
default:
return "UNKNOWN_ZLIB_ERROR";
}
}
} // namespace
const uint8 NpkArchive::NPK_KEY[256] = {
112, 117, 99, 104, 105, 107, 111, 110, 64, 110, 101, 111, 112, 108, 101, 32,
100, 117, 110, 103, 101, 111, 110, 32, 97, 110, 100, 32, 102, 105, 103, 104,
116, 101, 114, 32, 68, 78, 70, 68, 78, 70, 68, 78, 70, 68, 78, 70, 68, 78,
70, 68, 78, 70, 68, 78, 70, 68, 78, 70, 68, 78, 70, 68, 78, 70, 68, 78, 70,
68, 78, 70, 68, 78, 70, 68, 78, 70, 68, 78, 70, 68, 78, 70, 68, 78, 70, 68,
78, 70, 68, 78, 70, 68, 78, 70, 68, 78, 70, 68, 78, 70, 68, 78, 70, 68, 78,
70, 68, 78, 70, 68, 78, 70, 68, 78, 70, 68, 78, 70, 68, 78, 70, 68, 78, 70,
68, 78, 70, 68, 78, 70, 68, 78, 70, 68, 78, 70, 68, 78, 70, 68, 78, 70, 68,
78, 70, 68, 78, 70, 68, 78, 70, 68, 78, 70, 68, 78, 70, 68, 78, 70, 68, 78,
70, 68, 78, 70, 68, 78, 70, 68, 78, 70, 68, 78, 70, 68, 78, 70, 68, 78, 70,
68, 78, 70, 68, 78, 70, 68, 78, 70, 68, 78, 70, 0
};
NpkArchive& NpkArchive::get() {
static NpkArchive instance;
return instance;
}
void NpkArchive::setImagePackDirectory(const std::string& dir) {
imagePackDirectory_ = dir;
}
const std::string& NpkArchive::getImagePackDirectory() const {
return imagePackDirectory_;
}
void NpkArchive::init() {
if (initialized_) {
close();
}
scanNpkFiles();
initialized_ = true;
}
void NpkArchive::close() {
imgIndex_.clear();
imageCache_.clear();
lruList_.clear();
lruLookup_.clear();
currentCacheSize_ = 0;
initialized_ = false;
}
bool NpkArchive::isOpen() const {
return initialized_;
}
std::string NpkArchive::normalizePath(const std::string& path) const {
std::string result = path;
std::transform(result.begin(), result.end(), result.begin(),
[](unsigned char c) { return std::tolower(c); });
return result;
}
std::string NpkArchive::getCacheFilePath() const {
Asset& asset = Asset::get();
std::string relativePath =
asset.combinePath(imagePackDirectory_, kIndexCacheFileName);
return asset.resolvePath(relativePath);
}
NpkArchive::IndexCacheValidationMode NpkArchive::getIndexCacheValidationMode()
const {
#ifdef __SWITCH__
return IndexCacheValidationMode::TrustCache;
#else
return IndexCacheValidationMode::StrictSourceState;
#endif
}
std::vector<NpkArchive::SourceFileState> NpkArchive::collectSourceFileStates()
const {
Asset& asset = Asset::get();
std::string npkDir = asset.resolvePath(imagePackDirectory_);
std::vector<NpkArchive::SourceFileState> result;
if (!asset.isDirectory(npkDir)) {
return result;
}
std::vector<std::string> files = asset.listFilesWithExtension(npkDir, ".npk");
result.reserve(files.size());
for (const auto& file : files) {
std::error_code error;
fs::path path = toFsPath(file);
if (!fs::is_regular_file(path, error)) {
continue;
}
SourceFileState state;
state.fileName = asset.getFileName(file);
state.size = static_cast<uint64>(fs::file_size(path, error));
if (error) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"NpkArchive: failed to read file size for %s",
file.c_str());
state.size = asset.getFileSize(file);
}
error.clear();
fs::file_time_type lastWriteTime = fs::last_write_time(path, error);
if (error) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"NpkArchive: failed to read write time for %s",
file.c_str());
state.writeTime = 0;
} else {
state.writeTime = toCacheTimestamp(lastWriteTime);
}
result.push_back(std::move(state));
}
std::sort(result.begin(), result.end(),
[](const SourceFileState& left, const SourceFileState& right) {
return left.fileName < right.fileName;
});
return result;
}
bool NpkArchive::loadIndexCache(
const std::vector<SourceFileState>& sourceFiles) {
Asset& asset = Asset::get();
std::string cachePath = getCacheFilePath();
if (!asset.isRegularFile(cachePath)) {
return false;
}
std::ifstream stream(toFsPath(cachePath), std::ios::binary);
if (!stream.is_open()) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"NpkArchive: failed to open cache file %s",
cachePath.c_str());
return false;
}
IndexCacheValidationMode validationMode = getIndexCacheValidationMode();
uint32 version = 0;
uint32 cachedValidationMode = 0;
uint32 cachedSourceCount = 0;
uint32 cachedImageCount = 0;
{
ScopedStartupTrace stageTrace("NpkArchive cache header");
char magic[sizeof(kIndexCacheMagic)] = {};
stream.read(magic, sizeof(magic));
if (!stream.good() ||
std::memcmp(magic, kIndexCacheMagic, sizeof(kIndexCacheMagic)) != 0) {
SDL_Log("NpkArchive: cache magic mismatch, will rescan");
return false;
}
if (!readPod(stream, version) || !readPod(stream, cachedValidationMode) ||
!readPod(stream, cachedSourceCount) || !readPod(stream, cachedImageCount)) {
SDL_Log("NpkArchive: cache header incomplete, will rescan");
return false;
}
if (version != kIndexCacheVersion) {
SDL_Log("NpkArchive: cache version mismatch (%u != %u), will rescan",
version, kIndexCacheVersion);
return false;
}
if (cachedSourceCount > kMaxCacheSourceFileCount ||
cachedImageCount > kMaxCacheImageCount) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"NpkArchive: cache counts look invalid (sources=%u, images=%u)",
cachedSourceCount, cachedImageCount);
return false;
}
if (cachedValidationMode != static_cast<uint32>(validationMode)) {
SDL_Log("NpkArchive: cache validation mode mismatch (%u != %u), will rescan",
cachedValidationMode, static_cast<uint32>(validationMode));
return false;
}
}
if (validationMode == IndexCacheValidationMode::StrictSourceState) {
if (sourceFiles.empty()) {
SDL_Log("NpkArchive: strict cache validation requires source metadata, will rescan");
return false;
}
std::vector<SourceFileState> cachedSourceFiles;
{
ScopedStartupTrace stageTrace("NpkArchive cache sources");
cachedSourceFiles.reserve(cachedSourceCount);
for (uint32 i = 0; i < cachedSourceCount; ++i) {
SourceFileState state;
if (!readString(stream, state.fileName) || !readPod(stream, state.size) ||
!readPod(stream, state.writeTime)) {
SDL_Log("NpkArchive: cache source file section incomplete, will rescan");
return false;
}
cachedSourceFiles.push_back(std::move(state));
}
}
if (cachedSourceFiles != sourceFiles) {
SDL_Log("NpkArchive: cache invalidated by directory changes, will rescan");
return false;
}
} else if (cachedSourceCount != 0) {
SDL_Log("NpkArchive: trusted cache should not contain source metadata, will rescan");
return false;
}
std::unordered_map<std::string, ImgRef> cachedIndex;
{
ScopedStartupTrace stageTrace("NpkArchive cache images");
cachedIndex.reserve(cachedImageCount);
for (uint32 i = 0; i < cachedImageCount; ++i) {
std::string path;
std::string npkFile;
ImgRef img;
if (!readString(stream, path) || !readString(stream, npkFile) ||
!readPod(stream, img.frameCount) || !readPod(stream, img.offset) ||
!readPod(stream, img.size)) {
SDL_Log("NpkArchive: cache image section incomplete, will rescan");
return false;
}
img.path = path;
img.npkFile = std::move(npkFile);
img.loaded = false;
cachedIndex.emplace(std::move(path), std::move(img));
}
}
if (!stream.good() && !stream.eof()) {
SDL_Log("NpkArchive: cache read failed, will rescan");
return false;
}
{
ScopedStartupTrace stageTrace("NpkArchive cache commit");
imgIndex_.swap(cachedIndex);
}
SDL_Log("NpkArchive: loaded %zu image refs from cache %s", imgIndex_.size(),
cachePath.c_str());
return true;
}
bool NpkArchive::saveIndexCache(
const std::vector<SourceFileState>& sourceFiles) const {
std::string cachePath = getCacheFilePath();
IndexCacheValidationMode validationMode = getIndexCacheValidationMode();
uint32 sourceCount =
validationMode == IndexCacheValidationMode::StrictSourceState
? static_cast<uint32>(sourceFiles.size())
: 0;
uint32 imageCount = static_cast<uint32>(imgIndex_.size());
std::vector<uint8> buffer;
{
ScopedStartupTrace stageTrace("NpkArchive cache serialize");
size_t estimatedSize = sizeof(kIndexCacheMagic) + sizeof(uint32) * 4;
if (validationMode == IndexCacheValidationMode::StrictSourceState) {
for (const auto& sourceFile : sourceFiles) {
estimatedSize += sizeof(uint32) + sourceFile.fileName.size();
estimatedSize += sizeof(sourceFile.size);
estimatedSize += sizeof(sourceFile.writeTime);
}
}
for (const auto& [path, img] : imgIndex_) {
estimatedSize += sizeof(uint32) + path.size();
estimatedSize += sizeof(uint32) + img.npkFile.size();
estimatedSize += sizeof(img.frameCount);
estimatedSize += sizeof(img.offset);
estimatedSize += sizeof(img.size);
}
buffer.reserve(estimatedSize);
if (!appendBytes(buffer, kIndexCacheMagic, sizeof(kIndexCacheMagic)) ||
!appendPod(buffer, kIndexCacheVersion) ||
!appendPod(buffer, static_cast<uint32>(validationMode)) ||
!appendPod(buffer, sourceCount) ||
!appendPod(buffer, imageCount)) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"NpkArchive: failed to serialize cache header %s",
cachePath.c_str());
return false;
}
if (validationMode == IndexCacheValidationMode::StrictSourceState) {
for (const auto& sourceFile : sourceFiles) {
if (!appendString(buffer, sourceFile.fileName) ||
!appendPod(buffer, sourceFile.size) ||
!appendPod(buffer, sourceFile.writeTime)) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"NpkArchive: failed to serialize cache source data %s",
cachePath.c_str());
return false;
}
}
}
for (const auto& [path, img] : imgIndex_) {
if (!appendString(buffer, path) || !appendString(buffer, img.npkFile) ||
!appendPod(buffer, img.frameCount) || !appendPod(buffer, img.offset) ||
!appendPod(buffer, img.size)) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"NpkArchive: failed to serialize cache image data %s",
cachePath.c_str());
return false;
}
}
}
{
ScopedStartupTrace stageTrace("NpkArchive cache flush");
std::ofstream stream(toFsPath(cachePath),
std::ios::binary | std::ios::trunc);
if (!stream.is_open()) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"NpkArchive: failed to create cache file %s",
cachePath.c_str());
return false;
}
if (!buffer.empty()) {
stream.write(reinterpret_cast<const char*>(buffer.data()), buffer.size());
}
if (!stream.good()) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"NpkArchive: cache flush failed %s", cachePath.c_str());
return false;
}
}
SDL_Log("NpkArchive: wrote index cache %s (mode=%u, %u source files, %u images)",
cachePath.c_str(), static_cast<uint32>(validationMode), sourceCount,
imageCount);
return true;
}
void NpkArchive::scanNpkFiles() {
Asset& asset = Asset::get();
std::string npkDir = asset.resolvePath(imagePackDirectory_);
if (!asset.isDirectory(npkDir)) {
SDL_Log("NPK directory not found: %s", npkDir.c_str());
return;
}
imgIndex_.clear();
IndexCacheValidationMode validationMode = getIndexCacheValidationMode();
std::vector<SourceFileState> sourceFiles;
if (validationMode == IndexCacheValidationMode::StrictSourceState) {
{
ScopedStartupTrace stageTrace("NpkArchive collect source states");
sourceFiles = collectSourceFileStates();
}
if (sourceFiles.empty()) {
SDL_Log("NpkArchive: no NPK files found in %s", npkDir.c_str());
return;
}
{
ScopedStartupTrace stageTrace("NpkArchive cache strict-load");
if (loadIndexCache(sourceFiles)) {
return;
}
}
} else {
{
ScopedStartupTrace stageTrace("NpkArchive cache fast-load");
if (loadIndexCache(sourceFiles)) {
return;
}
}
}
std::vector<std::string> files = asset.listFilesWithExtension(npkDir, ".npk");
std::sort(files.begin(), files.end());
if (files.empty()) {
SDL_Log("NpkArchive: no NPK files found in %s", npkDir.c_str());
return;
}
SDL_Log("Scanning %d NPK files...", static_cast<int>(files.size()));
{
ScopedStartupTrace stageTrace("NpkArchive parse files");
for (const auto& file : files) {
parseNpkFile(file);
}
}
{
ScopedStartupTrace stageTrace("NpkArchive cache save");
saveIndexCache(sourceFiles);
}
}
bool NpkArchive::parseNpkFile(const std::string& npkPath) {
Asset& asset = Asset::get();
BinaryFileStreamReader reader(npkPath);
if (!reader.isOpen()) {
SDL_Log("Failed to open NPK file: %s", npkPath.c_str());
return false;
}
std::string npkFileName = asset.getFileName(npkPath);
std::string header = reader.readNullTerminatedString();
if (header.find("NeoplePack_Bill") == std::string::npos) {
return false;
}
int32 imageCount = reader.readInt32();
for (int32 i = 0; i < imageCount; ++i) {
int32 offset = reader.readInt32();
int32 length = reader.readInt32();
std::string imgPath = readNpkInfoString(reader);
ImgRef img;
img.path = normalizePath(imgPath);
img.npkFile = npkFileName;
img.offset = static_cast<uint32>(offset);
img.size = static_cast<uint32>(length);
img.loaded = false;
imgIndex_[img.path] = img;
}
return true;
}
bool NpkArchive::hasImg(const std::string& path) const {
return imgIndex_.find(normalizePath(path)) != imgIndex_.end();
}
std::optional<ImgRef> NpkArchive::getImg(const std::string& path) {
auto it = imgIndex_.find(normalizePath(path));
if (it == imgIndex_.end()) {
return std::nullopt;
}
return it->second;
}
std::vector<std::string> NpkArchive::listImgs() const {
std::vector<std::string> result;
result.reserve(imgIndex_.size());
for (const auto& pair : imgIndex_) {
result.push_back(pair.first);
}
std::sort(result.begin(), result.end());
return result;
}
std::optional<ImageFrame> NpkArchive::getImageFrame(const ImgRef& img, size_t index) {
std::string imgPath = normalizePath(img.path);
auto cacheIt = imageCache_.find(imgPath);
if (cacheIt == imageCache_.end()) {
auto it = imgIndex_.find(imgPath);
if (it == imgIndex_.end()) {
return std::nullopt;
}
if (!loadImgData(it->second)) {
return std::nullopt;
}
cacheIt = imageCache_.find(imgPath);
if (cacheIt == imageCache_.end()) {
return std::nullopt;
}
}
updateCacheUsage(imgPath);
if (index >= cacheIt->second.frames.size()) {
return std::nullopt;
}
return cacheIt->second.frames[index];
}
size_t NpkArchive::getFrameCount(const ImgRef& img) const {
std::string imgPath = normalizePath(img.path);
auto it = imgIndex_.find(imgPath);
if (it == imgIndex_.end()) {
return 0;
}
return it->second.frameCount;
}
bool NpkArchive::loadImgData(ImgRef& img) {
Asset& asset = Asset::get();
std::string npkPath = asset.combinePath(imagePackDirectory_, img.npkFile);
npkPath = asset.resolvePath(npkPath);
BinaryFileStreamReader reader(npkPath);
if (!reader.isOpen()) {
SDL_Log("Failed to open NPK for IMG: %s", npkPath.c_str());
return false;
}
reader.seek(img.offset);
std::string flag = reader.readNullTerminatedString();
if (flag.find("Neople Img File") == std::string::npos) {
return false;
}
int64 tableLength = reader.readInt64();
reader.readInt32();
int32 indexCount = reader.readInt32();
img.frameCount = static_cast<size_t>(indexCount);
CachedImageData cachedData;
cachedData.frames.resize(indexCount);
size_t dataStartPos = img.offset + static_cast<uint32>(tableLength) + 32;
for (int32 i = 0; i < indexCount; ++i) {
ImageFrame& frame = cachedData.frames[i];
frame.type = reader.readInt32();
if (frame.type == 17) {
int32 refIndex = reader.readInt32();
frame.compressionType = refIndex;
frame.width = cachedData.frames[refIndex].width;
frame.height = cachedData.frames[refIndex].height;
frame.xPos = cachedData.frames[refIndex].xPos;
frame.yPos = cachedData.frames[refIndex].yPos;
frame.frameXPos = cachedData.frames[refIndex].frameXPos;
frame.frameYPos = cachedData.frames[refIndex].frameYPos;
frame.offset = cachedData.frames[refIndex].offset;
frame.size = cachedData.frames[refIndex].size;
continue;
}
frame.compressionType = reader.readInt32();
frame.width = reader.readInt32();
frame.height = reader.readInt32();
int32 size = reader.readInt32();
frame.xPos = reader.readInt32();
frame.yPos = reader.readInt32();
frame.frameXPos = reader.readInt32();
frame.frameYPos = reader.readInt32();
frame.size = size;
if (i == 0) {
frame.offset = static_cast<uint32>(dataStartPos);
} else {
frame.offset = cachedData.frames[i - 1].offset + cachedData.frames[i - 1].size;
}
}
for (int32 i = 0; i < indexCount; ++i) {
ImageFrame& frame = cachedData.frames[i];
if (frame.type == 17) {
int32 refIndex = frame.compressionType;
frame.data = cachedData.frames[refIndex].data;
continue;
}
reader.seek(frame.offset);
std::vector<uint8> compressedData = reader.readBytes(static_cast<size_t>(frame.size));
int32 deSize = frame.width * frame.height * 4;
std::vector<uint8> decompressedData(deSize);
unsigned long realSize = static_cast<unsigned long>(deSize);
int uncompressResult = uncompress(
decompressedData.data(),
&realSize,
compressedData.data(),
static_cast<unsigned long>(frame.size)
);
if (uncompressResult != Z_OK) {
if (frame.type == 16 && frame.compressionType == 5 &&
frame.size == static_cast<uint32>(deSize)) {
frame.data = std::move(compressedData);
cachedData.memoryUsage += frame.data.size();
if (verboseFallbackLog_) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"NpkArchive: fallback to raw RGBA frame, img=%s, frame=%d, "
"type=%d, compression=%d, size=%dx%d",
img.path.c_str(), i, frame.type, frame.compressionType,
frame.width, frame.height);
}
continue;
}
if (verboseFallbackLog_) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to uncompress image data: %d (%s), img=%s, "
"npk=%s, frame=%d, "
"type=%d, compression=%d, size=%dx%d, offset=%u, "
"packed=%u, expected=%d",
uncompressResult, getZlibErrorName(uncompressResult),
img.path.c_str(), img.npkFile.c_str(), i, frame.type,
frame.compressionType, frame.width, frame.height,
frame.offset, frame.size, deSize);
}
continue;
}
if (frame.type != 16) {
int32 pngByteSize = deSize * 2;
frame.data.resize(pngByteSize);
for (int32 e = 0; e < pngByteSize; e += 4) {
uint8 needData[2] = {0, 0};
if ((e / 4) * 2 + 1 < static_cast<int32>(decompressedData.size())) {
needData[0] = decompressedData[(e / 4) * 2];
needData[1] = decompressedData[(e / 4) * 2 + 1];
}
parseColor(needData, frame.type, frame.data.data(), e);
}
} else {
frame.data = std::move(decompressedData);
}
cachedData.memoryUsage += frame.data.size();
}
auto existingCacheIt = imageCache_.find(img.path);
if (existingCacheIt != imageCache_.end()) {
currentCacheSize_ -= existingCacheIt->second.memoryUsage;
imageCache_.erase(existingCacheIt);
}
evictCacheIfNeeded(cachedData.memoryUsage);
auto cacheIt = imageCache_.emplace(img.path, std::move(cachedData)).first;
currentCacheSize_ += cacheIt->second.memoryUsage;
updateCacheUsage(img.path);
img.loaded = true;
imgIndex_[img.path] = img;
return true;
}
void NpkArchive::parseColor(const uint8* tab, int type, uint8* saveByte, int offset) {
uint8 a = 0, r = 0, g = 0, b = 0;
switch (type) {
case 0x0e:
a = static_cast<uint8>(tab[1] >> 7);
r = static_cast<uint8>((tab[1] >> 2) & 0x1f);
g = static_cast<uint8>((tab[0] >> 5) | ((tab[1] & 3) << 3));
b = static_cast<uint8>(tab[0] & 0x1f);
a = static_cast<uint8>(a * 0xff);
r = static_cast<uint8>((r << 3) | (r >> 2));
g = static_cast<uint8>((g << 3) | (g >> 2));
b = static_cast<uint8>((b << 3) | (b >> 2));
break;
case 0x0f:
a = static_cast<uint8>(tab[1] & 0xf0);
r = static_cast<uint8>((tab[1] & 0xf) << 4);
g = static_cast<uint8>(tab[0] & 0xf0);
b = static_cast<uint8>((tab[0] & 0xf) << 4);
break;
}
saveByte[offset + 0] = b;
saveByte[offset + 1] = g;
saveByte[offset + 2] = r;
saveByte[offset + 3] = a;
}
void NpkArchive::setCacheSize(size_t maxBytes) {
maxCacheSize_ = maxBytes;
evictCacheIfNeeded(0);
}
void NpkArchive::clearCache() {
imageCache_.clear();
lruList_.clear();
lruLookup_.clear();
currentCacheSize_ = 0;
}
size_t NpkArchive::getCacheUsage() const {
return currentCacheSize_;
}
void NpkArchive::evictCacheIfNeeded(size_t requiredSize) {
while (currentCacheSize_ + requiredSize > maxCacheSize_ && !lruList_.empty()) {
std::string oldest = lruList_.back();
lruList_.pop_back();
lruLookup_.erase(oldest);
auto it = imageCache_.find(oldest);
if (it != imageCache_.end()) {
currentCacheSize_ -= it->second.memoryUsage;
imageCache_.erase(it);
}
}
}
void NpkArchive::updateCacheUsage(const std::string& imgPath) {
auto lruIt = lruLookup_.find(imgPath);
if (lruIt != lruLookup_.end()) {
lruList_.erase(lruIt->second);
}
lruList_.push_front(imgPath);
lruLookup_[imgPath] = lruList_.begin();
auto cacheIt = imageCache_.find(imgPath);
if (cacheIt != imageCache_.end()) {
cacheIt->second.lastUseTime = SDL_GetTicks();
}
}
std::string NpkArchive::readNpkInfoString(BinaryFileStreamReader& reader) {
if (reader.eof()) {
return "";
}
std::vector<uint8> encrypted = reader.readBytes(256);
if (encrypted.size() < 256) {
return "";
}
std::vector<char> decrypted(256);
for (int i = 0; i < 256; ++i) {
decrypted[i] = static_cast<char>(encrypted[i] ^ NPK_KEY[i]);
}
return std::string(decrypted.data());
}
void NpkArchive::setDefaultImg(const std::string& imgPath, size_t frameIndex) {
defaultImgPath_ = normalizePath(imgPath);
defaultImgFrame_ = frameIndex;
}
const std::string& NpkArchive::getDefaultImgPath() const {
return defaultImgPath_;
}
size_t NpkArchive::getDefaultImgFrame() const {
return defaultImgFrame_;
}
}
@@ -0,0 +1,632 @@
#include <frostbite2D/resource/pvf_archive.h>
#include <SDL.h>
#include <algorithm>
#include <cctype>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <sstream>
#include <frostbite2D/utils/startup_trace.h>
namespace frostbite2D {
namespace {
bool canReadSpan(const std::vector<uint8>& bytes, size_t offset, size_t length) {
return offset <= bytes.size() && length <= bytes.size() - offset;
}
template <typename T>
bool readPodAt(const std::vector<uint8>& bytes, size_t offset, T& value) {
if (!canReadSpan(bytes, offset, sizeof(T))) {
value = T{};
return false;
}
std::memcpy(&value, bytes.data() + offset, sizeof(T));
return true;
}
std::string readStringAt(const std::vector<uint8>& bytes, size_t offset,
size_t length) {
if (!canReadSpan(bytes, offset, length)) {
return {};
}
return std::string(reinterpret_cast<const char*>(bytes.data() + offset),
length);
}
} // namespace
PvfArchive& PvfArchive::get() {
static PvfArchive instance;
return instance;
}
bool PvfArchive::open(const std::string& filePath) {
close();
ScopedStartupTrace stageTrace("PvfArchive::open stream");
if (!reader_.open(filePath)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"PvfArchive: unable to open file: %s", filePath.c_str());
return false;
}
return true;
}
void PvfArchive::close() {
reader_.close();
clearInitData();
}
bool PvfArchive::isOpen() const {
return reader_.isOpen();
}
void PvfArchive::init() {
if (!isOpen()) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"PvfArchive: open the archive before calling init");
return;
}
clearInitData();
{
ScopedStartupTrace stageTrace("PvfArchive::initHeader");
initHeader();
}
if (fileInfo_.empty()) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"PvfArchive: header parse produced no file entries");
return;
}
{
ScopedStartupTrace stageTrace("PvfArchive::initBinStringTable");
initBinStringTable();
}
{
ScopedStartupTrace stageTrace("PvfArchive::initLoadStrings");
initLoadStrings();
}
}
void PvfArchive::initHeader() {
fileInfo_.clear();
dataStartPos_ = 0;
decodedFileCache_.clear();
if (!isOpen()) {
return;
}
reader_.seek(0);
int32 uuidLength = reader_.readInt32();
if (uuidLength < 0 || static_cast<size_t>(uuidLength) > reader_.remaining()) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"PvfArchive: invalid uuid length in archive header");
return;
}
std::string uuid = reader_.readString(static_cast<size_t>(uuidLength));
(void)uuid;
int32 version = reader_.readInt32();
(void)version;
int32 alignedIndexHeaderSize = reader_.readInt32();
int32 indexHeaderCrc = reader_.readInt32();
int32 indexSize = reader_.readInt32();
if (alignedIndexHeaderSize <= 0 || indexSize < 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"PvfArchive: invalid index header metadata (size=%d, count=%d)",
alignedIndexHeaderSize, indexSize);
return;
}
size_t indexHeaderPos = reader_.tell();
auto headerBytesOpt =
readArchiveBytes(indexHeaderPos, static_cast<size_t>(alignedIndexHeaderSize));
if (!headerBytesOpt.has_value()) {
return;
}
std::vector<uint8> headerBytes = std::move(headerBytesOpt.value());
crcDecodeBuffer(headerBytes, static_cast<uint32>(indexHeaderCrc));
dataStartPos_ = indexHeaderPos + static_cast<size_t>(alignedIndexHeaderSize);
size_t cursor = 0;
for (int32 i = 0; i < indexSize; ++i) {
int32 fileNumber = 0;
int32 filePathLength = 0;
int32 fileLength = 0;
int32 crc32 = 0;
int32 relativeOffset = 0;
if (!readPodAt(headerBytes, cursor, fileNumber) ||
!readPodAt(headerBytes, cursor + 4, filePathLength)) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"PvfArchive: truncated index header at entry %d", i);
break;
}
(void)fileNumber;
cursor += 8;
if (filePathLength < 0) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"PvfArchive: negative path length in index entry %d", i);
break;
}
size_t pathLength = static_cast<size_t>(filePathLength);
if (!canReadSpan(headerBytes, cursor, pathLength + 12)) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"PvfArchive: truncated file entry payload at index %d", i);
break;
}
std::string fileName =
normalizePath(readStringAt(headerBytes, cursor, pathLength));
cursor += pathLength;
readPodAt(headerBytes, cursor, fileLength);
readPodAt(headerBytes, cursor + 4, crc32);
readPodAt(headerBytes, cursor + 8, relativeOffset);
cursor += 12;
if (fileLength > 0 && relativeOffset >= 0) {
uint32 alignedLength = (static_cast<uint32>(fileLength) + 3u) & 0xFFFFFFFCu;
PvfFileInfo info;
info.offset = static_cast<size_t>(relativeOffset);
info.crc32 = static_cast<uint32>(crc32);
info.length = static_cast<size_t>(alignedLength);
info.decoded = false;
fileInfo_[fileName] = info;
}
}
}
void PvfArchive::initBinStringTable() {
binStringTable_.clear();
auto infoOpt = getFileInfo("stringtable.bin");
if (!infoOpt.has_value()) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"PvfArchive: stringtable.bin was not found");
return;
}
auto bytesOpt = readDecodedFileBytes(infoOpt.value());
if (!bytesOpt.has_value()) {
return;
}
const std::vector<uint8>& bytes = bytesOpt.value();
int32 count = 0;
if (!readPodAt(bytes, 0, count) || count < 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"PvfArchive: invalid stringtable.bin header");
return;
}
for (int32 i = 0; i < count; ++i) {
size_t offsetsPos = static_cast<size_t>(i) * 4 + 4;
int32 startPos = 0;
int32 endPos = 0;
if (!readPodAt(bytes, offsetsPos, startPos) ||
!readPodAt(bytes, offsetsPos + 4, endPos)) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"PvfArchive: truncated string table at entry %d", i);
break;
}
if (startPos < 0 || endPos < startPos) {
continue;
}
size_t stringPos = static_cast<size_t>(startPos) + 4;
size_t length = static_cast<size_t>(endPos - startPos);
if (!canReadSpan(bytes, stringPos, length)) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"PvfArchive: invalid string span at entry %d", i);
continue;
}
binStringTable_[i] = readStringAt(bytes, stringPos, length);
}
}
void PvfArchive::initLoadStrings() {
loadStrings_.clear();
auto infoOpt = getFileInfo("n_string.lst");
if (!infoOpt.has_value()) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"PvfArchive: n_string.lst was not found");
return;
}
auto listBytesOpt = readDecodedFileBytes(infoOpt.value());
if (!listBytesOpt.has_value()) {
return;
}
const std::vector<uint8>& listBytes = listBytesOpt.value();
if (listBytes.size() < sizeof(int16)) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"PvfArchive: n_string.lst is too small");
return;
}
std::map<std::string, std::string> stringFileCache;
size_t cursor = sizeof(int16);
while (cursor + 10 <= listBytes.size()) {
int32 findKey = 0;
if (!readPodAt(listBytes, cursor + 6, findKey)) {
break;
}
auto binStrOpt = getBinString(findKey);
if (!binStrOpt.has_value()) {
cursor += 10;
continue;
}
std::string key = normalizePath(binStrOpt.value());
if (key.empty()) {
cursor += 10;
continue;
}
size_t slashPos = key.find('/');
std::string type =
(slashPos != std::string::npos) ? normalizePath(key.substr(0, slashPos))
: std::string();
std::string content;
auto cacheIt = stringFileCache.find(key);
if (cacheIt != stringFileCache.end()) {
content = cacheIt->second;
} else {
auto fileIt = fileInfo_.find(key);
if (fileIt != fileInfo_.end()) {
auto contentBytesOpt = readDecodedFileBytes(fileIt->second);
if (contentBytesOpt.has_value()) {
const std::vector<uint8>& contentBytes = contentBytesOpt.value();
content.assign(reinterpret_cast<const char*>(contentBytes.data()),
contentBytes.size());
stringFileCache[key] = content;
}
}
}
if (!content.empty()) {
auto& table = loadStrings_[type];
std::vector<std::string> lines = splitString(content, "\n");
for (const auto& line : lines) {
size_t gtPos = line.find('>');
if (gtPos != std::string::npos && gtPos + 1 < line.length()) {
table[line.substr(0, gtPos)] = line.substr(gtPos + 1);
}
}
}
cursor += 10;
}
}
bool PvfArchive::hasFile(const std::string& path) const {
return fileInfo_.find(normalizePath(path)) != fileInfo_.end();
}
std::optional<PvfFileInfo> PvfArchive::getFileInfo(const std::string& path) const {
std::string normalizedPath = normalizePath(path);
auto it = fileInfo_.find(normalizedPath);
if (it != fileInfo_.end()) {
return it->second;
}
return std::nullopt;
}
std::vector<std::string> PvfArchive::listFiles() const {
std::vector<std::string> files;
files.reserve(fileInfo_.size());
for (const auto& pair : fileInfo_) {
files.push_back(pair.first);
}
return files;
}
std::optional<std::string> PvfArchive::getFileContent(const std::string& path) {
std::string normalizedPath = normalizePath(path);
auto it = fileInfo_.find(normalizedPath);
if (it == fileInfo_.end()) {
return std::nullopt;
}
PvfFileInfo& info = it->second;
if (!decodeFile(normalizedPath, info)) {
return std::nullopt;
}
auto cacheIt = decodedFileCache_.find(normalizedPath);
if (cacheIt == decodedFileCache_.end()) {
return std::nullopt;
}
const std::vector<uint8>& bytes = cacheIt->second;
if (bytes.empty()) {
return std::string();
}
return std::string(reinterpret_cast<const char*>(bytes.data()), bytes.size());
}
std::optional<std::vector<uint8>> PvfArchive::getFileBytes(const std::string& path) {
std::string normalizedPath = normalizePath(path);
auto it = fileInfo_.find(normalizedPath);
if (it == fileInfo_.end()) {
return std::nullopt;
}
PvfFileInfo& info = it->second;
if (!decodeFile(normalizedPath, info)) {
return std::nullopt;
}
auto cacheIt = decodedFileCache_.find(normalizedPath);
if (cacheIt == decodedFileCache_.end()) {
return std::nullopt;
}
return cacheIt->second;
}
std::optional<RawData> PvfArchive::getFileRawData(const std::string& path) {
std::string normalizedPath = normalizePath(path);
auto it = fileInfo_.find(normalizedPath);
if (it == fileInfo_.end()) {
return std::nullopt;
}
PvfFileInfo& info = it->second;
if (!decodeFile(normalizedPath, info)) {
return std::nullopt;
}
auto cacheIt = decodedFileCache_.find(normalizedPath);
if (cacheIt == decodedFileCache_.end()) {
return std::nullopt;
}
const std::vector<uint8>& bytes = cacheIt->second;
RawData result;
result.size = bytes.size();
result.data = std::make_unique<char[]>(bytes.size());
if (!bytes.empty()) {
std::memcpy(result.data.get(), bytes.data(), bytes.size());
}
return result;
}
std::optional<std::string> PvfArchive::getBinString(int key) const {
auto it = binStringTable_.find(key);
if (it != binStringTable_.end()) {
return it->second;
}
return std::nullopt;
}
std::optional<std::string> PvfArchive::getLoadString(const std::string& type,
const std::string& key) const {
std::string normalizedType = normalizePath(type);
auto typeIt = loadStrings_.find(normalizedType);
if (typeIt != loadStrings_.end()) {
auto keyIt = typeIt->second.find(key);
if (keyIt != typeIt->second.end()) {
return keyIt->second;
}
}
return std::nullopt;
}
bool PvfArchive::hasBinString(int key) const {
return binStringTable_.find(key) != binStringTable_.end();
}
bool PvfArchive::hasLoadString(const std::string& type,
const std::string& key) const {
std::string normalizedType = normalizePath(type);
auto typeIt = loadStrings_.find(normalizedType);
if (typeIt != loadStrings_.end()) {
return typeIt->second.find(key) != typeIt->second.end();
}
return false;
}
std::string PvfArchive::normalizePath(const std::string& path) const {
if (path.empty()) {
return {};
}
std::string normalized = path;
std::replace(normalized.begin(), normalized.end(), '\\', '/');
std::transform(normalized.begin(), normalized.end(), normalized.begin(),
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
std::vector<std::string> parts;
std::stringstream stream(normalized);
std::string part;
while (std::getline(stream, part, '/')) {
if (part.empty() || part == ".") {
continue;
}
if (part == "..") {
if (!parts.empty() && parts.back() != "..") {
parts.pop_back();
} else {
parts.push_back(part);
}
continue;
}
parts.push_back(part);
}
std::string result;
for (size_t i = 0; i < parts.size(); ++i) {
if (i > 0) {
result += '/';
}
result += parts[i];
}
return result;
}
std::string PvfArchive::resolvePath(const std::string& baseDir,
const std::string& path) const {
if (path.empty()) {
return {};
}
std::string rawPath = path;
std::replace(rawPath.begin(), rawPath.end(), '\\', '/');
std::transform(rawPath.begin(), rawPath.end(), rawPath.begin(),
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
size_t slashPos = rawPath.find('/');
std::string root =
slashPos == std::string::npos ? rawPath : rawPath.substr(0, slashPos);
static const char* kLogicalRoots[] = {"map", "sprite", "town",
"sound", "audio", "monster",
"character", "ui"};
for (const char* logicalRoot : kLogicalRoots) {
if (root == logicalRoot) {
return normalizePath(rawPath);
}
}
std::string normalizedBaseDir = normalizePath(baseDir);
if (!normalizedBaseDir.empty() && normalizedBaseDir.back() != '/') {
normalizedBaseDir.push_back('/');
}
return normalizePath(normalizedBaseDir + rawPath);
}
std::vector<std::string> PvfArchive::splitString(
const std::string& str, const std::string& delimiter) const {
std::vector<std::string> tokens;
size_t pos = 0;
size_t found;
while ((found = str.find(delimiter, pos)) != std::string::npos) {
tokens.push_back(str.substr(pos, found - pos));
pos = found + delimiter.length();
}
if (pos < str.length()) {
tokens.push_back(str.substr(pos));
}
return tokens;
}
bool PvfArchive::decodeFile(const std::string& normalizedPath,
PvfFileInfo& info) {
auto cacheIt = decodedFileCache_.find(normalizedPath);
if (cacheIt != decodedFileCache_.end()) {
info.decoded = true;
return true;
}
auto bytesOpt = readDecodedFileBytes(info);
if (!bytesOpt.has_value()) {
return false;
}
decodedFileCache_[normalizedPath] = std::move(bytesOpt.value());
info.decoded = true;
return true;
}
void PvfArchive::clearInitData() {
dataStartPos_ = 0;
fileInfo_.clear();
binStringTable_.clear();
loadStrings_.clear();
decodedFileCache_.clear();
}
std::optional<std::vector<uint8>> PvfArchive::readArchiveBytes(
size_t absoluteOffset, size_t length) {
if (!isOpen()) {
return std::nullopt;
}
if (length == 0) {
return std::vector<uint8>();
}
if (absoluteOffset > reader_.size() ||
length > reader_.size() - absoluteOffset) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"PvfArchive: attempted to read beyond archive bounds (offset=%zu, size=%zu, archive=%zu)",
absoluteOffset, length, reader_.size());
return std::nullopt;
}
reader_.seek(absoluteOffset);
std::vector<uint8> bytes = reader_.readBytes(length);
if (bytes.size() != length) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"PvfArchive: short read at offset %zu (expected %zu bytes, got %zu)",
absoluteOffset, length, bytes.size());
return std::nullopt;
}
return bytes;
}
std::optional<std::vector<uint8>> PvfArchive::readDecodedFileBytes(
const PvfFileInfo& info) {
auto bytesOpt = readArchiveBytes(dataStartPos_ + info.offset, info.length);
if (!bytesOpt.has_value()) {
return std::nullopt;
}
std::vector<uint8> bytes = std::move(bytesOpt.value());
crcDecodeBuffer(bytes, info.crc32);
return bytes;
}
void PvfArchive::crcDecodeBuffer(std::vector<uint8>& data, uint32 crc32) {
if (data.empty()) {
return;
}
const uint32 key = 0x81A79011u;
for (size_t i = 0; i + 3 < data.size(); i += 4) {
uint32 value = static_cast<uint32>(data[i]) |
(static_cast<uint32>(data[i + 1]) << 8) |
(static_cast<uint32>(data[i + 2]) << 16) |
(static_cast<uint32>(data[i + 3]) << 24);
uint32 decoded = (value ^ key ^ crc32);
decoded = (decoded >> 6) | ((decoded << (32 - 6)) & 0xFFFFFFFFu);
data[i] = static_cast<uint8>((decoded >> 0) & 0xFFu);
data[i + 1] = static_cast<uint8>((decoded >> 8) & 0xFFu);
data[i + 2] = static_cast<uint8>((decoded >> 16) & 0xFFu);
data[i + 3] = static_cast<uint8>((decoded >> 24) & 0xFFu);
}
}
} // namespace frostbite2D
@@ -0,0 +1,381 @@
#include <frostbite2D/resource/save_system.h>
#include <SDL2/SDL_log.h>
#include <chrono>
#include <cstdio>
#include <ctime>
#include <frostbite2D/resource/asset.h>
namespace frostbite2D {
namespace {
constexpr int32 kDefaultSlotCount = 10;
constexpr int32 kMinSlotCount = 1;
constexpr int32 kCurrentSchemaVersion = 1;
constexpr int32 kCurrentIndexVersion = 1;
} // namespace
SaveSystem& SaveSystem::get() {
static SaveSystem instance;
return instance;
}
bool SaveSystem::init(const SaveSystemConfig& config) {
config_ = config;
if (config_.slotCount < kMinSlotCount) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"SaveSystem invalid slot count %d, fallback to %d",
config_.slotCount, kDefaultSlotCount);
config_.slotCount = kDefaultSlotCount;
}
if (config_.saveRoot.empty()) {
config_.saveRoot = ".save";
}
if (config_.indexFile.empty()) {
config_.indexFile = "index.json";
}
if (config_.slotPrefix.empty()) {
config_.slotPrefix = "slot_";
}
resetSlots();
initialized_ = true;
if (!ensureSaveDirectory()) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SaveSystem failed to create save root: %s",
config_.saveRoot.c_str());
initialized_ = false;
return false;
}
if (!refreshSlotsFromDisk()) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SaveSystem failed to refresh slot metadata");
initialized_ = false;
return false;
}
if (!writeIndexFile()) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SaveSystem failed to write index file");
initialized_ = false;
return false;
}
SDL_Log("SaveSystem initialized at %s with %d slots",
config_.saveRoot.c_str(), config_.slotCount);
return true;
}
bool SaveSystem::saveSlot(int32 slot, const nlohmann::json& payload,
const SaveMeta& meta) {
if (!initialized_) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SaveSystem not initialized before saveSlot");
return false;
}
if (!isValidSlot(slot)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SaveSystem invalid slot: %d",
slot);
return false;
}
Asset& asset = Asset::get();
if (!ensureSaveDirectory()) {
return false;
}
nlohmann::json document;
document["schemaVersion"] = kCurrentSchemaVersion;
document["savedAtUtc"] = makeUtcTimestamp();
document["slot"] = slot;
document["meta"] = toJson(meta);
document["payload"] = payload;
const std::string targetPath = slotPath(slot);
const std::string tempPath = targetPath + ".tmp";
if (!asset.writeTextFile(tempPath, document.dump(2), false)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SaveSystem failed to write temp slot file: %s",
tempPath.c_str());
return false;
}
if (asset.exists(targetPath) && !asset.removeFile(targetPath)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SaveSystem failed to remove old slot file: %s",
targetPath.c_str());
asset.removeFile(tempPath);
return false;
}
bool moved = asset.moveFile(tempPath, targetPath);
if (!moved) {
if (!asset.copyFile(tempPath, targetPath, true)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SaveSystem failed to move temp slot file: %s -> %s",
tempPath.c_str(), targetPath.c_str());
asset.removeFile(tempPath);
return false;
}
asset.removeFile(tempPath);
}
SaveSlotInfo& info = slots_[static_cast<size_t>(slot)];
info.slot = slot;
info.exists = true;
info.updatedAtUtc = document.value("savedAtUtc", "");
info.fileSize = asset.getFileSize(targetPath);
info.schemaVersion = document.value("schemaVersion", kCurrentSchemaVersion);
info.meta = meta;
if (!writeIndexFile()) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SaveSystem failed to update index after saving slot %d", slot);
return false;
}
return true;
}
std::optional<nlohmann::json> SaveSystem::loadSlotPayload(int32 slot) const {
if (!initialized_) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SaveSystem not initialized before loadSlotPayload");
return std::nullopt;
}
if (!isValidSlot(slot)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SaveSystem invalid slot: %d",
slot);
return std::nullopt;
}
nlohmann::json document;
if (!tryReadSlotDocument(slot, document)) {
return std::nullopt;
}
if (!document.contains("payload")) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SaveSystem slot file missing payload field: %d", slot);
return std::nullopt;
}
return document["payload"];
}
std::optional<SaveSlotInfo> SaveSystem::getSlotInfo(int32 slot) const {
if (!initialized_ || !isValidSlot(slot)) {
return std::nullopt;
}
return slots_[static_cast<size_t>(slot)];
}
std::vector<SaveSlotInfo> SaveSystem::listSlots() const {
return slots_;
}
bool SaveSystem::deleteSlot(int32 slot) {
if (!initialized_) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SaveSystem not initialized before deleteSlot");
return false;
}
if (!isValidSlot(slot)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SaveSystem invalid slot: %d",
slot);
return false;
}
Asset& asset = Asset::get();
std::string path = slotPath(slot);
if (asset.exists(path) && !asset.removeFile(path)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SaveSystem failed to delete slot file: %s", path.c_str());
return false;
}
SaveSlotInfo cleared;
cleared.slot = slot;
slots_[static_cast<size_t>(slot)] = cleared;
if (!writeIndexFile()) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SaveSystem failed to update index after deleting slot %d",
slot);
return false;
}
return true;
}
bool SaveSystem::hasSlot(int32 slot) const {
if (!initialized_ || !isValidSlot(slot)) {
return false;
}
return slots_[static_cast<size_t>(slot)].exists;
}
bool SaveSystem::isValidSlot(int32 slot) const {
return slot >= 0 && slot < config_.slotCount;
}
void SaveSystem::resetSlots() {
slots_.clear();
slots_.resize(static_cast<size_t>(config_.slotCount));
for (int32 slot = 0; slot < config_.slotCount; ++slot) {
slots_[static_cast<size_t>(slot)].slot = slot;
}
}
bool SaveSystem::ensureSaveDirectory() const {
Asset& asset = Asset::get();
if (asset.isDirectory(config_.saveRoot)) {
return true;
}
if (asset.createDirectories(config_.saveRoot)) {
return true;
}
return asset.isDirectory(config_.saveRoot);
}
bool SaveSystem::refreshSlotsFromDisk() {
Asset& asset = Asset::get();
for (int32 slot = 0; slot < config_.slotCount; ++slot) {
SaveSlotInfo info;
info.slot = slot;
const std::string path = slotPath(slot);
if (!asset.isRegularFile(path)) {
slots_[static_cast<size_t>(slot)] = info;
continue;
}
info.exists = true;
info.fileSize = asset.getFileSize(path);
nlohmann::json document;
if (tryReadSlotDocument(slot, document)) {
info.updatedAtUtc = document.value("savedAtUtc", "");
info.schemaVersion =
document.value("schemaVersion", kCurrentSchemaVersion);
if (document.contains("meta") && document["meta"].is_object()) {
info.meta = fromJson(document["meta"]);
}
} else {
info.schemaVersion = 0;
}
slots_[static_cast<size_t>(slot)] = info;
}
return true;
}
bool SaveSystem::tryReadSlotDocument(int32 slot, nlohmann::json& outDoc) const {
Asset& asset = Asset::get();
std::string content;
if (!asset.readTextFile(slotPath(slot), content)) {
return false;
}
try {
outDoc = nlohmann::json::parse(content);
if (!outDoc.is_object()) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SaveSystem slot json root is not object: %d", slot);
return false;
}
} catch (const nlohmann::json::exception& e) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SaveSystem failed to parse slot json %d: %s", slot,
e.what());
return false;
}
return true;
}
bool SaveSystem::writeIndexFile() const {
if (!initialized_) {
return false;
}
if (!ensureSaveDirectory()) {
return false;
}
nlohmann::json root;
root["schemaVersion"] = kCurrentIndexVersion;
root["slotCount"] = config_.slotCount;
root["slots"] = nlohmann::json::array();
for (const SaveSlotInfo& info : slots_) {
nlohmann::json slotJson;
slotJson["slot"] = info.slot;
slotJson["exists"] = info.exists;
slotJson["updatedAtUtc"] = info.updatedAtUtc;
slotJson["fileSize"] = info.fileSize;
slotJson["schemaVersion"] = info.schemaVersion;
slotJson["meta"] = toJson(info.meta);
root["slots"].push_back(slotJson);
}
Asset& asset = Asset::get();
return asset.writeTextFile(indexPath(), root.dump(2), false);
}
std::string SaveSystem::indexPath() const {
return config_.saveRoot + "/" + config_.indexFile;
}
std::string SaveSystem::slotPath(int32 slot) const {
return config_.saveRoot + "/" + slotFileName(slot);
}
std::string SaveSystem::slotFileName(int32 slot) const {
char buffer[32] = {};
std::snprintf(buffer, sizeof(buffer), "%s%02d.json", config_.slotPrefix.c_str(),
static_cast<int>(slot));
return buffer;
}
std::string SaveSystem::makeUtcTimestamp() {
using namespace std::chrono;
std::time_t nowTime = system_clock::to_time_t(system_clock::now());
std::tm utc {};
#ifdef _WIN32
gmtime_s(&utc, &nowTime);
#else
gmtime_r(&nowTime, &utc);
#endif
char buffer[32] = {};
std::strftime(buffer, sizeof(buffer), "%Y-%m-%dT%H:%M:%SZ", &utc);
return buffer;
}
nlohmann::json SaveSystem::toJson(const SaveMeta& meta) {
nlohmann::json metaJson;
metaJson["displayName"] = meta.displayName;
metaJson["playTimeSeconds"] = meta.playTimeSeconds;
metaJson["userTag"] = meta.userTag;
return metaJson;
}
SaveMeta SaveSystem::fromJson(const nlohmann::json& json) {
SaveMeta meta;
if (!json.is_object()) {
return meta;
}
meta.displayName = json.value("displayName", "");
meta.playTimeSeconds = json.value("playTimeSeconds", 0.0);
meta.userTag = json.value("userTag", "");
return meta;
}
} // namespace frostbite2D
@@ -0,0 +1,226 @@
#include <frostbite2D/resource/script_parser.h>
#include <frostbite2D/resource/pvf_archive.h>
#include <algorithm>
#include <cctype>
#include <SDL.h>
namespace frostbite2D {
namespace {
std::string resolveScriptString(PvfArchive& archive, const std::string& fileType, int32 key) {
auto binStr = archive.getBinString(key);
if (!binStr || binStr->empty()) {
return "";
}
if (auto loadStr = archive.getLoadString(fileType, *binStr)) {
if (!loadStr->empty()) {
return *loadStr;
}
}
return *binStr;
}
} // namespace
std::string ScriptValue::toString() const {
switch (type) {
case ScriptValueType::Integer:
return std::to_string(intValue);
case ScriptValueType::Float:
return std::to_string(floatValue);
case ScriptValueType::String:
return stringValue;
case ScriptValueType::Empty:
default:
return "";
}
}
ScriptParser::ScriptParser(const RawData& data, const std::string& filePath)
: filePath_(filePath)
, fileType_(extractFileType(filePath)) {
if (data.data && data.size > 0) {
data_.resize(data.size);
std::memcpy(data_.data(), data.data.get(), data.size);
}
if (data_.size() < 7) {
data_.clear();
}
}
ScriptParser::ScriptParser(const std::vector<uint8>& data, const std::string& filePath)
: filePath_(filePath)
, fileType_(extractFileType(filePath)) {
if (data.size() > 0) {
data_.resize(data.size());
std::memcpy(data_.data(), data.data(), data.size());
}
if (data_.size() < 7) {
data_.clear();
}
}
ScriptParser::ScriptParser(const char* data, size_t size, const std::string& filePath)
: filePath_(filePath)
, fileType_(extractFileType(filePath)) {
if (data && size > 0) {
data_.resize(size);
std::memcpy(data_.data(), data, size);
}
if (data_.size() < 7) {
data_.clear();
}
}
std::optional<ScriptValue> ScriptParser::next() {
if (!isValid() || isEnd()) {
return std::nullopt;
}
auto value = parseValueAt(position_);
if (value.has_value()) {
position_ += 5;
}
return value;
}
void ScriptParser::back() {
if (position_ >= 7) {
position_ -= 5;
}
}
bool ScriptParser::isEnd() const {
return !isValid() || position_ >= data_.size() || (data_.size() - position_) < 5;
}
void ScriptParser::reset() {
position_ = 2;
}
std::vector<ScriptValue> ScriptParser::parseAll() {
std::vector<ScriptValue> values;
reset();
while (!isEnd()) {
if (auto value = next()) {
values.push_back(*value);
}
}
return values;
}
size_t ScriptParser::position() const {
return position_;
}
size_t ScriptParser::size() const {
return data_.size();
}
const std::string& ScriptParser::filePath() const {
return filePath_;
}
const std::string& ScriptParser::fileType() const {
return fileType_;
}
bool ScriptParser::isValid() const {
return !data_.empty();
}
std::string ScriptParser::extractFileType(const std::string& filePath) const {
size_t slashPos = filePath.find_first_of("/");
if (slashPos == std::string::npos) {
return "";
}
std::string type = filePath.substr(0, slashPos);
std::transform(type.begin(), type.end(), type.begin(),
[](unsigned char c) { return std::tolower(c); });
return type;
}
uint8 ScriptParser::readByte(size_t offset) const {
if (offset >= data_.size()) {
return 0;
}
return static_cast<uint8>(data_[offset]);
}
int32 ScriptParser::readInt32(size_t offset) const {
if (offset + 4 > data_.size()) {
return 0;
}
int32 value;
std::memcpy(&value, data_.data() + offset, sizeof(int32));
return value;
}
std::optional<ScriptValue> ScriptParser::parseValueAt(size_t offset) const {
if (offset + 5 > data_.size()) {
return std::nullopt;
}
ScriptValue result;
PvfArchive& archive = PvfArchive::get();
uint8 opcode = readByte(offset);
int32 data = readInt32(offset + 1);
switch (static_cast<ScriptOpcode>(opcode)) {
case ScriptOpcode::Integer: {
result.type = ScriptValueType::Integer;
result.intValue = data;
break;
}
case ScriptOpcode::Float: {
result.type = ScriptValueType::Float;
float floatValue;
std::memcpy(&floatValue, &data, sizeof(float));
result.floatValue = floatValue;
break;
}
case ScriptOpcode::StringRef5:
case ScriptOpcode::StringRef6:
case ScriptOpcode::StringRef7:
case ScriptOpcode::StringRef8: {
result.type = ScriptValueType::String;
result.stringValue = resolveScriptString(archive, fileType_, data);
break;
}
case ScriptOpcode::ExtendedString9: {
result.type = ScriptValueType::String;
if (offset + 10 <= data_.size()) {
uint8 newOpcode = readByte(offset + 5);
(void)newOpcode;
int32 newData = readInt32(offset + 6);
result.stringValue = resolveScriptString(archive, fileType_, newData);
}
break;
}
case ScriptOpcode::ExtendedString10: {
result.type = ScriptValueType::String;
result.stringValue = resolveScriptString(archive, fileType_, data);
break;
}
default: {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "ScriptParser: 未知的操作码: %d", opcode);
return std::nullopt;
}
}
return result;
}
} // namespace frostbite2D
@@ -0,0 +1,260 @@
#include <frostbite2D/resource/sound_pack_archive.h>
#include <frostbite2D/resource/binary_file_stream_reader.h>
#include <algorithm>
#include <cctype>
#include <SDL.h>
namespace frostbite2D {
const uint8 SoundPackArchive::NPK_KEY[256] = {
112, 117, 99, 104, 105, 107, 111, 110, 64, 110, 101, 111, 112, 108, 101, 32,
100, 117, 110, 103, 101, 111, 110, 32, 97, 110, 100, 32, 102, 105, 103, 104,
116, 101, 114, 32, 68, 78, 70, 68, 78, 70, 68, 78, 70, 68, 78, 70, 68, 78,
70, 68, 78, 70, 68, 78, 70, 68, 78, 70, 68, 78, 70, 68, 78, 70, 68, 78, 70,
68, 78, 70, 68, 78, 70, 68, 78, 70, 68, 78, 70, 68, 78, 70, 68, 78, 70, 68,
78, 70, 68, 78, 70, 68, 78, 70, 68, 78, 70, 68, 78, 70, 68, 78, 70, 68, 78,
70, 68, 78, 70, 68, 78, 70, 68, 78, 70, 68, 78, 70, 68, 78, 70, 68, 78, 70,
68, 78, 70, 68, 78, 70, 68, 78, 70, 68, 78, 70, 68, 78, 70, 68, 78, 70, 68,
78, 70, 68, 78, 70, 68, 78, 70, 68, 78, 70, 0
};
SoundPackArchive& SoundPackArchive::get() {
static SoundPackArchive instance;
return instance;
}
void SoundPackArchive::setSoundPackDirectory(const std::string& dir) {
soundPackDirectory_ = dir;
}
const std::string& SoundPackArchive::getSoundPackDirectory() const {
return soundPackDirectory_;
}
void SoundPackArchive::init() {
if (initialized_) {
close();
}
scanNpkFiles();
initialized_ = true;
}
void SoundPackArchive::close() {
audioIndex_.clear();
audioCache_.clear();
lruList_.clear();
currentCacheSize_ = 0;
initialized_ = false;
}
bool SoundPackArchive::isOpen() const {
return initialized_;
}
std::string SoundPackArchive::normalizePath(const std::string& path) const {
std::string result = path;
std::transform(result.begin(), result.end(), result.begin(),
[](unsigned char c) { return std::tolower(c); });
return result;
}
void SoundPackArchive::scanNpkFiles() {
Asset& asset = Asset::get();
std::string npkDir = asset.resolvePath(soundPackDirectory_);
if (!asset.isDirectory(npkDir)) {
SDL_Log("Sound NPK directory not found: %s", npkDir.c_str());
return;
}
std::vector<std::string> files = asset.listFilesWithExtension(npkDir, ".npk");
SDL_Log("Scanning %d sound NPK files...", static_cast<int>(files.size()));
for (const auto &file : files) {
parseNpkFile(file);
}
}
bool SoundPackArchive::parseNpkFile(const std::string& npkPath) {
Asset& asset = Asset::get();
BinaryFileStreamReader reader(npkPath);
if (!reader.isOpen()) {
SDL_Log("Failed to open sound NPK file: %s", npkPath.c_str());
return false;
}
std::string npkFileName = asset.getFileName(npkPath);
std::string header = reader.readNullTerminatedString();
if (header.find("NeoplePack_Bill") == std::string::npos) {
return false;
}
int32 audioCount = reader.readInt32();
for (int32 i = 0; i < audioCount; ++i) {
int32 offset = reader.readInt32();
int32 length = reader.readInt32();
std::string audioPath = readNpkInfoString(reader);
AudioRef audio;
audio.path = normalizePath(audioPath);
audio.npkFile = npkFileName;
audio.offset = static_cast<uint32>(offset);
audio.size = static_cast<uint32>(length);
audio.loaded = false;
audioIndex_[audio.path] = audio;
}
return true;
}
bool SoundPackArchive::hasAudio(const std::string& path) const {
return audioIndex_.find(normalizePath(path)) != audioIndex_.end();
}
std::optional<AudioRef> SoundPackArchive::getAudio(const std::string& path) {
auto it = audioIndex_.find(normalizePath(path));
if (it == audioIndex_.end()) {
return std::nullopt;
}
return it->second;
}
std::vector<std::string> SoundPackArchive::listAudios() const {
std::vector<std::string> result;
result.reserve(audioIndex_.size());
for (const auto& pair : audioIndex_) {
result.push_back(pair.first);
}
return result;
}
std::optional<std::vector<uint8>> SoundPackArchive::getAudioData(const AudioRef& audio) {
std::string audioPath = normalizePath(audio.path);
auto cacheIt = audioCache_.find(audioPath);
if (cacheIt == audioCache_.end()) {
auto it = audioIndex_.find(audioPath);
if (it == audioIndex_.end()) {
return std::nullopt;
}
if (!loadAudioData(it->second)) {
return std::nullopt;
}
cacheIt = audioCache_.find(audioPath);
if (cacheIt == audioCache_.end()) {
return std::nullopt;
}
}
updateCacheUsage(audioPath);
return cacheIt->second.data;
}
bool SoundPackArchive::loadAudioData(AudioRef& audio) {
Asset& asset = Asset::get();
std::string npkPath = asset.combinePath(soundPackDirectory_, audio.npkFile);
npkPath = asset.resolvePath(npkPath);
BinaryReader reader(npkPath);
if (!reader.isOpen()) {
SDL_Log("Failed to open NPK for audio: %s", npkPath.c_str());
return false;
}
reader.seek(audio.offset);
std::vector<uint8> audioData = reader.readBytes(audio.size);
if (audioData.size() != audio.size) {
SDL_Log("Failed to read complete audio data");
return false;
}
evictCacheIfNeeded(audioData.size());
CachedAudioData cachedData;
cachedData.data = std::move(audioData);
cachedData.memoryUsage = cachedData.data.size();
cachedData.lastUseTime = SDL_GetTicks();
audioCache_[audio.path] = std::move(cachedData);
currentCacheSize_ += audioCache_[audio.path].memoryUsage;
lruList_.push_front(audio.path);
audio.loaded = true;
audioIndex_[audio.path] = audio;
return true;
}
void SoundPackArchive::setCacheSize(size_t maxBytes) {
maxCacheSize_ = maxBytes;
evictCacheIfNeeded(0);
}
void SoundPackArchive::clearCache() {
audioCache_.clear();
lruList_.clear();
currentCacheSize_ = 0;
}
size_t SoundPackArchive::getCacheUsage() const {
return currentCacheSize_;
}
void SoundPackArchive::evictCacheIfNeeded(size_t requiredSize) {
while (currentCacheSize_ + requiredSize > maxCacheSize_ && !lruList_.empty()) {
std::string oldest = lruList_.back();
lruList_.pop_back();
auto it = audioCache_.find(oldest);
if (it != audioCache_.end()) {
currentCacheSize_ -= it->second.memoryUsage;
audioCache_.erase(it);
}
}
}
void SoundPackArchive::updateCacheUsage(const std::string& audioPath) {
auto it = std::find(lruList_.begin(), lruList_.end(), audioPath);
if (it != lruList_.end()) {
lruList_.erase(it);
}
lruList_.push_front(audioPath);
auto cacheIt = audioCache_.find(audioPath);
if (cacheIt != audioCache_.end()) {
cacheIt->second.lastUseTime = SDL_GetTicks();
}
}
std::string SoundPackArchive::readNpkInfoString(BinaryFileStreamReader& reader) {
if (reader.eof()) {
return "";
}
std::vector<uint8> encrypted = reader.readBytes(256);
if (encrypted.size() < 256) {
return "";
}
std::vector<char> decrypted(256);
for (int i = 0; i < 256; ++i) {
decrypted[i] = static_cast<char>(encrypted[i] ^ NPK_KEY[i]);
}
return std::string(decrypted.data());
}
void SoundPackArchive::setDefaultAudio(const std::string& audioPath) {
defaultAudioPath_ = normalizePath(audioPath);
}
const std::string& SoundPackArchive::getDefaultAudioPath() const {
return defaultAudioPath_;
}
}
@@ -0,0 +1,93 @@
#include <frostbite2D/scene/scene.h>
#include <algorithm>
#include <SDL2/SDL.h>
namespace frostbite2D {
Scene* Scene::current_ = nullptr;
Scene::Scene() = default;
Scene::~Scene() {
}
void Scene::onEnter() {
}
void Scene::onExit() {
}
void Scene::Update(float deltaTime) {
UpdateChildren(deltaTime);
}
void Scene::Render() {
RenderChildren();
}
Scene* Scene::GetCurrent() {
return current_;
}
bool Scene::OnEvent(const Event& event) {
if (Actor::OnEvent(event)) {
return true;
}
if (dispatchToChildren(event)) {
return true;
}
return false;
}
void Scene::SetRenderStyleProfile(RenderStyleProfileId profile) {
renderStyleProfileOverride_ = profile;
hasRenderStyleProfileOverride_ = true;
}
void Scene::ClearRenderStyleProfileOverride() {
hasRenderStyleProfileOverride_ = false;
}
RenderStyleProfileId Scene::ResolveRenderStyleProfile(
RenderStyleProfileId defaultProfile) const {
return hasRenderStyleProfileOverride_ ? renderStyleProfileOverride_
: defaultProfile;
}
bool Scene::dispatchToChildren(const Event& event) {
if (event.isPropagationStopped()) {
return false;
}
std::vector<Actor*> sortedChildren;
for (auto it = children_.begin(); it != children_.end(); ++it) {
Actor* child = it->Get();
if (child && child->IsEventReceiveEnabled()) {
sortedChildren.push_back(child);
}
}
std::sort(sortedChildren.begin(), sortedChildren.end(),
[](Actor* a, Actor* b) {
return a->GetEventPriority() < b->GetEventPriority();
});
for (Actor* child : sortedChildren) {
if (child->IsEventInterceptEnabled()) {
if (child->OnEvent(event)) {
return true;
}
} else {
if (child->DispatchEvent(event)) {
return true;
}
}
}
return false;
}
}
@@ -0,0 +1,248 @@
#include <frostbite2D/graphics/renderer.h>
#include <frostbite2D/scene/scene.h>
#include <frostbite2D/scene/scene_manager.h>
#include <frostbite2D/scene/ui_scene.h>
namespace frostbite2D {
namespace {
RenderStyleProfileId resolveSceneRenderStyleProfile(const Scene* scene,
const Renderer& renderer) {
if (!scene) {
return renderer.getDefaultRenderStyleProfile();
}
return scene->ResolveRenderStyleProfile(renderer.getDefaultRenderStyleProfile());
}
} // namespace
SceneManager& SceneManager::get() {
static SceneManager instance;
return instance;
}
void SceneManager::PushScene(Ptr<Scene> scene) {
if (!scene) {
return;
}
if (!sceneStack_.empty()) {
sceneStack_.back()->onExit();
}
sceneStack_.push_back(scene);
Scene::current_ = scene.Get();
scene->onEnter();
}
void SceneManager::PopScene() {
if (sceneStack_.empty()) {
return;
}
sceneStack_.back()->onExit();
sceneStack_.pop_back();
Scene::current_ = sceneStack_.empty() ? nullptr : sceneStack_.back().Get();
if (!sceneStack_.empty()) {
sceneStack_.back()->onEnter();
}
}
void SceneManager::ReplaceScene(Ptr<Scene> scene) {
if (!scene) {
return;
}
if (!sceneStack_.empty()) {
sceneStack_.back()->onExit();
sceneStack_.pop_back();
}
sceneStack_.push_back(scene);
Scene::current_ = scene.Get();
scene->onEnter();
}
void SceneManager::PushUIScene(Ptr<UIScene> scene) {
if (!scene) {
return;
}
uiSceneStack_.push_back(scene);
scene->onEnter();
}
void SceneManager::PopUIScene() {
if (uiSceneStack_.empty()) {
return;
}
uiSceneStack_.back()->onExit();
uiSceneStack_.pop_back();
}
void SceneManager::ReplaceUIScene(Ptr<UIScene> scene) {
if (!scene) {
return;
}
if (!uiSceneStack_.empty()) {
uiSceneStack_.back()->onExit();
uiSceneStack_.pop_back();
}
uiSceneStack_.push_back(scene);
scene->onEnter();
}
bool SceneManager::RemoveUIScene(UIScene* scene) {
if (!scene) {
return false;
}
for (auto it = uiSceneStack_.begin(); it != uiSceneStack_.end(); ++it) {
if (it->Get() != scene) {
continue;
}
(*it)->onExit();
uiSceneStack_.erase(it);
return true;
}
return false;
}
void SceneManager::ClearUIScenes() {
while (!uiSceneStack_.empty()) {
uiSceneStack_.back()->onExit();
uiSceneStack_.pop_back();
}
}
void SceneManager::ClearAll() {
ClearUIScenes();
while (!sceneStack_.empty()) {
sceneStack_.back()->onExit();
sceneStack_.pop_back();
}
Scene::current_ = nullptr;
}
void SceneManager::Update(float deltaTime) {
if (!sceneStack_.empty()) {
Renderer& renderer = Renderer::get();
RenderStyleProfileId profile =
resolveSceneRenderStyleProfile(sceneStack_.back().Get(), renderer);
renderer.applyRenderStyleToCamera(renderer.getCamera(), profile,
RenderStyleLayerRole::World);
sceneStack_.back()->Update(deltaTime);
}
}
void SceneManager::UpdateUI(float deltaTime) {
Renderer& renderer = Renderer::get();
for (auto& scene : uiSceneStack_) {
if (scene) {
scene->GetCamera()->setViewport(renderer.getUIVirtualWidth(),
renderer.getUIVirtualHeight());
RenderStyleProfileId profile =
resolveSceneRenderStyleProfile(scene.Get(), renderer);
renderer.applyRenderStyleToCamera(scene->GetCamera(), profile,
RenderStyleLayerRole::UI);
scene->Update(deltaTime);
}
}
}
void SceneManager::Render() {
Renderer& renderer = Renderer::get();
Camera* worldCamera = renderer.getCamera();
RenderStyleProfileId worldProfile = renderer.getDefaultRenderStyleProfile();
bool previousCullingEnabled = renderer.isCullingEnabled();
if (!sceneStack_.empty()) {
renderer.applyWorldViewport();
renderer.setCullingEnabled(true);
worldProfile =
resolveSceneRenderStyleProfile(sceneStack_.back().Get(), renderer);
renderer.applyRenderStyleToCamera(worldCamera, worldProfile,
RenderStyleLayerRole::World);
renderer.setActiveRenderStyleProfile(worldProfile,
RenderStyleLayerRole::World);
sceneStack_.back()->Render();
}
renderer.setCullingEnabled(false);
for (auto& scene : uiSceneStack_) {
if (!scene) {
continue;
}
RenderStyleProfileId uiProfile =
resolveSceneRenderStyleProfile(scene.Get(), renderer);
scene->GetCamera()->setViewport(renderer.getUIVirtualWidth(),
renderer.getUIVirtualHeight());
renderer.applyRenderStyleToCamera(scene->GetCamera(), uiProfile,
RenderStyleLayerRole::UI);
renderer.setActiveRenderStyleProfile(uiProfile, RenderStyleLayerRole::UI);
renderer.setCamera(scene->GetCamera());
renderer.applyUIViewport();
scene->Render();
}
if (renderer.getCamera() != worldCamera) {
renderer.setActiveRenderStyleProfile(worldProfile, RenderStyleLayerRole::World);
renderer.setCamera(worldCamera);
}
renderer.applyWorldViewport();
renderer.setCullingEnabled(previousCullingEnabled);
renderer.setActiveRenderStyleProfile(worldProfile, RenderStyleLayerRole::World);
}
bool SceneManager::DispatchEvent(const Event& event) {
if (DispatchUIEvent(event)) {
return true;
}
Scene* currentScene = GetCurrentScene();
return currentScene ? currentScene->OnEvent(event) : false;
}
bool SceneManager::DispatchUIEvent(const Event& event) {
for (auto it = uiSceneStack_.rbegin(); it != uiSceneStack_.rend(); ++it) {
if (*it && (*it)->OnEvent(event)) {
return true;
}
}
return false;
}
Scene* SceneManager::GetCurrentScene() const {
if (sceneStack_.empty()) {
return nullptr;
}
return sceneStack_.back().Get();
}
UIScene* SceneManager::GetCurrentUIScene() const {
if (uiSceneStack_.empty()) {
return nullptr;
}
return uiSceneStack_.back().Get();
}
bool SceneManager::HasActiveScene() const {
return !sceneStack_.empty();
}
bool SceneManager::HasActiveUIScene() const {
return !uiSceneStack_.empty();
}
} // namespace frostbite2D
@@ -0,0 +1,14 @@
#include <frostbite2D/graphics/renderer.h>
#include <frostbite2D/scene/ui_scene.h>
namespace frostbite2D {
UIScene::UIScene() {
Renderer& renderer = Renderer::get();
camera_.setViewport(renderer.getUIVirtualWidth(), renderer.getUIVirtualHeight());
camera_.setFlipY(true);
camera_.setZoom(1.0f);
camera_.setPosition(Vec2::Zero());
}
} // namespace frostbite2D
@@ -0,0 +1,120 @@
#include <frostbite2D/script/quickjs_bridge_base.h>
#include <SDL2/SDL.h>
#include <frostbite2D/script/quickjs_vm.h>
#include <quickjspp.hpp>
#include <exception>
#include <utility>
namespace frostbite2D {
std::shared_ptr<JsActor> JsActor::GetParent() const {
if (!actor_ || !actor_->GetParent()) {
return nullptr;
}
return std::make_shared<JsActor>(Ptr<Actor>(actor_->GetParent()));
}
std::shared_ptr<JsVec2> JsActor::GetTopLeftPosition() const {
if (!actor_) {
return std::make_shared<JsVec2>(0.0, 0.0);
}
return std::make_shared<JsVec2>(actor_->GetTopLeftPosition());
}
void JsActor::SetTopLeftPosition(const std::shared_ptr<JsVec2>& position) {
if (actor_ && position) {
actor_->SetTopLeftPosition(position->toNative());
}
}
void JsActor::SetTopLeftPositionXY(double x, double y) {
if (actor_) {
actor_->SetTopLeftPosition(static_cast<float>(x), static_cast<float>(y));
}
}
std::shared_ptr<JsVec2> JsActor::LocalToWorld(
const std::shared_ptr<JsVec2>& point) const {
if (!actor_ || !point) {
return std::make_shared<JsVec2>(0.0, 0.0);
}
return std::make_shared<JsVec2>(actor_->LocalToWorld(point->toNative()));
}
std::shared_ptr<JsVec2> JsActor::WorldToLocal(
const std::shared_ptr<JsVec2>& point) const {
if (!actor_ || !point) {
return std::make_shared<JsVec2>(0.0, 0.0);
}
return std::make_shared<JsVec2>(actor_->WorldToLocal(point->toNative()));
}
bool JsActor::ContainsWorldPoint(double x, double y) const {
return actor_ && actor_->ContainsWorldPoint(static_cast<float>(x),
static_cast<float>(y));
}
uint32 JsActor::AddEventListener(const std::string& type,
std::function<bool(qjs::Value)> callback) {
if (!actor_ || !callback) {
return 0;
}
QuickJSVM& vm = QuickJSVM::get();
auto eventType = vm.parseEventTypeName(type);
if (!eventType) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"[QuickJS] Unsupported event type for AddEventListener: %s",
type.c_str());
return 0;
}
actor_->EnableEventReceive();
return actor_->AddEventListener(
*eventType, [callback = std::move(callback)](const Event& event) -> bool {
QuickJSVM& vm = QuickJSVM::get();
if (!vm.isInitialized() || !vm.getContext()) {
return false;
}
try {
qjs::Value eventValue = vm.wrapEvent(event);
if (eventValue == JS_UNDEFINED || eventValue == JS_NULL) {
return false;
}
bool handled = callback(std::move(eventValue));
if (!vm.runPendingJobs()) {
return false;
}
return handled;
} catch (qjs::exception) {
vm.logException("JsActor::AddEventListener callback failed");
} catch (const std::exception& e) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"JsActor::AddEventListener runtime error: %s",
e.what());
}
return false;
});
}
bool JsActor::RemoveEventListener(uint32 listenerId) {
return actor_ && actor_->RemoveEventListener(listenerId);
}
void JsActor::ClearEventListeners() {
if (actor_) {
actor_->ClearEventListeners();
}
}
} // namespace frostbite2D
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,67 @@
#include <frostbite2D/types/uuid.h>
#include <cstdio>
#include <sstream>
#include <iomanip>
namespace frostbite2D {
UUID UUID::generate() {
std::array<uint8_t, 16> data;
for (int i = 0; i < 16; ++i) {
data[i] = static_cast<uint8_t>(rand() % 256);
}
data[6] = (data[6] & 0x0F) | 0x40;
data[8] = (data[8] & 0x3F) | 0x80;
return UUID(data);
}
UUID UUID::fromString(const std::string& str) {
if (str.length() != 36) {
return UUID{};
}
std::array<uint8_t, 16> data{};
std::stringstream ss(str);
char dash;
for (int i = 0; i < 16; ++i) {
int byte;
ss >> std::hex >> byte;
data[i] = static_cast<uint8_t>(byte);
if (i < 15) {
ss >> dash;
}
}
return UUID(data);
}
std::string UUID::toString() const {
const auto& data = data_;
std::stringstream ss;
ss << std::hex << std::setfill('0')
<< std::setw(2) << static_cast<int>(data[0])
<< std::setw(2) << static_cast<int>(data[1])
<< std::setw(2) << static_cast<int>(data[2])
<< std::setw(2) << static_cast<int>(data[3])
<< '-'
<< std::setw(2) << static_cast<int>(data[4])
<< std::setw(2) << static_cast<int>(data[5])
<< '-'
<< std::setw(2) << static_cast<int>(data[6])
<< std::setw(2) << static_cast<int>(data[7])
<< '-'
<< std::setw(2) << static_cast<int>(data[8])
<< std::setw(2) << static_cast<int>(data[9])
<< '-'
<< std::setw(2) << static_cast<int>(data[10])
<< std::setw(2) << static_cast<int>(data[11])
<< std::setw(2) << static_cast<int>(data[12])
<< std::setw(2) << static_cast<int>(data[13])
<< std::setw(2) << static_cast<int>(data[14])
<< std::setw(2) << static_cast<int>(data[15]);
return ss.str();
}
} // namespace frostbite2D
File diff suppressed because one or more lines are too long