#include "character/CharacterActionLibrary.h" #include #include #include #include #include #include #include namespace frostbite2D { namespace { std::string toLower(const std::string& value) { std::string result = value; std::transform(result.begin(), result.end(), result.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); return result; } int toInt(const std::string& value, int fallback = 0) { try { return std::stoi(value); } catch (...) { return fallback; } } float toFloat(const std::string& value, float fallback = 0.0f) { try { return std::stof(value); } catch (...) { return fallback; } } bool isTrueToken(const std::string& value) { std::string token = toLower(value); return token == "1" || token == "true" || token == "[true]" || token == "yes"; } std::string trim(const std::string& value) { size_t begin = 0; size_t end = value.size(); while (begin < end && std::isspace(static_cast(value[begin]))) { ++begin; } while (end > begin && std::isspace(static_cast(value[end - 1]))) { --end; } return value.substr(begin, end - begin); } std::string fileStem(const std::string& path) { size_t slashPos = path.find_last_of("/\\"); size_t begin = slashPos == std::string::npos ? 0 : slashPos + 1; size_t dotPos = path.find_last_of('.'); if (dotPos == std::string::npos || dotPos < begin) { return path.substr(begin); } return path.substr(begin, dotPos - begin); } class ScriptTokenStream { public: explicit ScriptTokenStream(const std::string& path) : path_(PvfArchive::get().normalizePath(path)) { auto rawData = PvfArchive::get().getFileRawData(path_); if (!rawData) { return; } ScriptParser parser(*rawData, path_); if (!parser.isValid()) { return; } for (const auto& value : parser.parseAll()) { tokens_.push_back(value.toString()); } valid_ = true; } bool IsValid() const { return valid_; } bool IsEnd() const { return index_ >= tokens_.size(); } std::string Next() { if (IsEnd()) { return {}; } return tokens_[index_++]; } private: std::string path_; std::vector tokens_; size_t index_ = 0; bool valid_ = false; }; // action_list.lst 同时兼容两种格式: // 1. 标准 PVF #PVF_File + 索引 + `path` // 2. 早期临时版的 actionId + path 成对写法 bool isNewActionListPath(const std::string& path) { return path.find("action_logic") != std::string::npos && path.size() >= 4 && path.substr(path.size() - 4) == ".act"; } // action_list.lst only accepts #PVF_File entries like `swordman/action_logic/idle.act`. std::vector> parseActionListEntries( ScriptTokenStream& listStream) { std::vector> entries; std::vector tokens; auto& pvf = PvfArchive::get(); while (!listStream.IsEnd()) { std::string token = trim(listStream.Next()); if (!token.empty()) { tokens.push_back(token); } } for (size_t i = 0; i + 1 < tokens.size(); i += 2) { std::string rawActionPath = trim(tokens[i + 1]); std::string actionPath = pvf.normalizePath(rawActionPath); if (actionPath.empty() || !isNewActionListPath(actionPath)) { continue; } entries.push_back({toLower(fileStem(actionPath)), actionPath}); } return entries; } CharacterFrameEventType parseFrameEventType(const std::string& token) { std::string lower = toLower(token); if (lower == "open_hit_window") { return CharacterFrameEventType::OpenHitWindow; } if (lower == "close_hit_window") { return CharacterFrameEventType::CloseHitWindow; } if (lower == "open_input_buffer") { return CharacterFrameEventType::OpenInputBuffer; } if (lower == "close_input_buffer") { return CharacterFrameEventType::CloseInputBuffer; } if (lower == "set_velocity_xy") { return CharacterFrameEventType::SetVelocityXY; } if (lower == "set_velocity_z") { return CharacterFrameEventType::SetVelocityZ; } if (lower == "finish_action") { return CharacterFrameEventType::FinishAction; } return CharacterFrameEventType::AnimationEvent; } void addOrReplaceAction(std::map& actions, CharacterActionDefinition definition) { if (definition.actionId.empty()) { return; } actions[definition.actionId] = std::move(definition); } } // namespace void CharacterCommandBuffer::Advance(float deltaTime) { for (auto& button : buttons_) { button.age += deltaTime; } buttons_.erase( std::remove_if(buttons_.begin(), buttons_.end(), [this](const BufferedButton& button) { return button.age > bufferWindowSeconds_; }), buttons_.end()); } void CharacterCommandBuffer::Submit(const CharacterCommand& command) { if (command.type == CharacterCommandType::Move) { moveAxis_ = command.moveAxis; if (moveAxis_.lengthSquared() > 1.0f) { moveAxis_ = moveAxis_.normalized(); } return; } buttons_.push_back({command.type, 0.0f}); } CharacterIntent CharacterCommandBuffer::BuildIntent() const { CharacterIntent intent; intent.moveX = moveAxis_.x; intent.moveY = moveAxis_.y; intent.wantJump = HasBuffered(CharacterCommandType::JumpPressed); intent.wantAttack = HasBuffered(CharacterCommandType::AttackPressed); intent.wantSkill1 = HasBuffered(CharacterCommandType::Skill1Pressed); intent.wantDash = HasBuffered(CharacterCommandType::DashPressed); return intent; } bool CharacterCommandBuffer::HasBuffered(CharacterCommandType type) const { for (const auto& button : buttons_) { if (button.type == type) { return true; } } return false; } void CharacterCommandBuffer::Consume(CharacterCommandType type) { auto it = std::find_if(buttons_.begin(), buttons_.end(), [type](const BufferedButton& button) { return button.type == type; }); if (it != buttons_.end()) { buttons_.erase(it); } } void CharacterCommandBuffer::Clear() { moveAxis_ = Vec2::Zero(); buttons_.clear(); } bool CharacterActionLibrary::LoadForConfig(const character::CharacterConfig& config) { actions_.clear(); TryLoadPvfActionScripts(config); return !actions_.empty(); } const CharacterActionDefinition* CharacterActionLibrary::FindAction( const std::string& actionId) const { auto it = actions_.find(actionId); if (it != actions_.end()) { return &it->second; } return nullptr; } const CharacterActionDefinition* CharacterActionLibrary::GetDefaultAction() const { return FindAction("idle"); } std::string CharacterActionLibrary::DescribeActionIds() const { if (actions_.empty()) { return ""; } std::ostringstream stream; bool first = true; for (const auto& [actionId, definition] : actions_) { (void)definition; if (!first) { stream << ", "; } stream << actionId; first = false; } return stream.str(); } bool CharacterActionLibrary::TryLoadPvfActionScripts( const character::CharacterConfig& config) { std::string listPath = "character/" + config.jobTag + "/action_logic/action_list.lst"; ScriptTokenStream listStream(listPath); if (!listStream.IsValid()) { return false; } auto entries = parseActionListEntries(listStream); if (entries.empty()) { SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "CharacterActionLibrary: action_list parsed 0 entries from %s", listPath.c_str()); } for (const auto& [actionId, relativePath] : entries) { if (actionId.empty() || relativePath.empty()) { continue; } std::string fullActPath = relativePath.rfind("character/", 0) == 0 ? relativePath : "character/" + relativePath; ScriptTokenStream actionStream(fullActPath); if (!actionStream.IsValid()) { continue; } CharacterActionDefinition definition; definition.actionId = actionId; while (!actionStream.IsEnd()) { std::string token = toLower(actionStream.Next()); if (token.empty()) { break; } if (token == "[action id]") { definition.actionId = toLower(trim(actionStream.Next())); } else if (token == "[animation tag]") { definition.animationTag = trim(actionStream.Next()); } else if (token == "[total frames]") { definition.totalFrames = toInt(actionStream.Next(), definition.totalFrames); } else if (token == "[loop]") { definition.loop = isTrueToken(actionStream.Next()); } else if (token == "[can move]") { definition.canMove = isTrueToken(actionStream.Next()); } else if (token == "[can turn]") { definition.canTurn = isTrueToken(actionStream.Next()); } else if (token == "[can be interrupted]") { definition.canBeInterrupted = isTrueToken(actionStream.Next()); } else if (token == "[move speed scale]") { definition.moveSpeedScale = toFloat(actionStream.Next(), definition.moveSpeedScale); } else if (token == "[cancel rule]") { CharacterCancelRuleDefinition rule; rule.targetAction = toLower(trim(actionStream.Next())); rule.beginFrame = toInt(actionStream.Next(), 0); rule.endFrame = toInt(actionStream.Next(), rule.beginFrame); rule.requireGrounded = isTrueToken(actionStream.Next()); rule.requireAirborne = isTrueToken(actionStream.Next()); definition.cancelRules.push_back(rule); } else if (token == "[frame event]") { CharacterFrameEventDefinition frameEvent; frameEvent.frame = toInt(actionStream.Next(), 0); frameEvent.type = parseFrameEventType(trim(actionStream.Next())); if (frameEvent.type == CharacterFrameEventType::SetVelocityXY) { frameEvent.velocityXY.x = toFloat(actionStream.Next(), 0.0f); frameEvent.velocityXY.y = toFloat(actionStream.Next(), 0.0f); } else if (frameEvent.type == CharacterFrameEventType::SetVelocityZ) { frameEvent.velocityZ = toFloat(actionStream.Next(), 0.0f); } else { frameEvent.stringValue = trim(actionStream.Next()); } definition.frameEvents.push_back(frameEvent); } } addOrReplaceAction(actions_, std::move(definition)); } return true; } std::optional loadCharacterActionLibrary( const character::CharacterConfig& config) { CharacterActionLibrary library; if (!library.LoadForConfig(config)) { return std::nullopt; } return library; } } // namespace frostbite2D