Initial engine repository
This commit is contained in:
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
@@ -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 条目下的无效 ITEM(TAG 为空): %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
Reference in New Issue
Block a user