-- Improvements --
- Adding better animation support + modupak support being added though WIP, - Optimized the engine a bit, - Added a few references to the UI for the Scripting API, - Improved the starting intro and made it quicker, - Added more shortcut keys for deleting objects, copying and pasting, - Improved hierarchy to have a better system to allow for moving objects between other objects out of the ID range without being affected, - Added WIP gizmos for 2D world, just gotta hook it up to the correct layer lol. - Added a few test scripts in the modularity repo, - Added more image functions to the engine to allow for 9 split scaling for image buttons, -- Known Bugs -- - Some stuff related to animations sometimes bug out when scrolling in the editor, - Sometimes the starting intro goes behind the wrong layers, but typically fixes itself, - Scripts that use the wrong include directive sometimes crash the engine instead of pushing an error and ignoring the script, - there's a bug that occasionally happens with switching workspaces that causes it to flicker in the sprite previewer instead of automatically spawning below the hierarchy, - auto scrollling in the console tends to shake a lot.
This commit is contained in:
145
Scripts/AutoEnableAndDisableListsOfObjectsAfterAmountOfTime.cpp
Normal file
145
Scripts/AutoEnableAndDisableListsOfObjectsAfterAmountOfTime.cpp
Normal file
@@ -0,0 +1,145 @@
|
||||
#include "DialoguePortShared.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
using namespace DialoguePort;
|
||||
|
||||
struct ToggleConfig {
|
||||
std::vector<std::string> objectsToEnable;
|
||||
std::vector<std::string> objectsToDisable;
|
||||
float intervalSeconds = 1.0f;
|
||||
};
|
||||
|
||||
struct ToggleRuntimeState {
|
||||
float timer = 0.0f;
|
||||
bool initialized = false;
|
||||
bool configInitialized = false;
|
||||
std::string cachedEnableRefsRaw;
|
||||
std::string cachedDisableRefsRaw;
|
||||
std::string cachedIntervalRaw;
|
||||
ToggleConfig cachedConfig;
|
||||
};
|
||||
|
||||
std::unordered_map<int, ToggleRuntimeState> g_runtimeStates;
|
||||
|
||||
constexpr const char* kSettingEnableRefs = "toggle.objectsToEnable";
|
||||
constexpr const char* kSettingDisableRefs = "toggle.objectsToDisable";
|
||||
constexpr const char* kSettingInterval = "toggle.intervalSeconds";
|
||||
|
||||
bool setSettingIfChanged(ScriptContext& ctx, const std::string& key, const std::string& value) {
|
||||
if (ctx.GetSetting(key, "") == value) return false;
|
||||
ctx.SetSetting(key, value);
|
||||
return true;
|
||||
}
|
||||
|
||||
ToggleConfig loadConfig(ScriptContext& ctx) {
|
||||
ToggleConfig config;
|
||||
config.objectsToEnable = DeserializeObjectRefs(ctx.GetSetting(kSettingEnableRefs, ""));
|
||||
config.objectsToDisable = DeserializeObjectRefs(ctx.GetSetting(kSettingDisableRefs, ""));
|
||||
config.intervalSeconds = std::max(0.0f, ParseFloat(ctx.GetSetting(kSettingInterval, "1"), 1.0f));
|
||||
return config;
|
||||
}
|
||||
|
||||
const ToggleConfig& getCachedConfig(ScriptContext& ctx, ToggleRuntimeState& state) {
|
||||
const std::string enableRefsRaw = ctx.GetSetting(kSettingEnableRefs, "");
|
||||
const std::string disableRefsRaw = ctx.GetSetting(kSettingDisableRefs, "");
|
||||
const std::string intervalRaw = ctx.GetSetting(kSettingInterval, "1");
|
||||
|
||||
const bool configChanged =
|
||||
!state.configInitialized ||
|
||||
state.cachedEnableRefsRaw != enableRefsRaw ||
|
||||
state.cachedDisableRefsRaw != disableRefsRaw ||
|
||||
state.cachedIntervalRaw != intervalRaw;
|
||||
|
||||
if (configChanged) {
|
||||
state.cachedEnableRefsRaw = enableRefsRaw;
|
||||
state.cachedDisableRefsRaw = disableRefsRaw;
|
||||
state.cachedIntervalRaw = intervalRaw;
|
||||
state.cachedConfig.objectsToEnable = DeserializeObjectRefs(enableRefsRaw);
|
||||
state.cachedConfig.objectsToDisable = DeserializeObjectRefs(disableRefsRaw);
|
||||
state.cachedConfig.intervalSeconds = std::max(0.0f, ParseFloat(intervalRaw, 1.0f));
|
||||
state.configInitialized = true;
|
||||
}
|
||||
|
||||
return state.cachedConfig;
|
||||
}
|
||||
|
||||
void saveConfig(ScriptContext& ctx, const ToggleConfig& config) {
|
||||
setSettingIfChanged(ctx, kSettingEnableRefs, SerializeObjectRefs(config.objectsToEnable));
|
||||
setSettingIfChanged(ctx, kSettingDisableRefs, SerializeObjectRefs(config.objectsToDisable));
|
||||
setSettingIfChanged(ctx, kSettingInterval, std::to_string(config.intervalSeconds));
|
||||
}
|
||||
|
||||
bool enableObjects(ScriptContext& ctx, const std::vector<std::string>& refs) {
|
||||
bool changed = false;
|
||||
for (const std::string& ref : refs) {
|
||||
SceneObject* obj = ResolveSceneObjectRef(ctx, ref);
|
||||
if (!obj) continue;
|
||||
|
||||
if (!obj->enabled) {
|
||||
obj->enabled = true;
|
||||
changed = true;
|
||||
}
|
||||
if (obj->hasCollider && !obj->collider.enabled) {
|
||||
obj->collider.enabled = true;
|
||||
changed = true;
|
||||
}
|
||||
if (obj->hasCollider2D && !obj->collider2D.enabled) {
|
||||
obj->collider2D.enabled = true;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (changed) {
|
||||
ctx.MarkDirty();
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
bool disableObjects(ScriptContext& ctx, const std::vector<std::string>& refs) {
|
||||
return SetObjectsEnabledState(ctx, refs, false);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" void Script_OnInspector(ScriptContext& ctx) {
|
||||
ToggleConfig config = loadConfig(ctx);
|
||||
bool changed = false;
|
||||
|
||||
changed |= ImGui::DragFloat("Interval (s)", &config.intervalSeconds, 0.01f, 0.0f, 120.0f, "%.2f");
|
||||
config.intervalSeconds = std::max(0.0f, config.intervalSeconds);
|
||||
|
||||
changed |= DrawObjectRefListEditor(ctx, "Objects To Enable", config.objectsToEnable);
|
||||
changed |= DrawObjectRefListEditor(ctx, "Objects To Disable", config.objectsToDisable);
|
||||
|
||||
if (changed) {
|
||||
saveConfig(ctx, config);
|
||||
}
|
||||
}
|
||||
|
||||
void Begin(ScriptContext& ctx, float /*deltaTime*/) {
|
||||
if (!ctx.object) return;
|
||||
ToggleRuntimeState& state = g_runtimeStates[ctx.object->id];
|
||||
state.timer = 0.0f;
|
||||
state.initialized = true;
|
||||
}
|
||||
|
||||
void TickUpdate(ScriptContext& ctx, float deltaTime) {
|
||||
if (!ctx.object || deltaTime <= 0.0f) return;
|
||||
|
||||
ToggleRuntimeState& state = g_runtimeStates[ctx.object->id];
|
||||
if (!state.initialized) {
|
||||
state.timer = 0.0f;
|
||||
state.initialized = true;
|
||||
}
|
||||
const ToggleConfig& config = getCachedConfig(ctx, state);
|
||||
|
||||
state.timer += deltaTime;
|
||||
if (state.timer < config.intervalSeconds) return;
|
||||
|
||||
enableObjects(ctx, config.objectsToEnable);
|
||||
disableObjects(ctx, config.objectsToDisable);
|
||||
state.timer = 0.0f;
|
||||
}
|
||||
558
Scripts/DialoguePortShared.h
Normal file
558
Scripts/DialoguePortShared.h
Normal file
@@ -0,0 +1,558 @@
|
||||
#pragma once
|
||||
|
||||
#include "ScriptRuntime.h"
|
||||
#include "SceneObject.h"
|
||||
#include "ThirdParty/imgui/imgui.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cctype>
|
||||
#include <cfloat>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <limits>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <GLFW/glfw3.h>
|
||||
|
||||
namespace DialoguePort {
|
||||
|
||||
enum class Language {
|
||||
English = 0,
|
||||
German = 1,
|
||||
JapaneseKana = 2
|
||||
};
|
||||
|
||||
enum class TextEffectType : int {
|
||||
None = 0,
|
||||
Wave = 1 << 0,
|
||||
Shake = 1 << 1,
|
||||
Bounce = 1 << 2,
|
||||
Rotate = 1 << 3,
|
||||
Fade = 1 << 4
|
||||
};
|
||||
|
||||
struct DialogueLine {
|
||||
std::string characterName;
|
||||
std::string sentence;
|
||||
std::string sentenceGerman;
|
||||
std::string sentenceJapaneseKana;
|
||||
std::string characterSoundClip;
|
||||
float typingSpeed = 0.03f;
|
||||
TextEffectType textEffect = TextEffectType::None;
|
||||
float animationSpeed = 1.0f;
|
||||
float effectIntensity = 1.0f;
|
||||
std::string notTalkingObjectRef;
|
||||
std::string openMouthObjectRef;
|
||||
std::string closedMouthObjectRef;
|
||||
std::vector<std::string> itemsToEnable;
|
||||
std::vector<std::string> itemsToDisable;
|
||||
};
|
||||
|
||||
inline std::string Trim(const std::string& value) {
|
||||
size_t start = 0;
|
||||
while (start < value.size() && std::isspace(static_cast<unsigned char>(value[start])) != 0) {
|
||||
++start;
|
||||
}
|
||||
size_t end = value.size();
|
||||
while (end > start && std::isspace(static_cast<unsigned char>(value[end - 1])) != 0) {
|
||||
--end;
|
||||
}
|
||||
return value.substr(start, end - start);
|
||||
}
|
||||
|
||||
inline int ParseInt(const std::string& value, int fallback = 0) {
|
||||
if (value.empty()) return fallback;
|
||||
char* endPtr = nullptr;
|
||||
long parsed = std::strtol(value.c_str(), &endPtr, 10);
|
||||
if (endPtr == value.c_str()) return fallback;
|
||||
if (parsed < std::numeric_limits<int>::min()) return std::numeric_limits<int>::min();
|
||||
if (parsed > std::numeric_limits<int>::max()) return std::numeric_limits<int>::max();
|
||||
return static_cast<int>(parsed);
|
||||
}
|
||||
|
||||
inline float ParseFloat(const std::string& value, float fallback = 0.0f) {
|
||||
if (value.empty()) return fallback;
|
||||
char* endPtr = nullptr;
|
||||
float parsed = std::strtof(value.c_str(), &endPtr);
|
||||
if (endPtr == value.c_str()) return fallback;
|
||||
return parsed;
|
||||
}
|
||||
|
||||
inline bool ParseBool(const std::string& value, bool fallback = false) {
|
||||
std::string lowered = Trim(value);
|
||||
std::transform(lowered.begin(), lowered.end(), lowered.begin(), [](char c) {
|
||||
return static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
||||
});
|
||||
if (lowered == "1" || lowered == "true" || lowered == "yes" || lowered == "on") return true;
|
||||
if (lowered == "0" || lowered == "false" || lowered == "no" || lowered == "off") return false;
|
||||
return fallback;
|
||||
}
|
||||
|
||||
inline std::string GetScriptSetting(const ScriptComponent* script, const std::string& key,
|
||||
const std::string& fallback = "") {
|
||||
if (!script) return fallback;
|
||||
for (const ScriptSetting& setting : script->settings) {
|
||||
if (setting.key == key) {
|
||||
return setting.value;
|
||||
}
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
inline bool SetScriptSetting(ScriptComponent* script, const std::string& key, const std::string& value) {
|
||||
if (!script) return false;
|
||||
for (ScriptSetting& setting : script->settings) {
|
||||
if (setting.key == key) {
|
||||
if (setting.value == value) return false;
|
||||
setting.value = value;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
script->settings.push_back(ScriptSetting{key, value});
|
||||
return true;
|
||||
}
|
||||
|
||||
inline std::string EscapeField(const std::string& value, char delimiter) {
|
||||
std::string out;
|
||||
out.reserve(value.size() + 8);
|
||||
for (char c : value) {
|
||||
if (c == '\\' || c == delimiter || c == '\n' || c == '\r') {
|
||||
out.push_back('\\');
|
||||
if (c == '\n') out.push_back('n');
|
||||
else if (c == '\r') out.push_back('r');
|
||||
else out.push_back(c);
|
||||
} else {
|
||||
out.push_back(c);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
inline std::string UnescapeField(const std::string& value) {
|
||||
std::string out;
|
||||
out.reserve(value.size());
|
||||
bool escaped = false;
|
||||
for (char c : value) {
|
||||
if (escaped) {
|
||||
if (c == 'n') out.push_back('\n');
|
||||
else if (c == 'r') out.push_back('\r');
|
||||
else out.push_back(c);
|
||||
escaped = false;
|
||||
continue;
|
||||
}
|
||||
if (c == '\\') {
|
||||
escaped = true;
|
||||
continue;
|
||||
}
|
||||
out.push_back(c);
|
||||
}
|
||||
if (escaped) out.push_back('\\');
|
||||
return out;
|
||||
}
|
||||
|
||||
inline std::vector<std::string> SplitEscaped(const std::string& value, char delimiter) {
|
||||
std::vector<std::string> fields;
|
||||
std::string current;
|
||||
bool escaped = false;
|
||||
for (char c : value) {
|
||||
if (escaped) {
|
||||
if (c == 'n') current.push_back('\n');
|
||||
else if (c == 'r') current.push_back('\r');
|
||||
else current.push_back(c);
|
||||
escaped = false;
|
||||
continue;
|
||||
}
|
||||
if (c == '\\') {
|
||||
escaped = true;
|
||||
continue;
|
||||
}
|
||||
if (c == delimiter) {
|
||||
fields.push_back(current);
|
||||
current.clear();
|
||||
continue;
|
||||
}
|
||||
current.push_back(c);
|
||||
}
|
||||
if (escaped) current.push_back('\\');
|
||||
fields.push_back(current);
|
||||
return fields;
|
||||
}
|
||||
|
||||
inline std::string JoinEscaped(const std::vector<std::string>& values, char delimiter) {
|
||||
std::string joined;
|
||||
for (size_t i = 0; i < values.size(); ++i) {
|
||||
joined += EscapeField(values[i], delimiter);
|
||||
if (i + 1 < values.size()) joined.push_back(delimiter);
|
||||
}
|
||||
return joined;
|
||||
}
|
||||
|
||||
inline std::vector<std::string> DeserializeObjectRefs(const std::string& encoded) {
|
||||
std::vector<std::string> refs;
|
||||
if (encoded.empty()) return refs;
|
||||
std::vector<std::string> parsed = SplitEscaped(encoded, ';');
|
||||
refs.reserve(parsed.size());
|
||||
for (const std::string& item : parsed) {
|
||||
std::string trimmed = Trim(item);
|
||||
if (!trimmed.empty()) refs.push_back(trimmed);
|
||||
}
|
||||
return refs;
|
||||
}
|
||||
|
||||
inline std::string SerializeObjectRefs(const std::vector<std::string>& refs) {
|
||||
std::vector<std::string> cleaned;
|
||||
cleaned.reserve(refs.size());
|
||||
for (const std::string& ref : refs) {
|
||||
std::string trimmed = Trim(ref);
|
||||
if (!trimmed.empty()) cleaned.push_back(trimmed);
|
||||
}
|
||||
return JoinEscaped(cleaned, ';');
|
||||
}
|
||||
|
||||
inline std::string SerializeDialogueLines(const std::vector<DialogueLine>& lines) {
|
||||
std::vector<std::string> encodedLines;
|
||||
encodedLines.reserve(lines.size());
|
||||
for (const DialogueLine& line : lines) {
|
||||
std::vector<std::string> fields;
|
||||
fields.reserve(14);
|
||||
fields.push_back(line.characterName);
|
||||
fields.push_back(line.sentence);
|
||||
fields.push_back(line.sentenceGerman);
|
||||
fields.push_back(line.sentenceJapaneseKana);
|
||||
fields.push_back(line.characterSoundClip);
|
||||
fields.push_back(std::to_string(line.typingSpeed));
|
||||
fields.push_back(std::to_string(static_cast<int>(line.textEffect)));
|
||||
fields.push_back(std::to_string(line.animationSpeed));
|
||||
fields.push_back(std::to_string(line.effectIntensity));
|
||||
fields.push_back(line.notTalkingObjectRef);
|
||||
fields.push_back(line.openMouthObjectRef);
|
||||
fields.push_back(line.closedMouthObjectRef);
|
||||
fields.push_back(SerializeObjectRefs(line.itemsToEnable));
|
||||
fields.push_back(SerializeObjectRefs(line.itemsToDisable));
|
||||
encodedLines.push_back(JoinEscaped(fields, '|'));
|
||||
}
|
||||
// Use tab as the outer delimiter so values stay single-line in scene files.
|
||||
return JoinEscaped(encodedLines, '\t');
|
||||
}
|
||||
|
||||
inline std::vector<DialogueLine> DeserializeDialogueLines(const std::string& encoded) {
|
||||
std::vector<DialogueLine> lines;
|
||||
if (encoded.empty()) return lines;
|
||||
|
||||
std::vector<std::string> rawLines = SplitEscaped(encoded, '\t');
|
||||
// Backward compatibility for older saved data that used newline delimiters.
|
||||
if (rawLines.size() <= 1 && encoded.find('\t') == std::string::npos &&
|
||||
encoded.find('\n') != std::string::npos) {
|
||||
rawLines = SplitEscaped(encoded, '\n');
|
||||
}
|
||||
lines.reserve(rawLines.size());
|
||||
|
||||
for (const std::string& rawLine : rawLines) {
|
||||
if (Trim(rawLine).empty()) continue;
|
||||
std::vector<std::string> fields = SplitEscaped(rawLine, '|');
|
||||
if (fields.empty()) continue;
|
||||
|
||||
DialogueLine line;
|
||||
if (fields.size() > 0) line.characterName = fields[0];
|
||||
if (fields.size() > 1) line.sentence = fields[1];
|
||||
if (fields.size() > 2) line.sentenceGerman = fields[2];
|
||||
if (fields.size() > 3) line.sentenceJapaneseKana = fields[3];
|
||||
if (fields.size() > 4) line.characterSoundClip = fields[4];
|
||||
if (fields.size() > 5) line.typingSpeed = ParseFloat(fields[5], line.typingSpeed);
|
||||
if (fields.size() > 6) line.textEffect = static_cast<TextEffectType>(ParseInt(fields[6], 0));
|
||||
if (fields.size() > 7) line.animationSpeed = ParseFloat(fields[7], line.animationSpeed);
|
||||
if (fields.size() > 8) line.effectIntensity = ParseFloat(fields[8], line.effectIntensity);
|
||||
if (fields.size() > 9) line.notTalkingObjectRef = fields[9];
|
||||
if (fields.size() > 10) line.openMouthObjectRef = fields[10];
|
||||
if (fields.size() > 11) line.closedMouthObjectRef = fields[11];
|
||||
if (fields.size() > 12) line.itemsToEnable = DeserializeObjectRefs(fields[12]);
|
||||
if (fields.size() > 13) line.itemsToDisable = DeserializeObjectRefs(fields[13]);
|
||||
|
||||
lines.push_back(std::move(line));
|
||||
}
|
||||
|
||||
return lines;
|
||||
}
|
||||
|
||||
inline std::string MakeObjectRef(int objectId) {
|
||||
return std::string("Object.ID-") + std::to_string(objectId);
|
||||
}
|
||||
|
||||
inline bool IsAllDigits(const std::string& value) {
|
||||
if (value.empty()) return false;
|
||||
size_t start = (value[0] == '-' || value[0] == '+') ? 1 : 0;
|
||||
if (start >= value.size()) return false;
|
||||
for (size_t i = start; i < value.size(); ++i) {
|
||||
if (!std::isdigit(static_cast<unsigned char>(value[i]))) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
inline SceneObject* ResolveSceneObjectRef(ScriptContext& ctx, const std::string& objectRef) {
|
||||
std::string trimmed = Trim(objectRef);
|
||||
if (trimmed.empty()) return nullptr;
|
||||
|
||||
if (SceneObject* resolved = ctx.ResolveObjectRef(trimmed)) {
|
||||
return resolved;
|
||||
}
|
||||
|
||||
if (IsAllDigits(trimmed)) {
|
||||
return ctx.FindObjectById(ParseInt(trimmed, -1));
|
||||
}
|
||||
|
||||
return ctx.FindObjectByName(trimmed);
|
||||
}
|
||||
|
||||
inline bool SetObjectEnabledState(ScriptContext& ctx, SceneObject* object, bool enabled) {
|
||||
if (!object) return false;
|
||||
if (object->enabled == enabled) return false;
|
||||
object->enabled = enabled;
|
||||
ctx.MarkDirty();
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool SetObjectsEnabledState(ScriptContext& ctx, const std::vector<std::string>& refs, bool enabled) {
|
||||
bool changed = false;
|
||||
for (const std::string& ref : refs) {
|
||||
SceneObject* object = ResolveSceneObjectRef(ctx, ref);
|
||||
if (SetObjectEnabledState(ctx, object, enabled)) {
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
inline bool SetUITextLabel(ScriptContext& ctx, const std::string& objectRef, const std::string& label) {
|
||||
SceneObject* object = ResolveSceneObjectRef(ctx, objectRef);
|
||||
if (!object || !HasUIComponent(*object) || object->ui.type != UIElementType::Text) return false;
|
||||
if (object->ui.label == label) return false;
|
||||
object->ui.label = label;
|
||||
ctx.MarkDirty();
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool SetUITextEffects(ScriptContext& ctx,
|
||||
const std::string& objectRef,
|
||||
TextEffectType effect,
|
||||
float animationSpeed,
|
||||
float effectIntensity) {
|
||||
SceneObject* object = ResolveSceneObjectRef(ctx, objectRef);
|
||||
if (!object || !HasUIComponent(*object) || object->ui.type != UIElementType::Text) return false;
|
||||
|
||||
const int flags = static_cast<int>(effect);
|
||||
const float speed = std::max(0.01f, animationSpeed);
|
||||
const float intensity = std::max(0.0f, effectIntensity);
|
||||
|
||||
bool changed = false;
|
||||
if (object->ui.textEffectFlags != flags) {
|
||||
object->ui.textEffectFlags = flags;
|
||||
changed = true;
|
||||
}
|
||||
if (std::abs(object->ui.textEffectSpeed - speed) > 1e-5f) {
|
||||
object->ui.textEffectSpeed = speed;
|
||||
changed = true;
|
||||
}
|
||||
if (std::abs(object->ui.textEffectIntensity - intensity) > 1e-5f) {
|
||||
object->ui.textEffectIntensity = intensity;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
ctx.MarkDirty();
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
inline bool SetRigidbody2DSimulated(ScriptContext& ctx, const std::string& objectRef, bool simulated) {
|
||||
SceneObject* object = ResolveSceneObjectRef(ctx, objectRef);
|
||||
if (!object || !object->hasRigidbody2D) return false;
|
||||
if (object->rigidbody2D.enabled == simulated) return false;
|
||||
object->rigidbody2D.enabled = simulated;
|
||||
ctx.MarkDirty();
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool DrawStdStringInput(const char* label, std::string& value,
|
||||
size_t capacity = 512,
|
||||
ImGuiInputTextFlags flags = 0,
|
||||
bool multiline = false,
|
||||
float multilineHeight = 90.0f) {
|
||||
std::vector<char> buffer(capacity + 1, '\0');
|
||||
std::snprintf(buffer.data(), buffer.size(), "%s", value.c_str());
|
||||
bool edited = false;
|
||||
if (multiline) {
|
||||
edited = ImGui::InputTextMultiline(label, buffer.data(), buffer.size(),
|
||||
ImVec2(-FLT_MIN, multilineHeight), flags);
|
||||
} else {
|
||||
edited = ImGui::InputText(label, buffer.data(), buffer.size(), flags);
|
||||
}
|
||||
if (edited) {
|
||||
value = buffer.data();
|
||||
}
|
||||
return edited;
|
||||
}
|
||||
|
||||
inline bool DrawObjectRefInput(ScriptContext& ctx, const char* label, std::string& objectRef) {
|
||||
bool changed = DrawStdStringInput(label, objectRef, 256);
|
||||
|
||||
if (ImGui::BeginDragDropTarget()) {
|
||||
if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("SCENE_OBJECT")) {
|
||||
if (payload->Data && payload->DataSize == static_cast<int>(sizeof(int))) {
|
||||
const int droppedId = *static_cast<const int*>(payload->Data);
|
||||
const std::string newRef = MakeObjectRef(droppedId);
|
||||
if (objectRef != newRef) {
|
||||
objectRef = newRef;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
ImGui::EndDragDropTarget();
|
||||
}
|
||||
|
||||
SceneObject* resolved = ResolveSceneObjectRef(ctx, objectRef);
|
||||
if (resolved) {
|
||||
ImGui::TextDisabled("-> %s (id=%d)", resolved->name.c_str(), resolved->id);
|
||||
} else if (!Trim(objectRef).empty()) {
|
||||
ImGui::TextDisabled("-> unresolved");
|
||||
}
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
inline bool IsAudioClipPath(const std::string& path) {
|
||||
const size_t dot = path.find_last_of('.');
|
||||
if (dot == std::string::npos || dot + 1 >= path.size()) return false;
|
||||
std::string ext = path.substr(dot + 1);
|
||||
std::transform(ext.begin(), ext.end(), ext.begin(), [](unsigned char c) {
|
||||
return static_cast<char>(std::tolower(c));
|
||||
});
|
||||
return ext == "wav" || ext == "mp3" || ext == "ogg" || ext == "flac" ||
|
||||
ext == "aac" || ext == "m4a";
|
||||
}
|
||||
|
||||
inline bool DrawAudioClipInput(const char* label, std::string& clipPath, size_t capacity = 512) {
|
||||
bool changed = DrawStdStringInput(label, clipPath, capacity);
|
||||
if (ImGui::BeginDragDropTarget()) {
|
||||
if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("FILE_PATH")) {
|
||||
if (payload->Data && payload->DataSize > 0) {
|
||||
const char* droppedPath = static_cast<const char*>(payload->Data);
|
||||
if (droppedPath) {
|
||||
std::string candidate = droppedPath;
|
||||
if (IsAudioClipPath(candidate) && clipPath != candidate) {
|
||||
clipPath = std::move(candidate);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ImGui::EndDragDropTarget();
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
inline bool DrawObjectRefListEditor(ScriptContext& ctx, const char* label, std::vector<std::string>& refs) {
|
||||
bool changed = false;
|
||||
if (!ImGui::TreeNode(label)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < refs.size(); ++i) {
|
||||
ImGui::PushID(static_cast<int>(i));
|
||||
ImGui::SetNextItemWidth(-80.0f);
|
||||
changed |= DrawStdStringInput("##ref", refs[i], 256);
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("X")) {
|
||||
refs.erase(refs.begin() + static_cast<std::ptrdiff_t>(i));
|
||||
changed = true;
|
||||
ImGui::PopID();
|
||||
--i;
|
||||
continue;
|
||||
}
|
||||
|
||||
SceneObject* resolved = ResolveSceneObjectRef(ctx, refs[i]);
|
||||
if (resolved) {
|
||||
ImGui::TextDisabled("%s (id=%d)", resolved->name.c_str(), resolved->id);
|
||||
}
|
||||
|
||||
if (ImGui::BeginDragDropTarget()) {
|
||||
if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("SCENE_OBJECT")) {
|
||||
if (payload->Data && payload->DataSize == static_cast<int>(sizeof(int))) {
|
||||
const int droppedId = *static_cast<const int*>(payload->Data);
|
||||
refs[i] = MakeObjectRef(droppedId);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
ImGui::EndDragDropTarget();
|
||||
}
|
||||
|
||||
ImGui::PopID();
|
||||
}
|
||||
|
||||
if (ImGui::Button("Add Reference")) {
|
||||
refs.emplace_back();
|
||||
changed = true;
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Add Selected")) {
|
||||
const int selectedId = ctx.GetSelectedObjectId();
|
||||
if (selectedId >= 0) {
|
||||
refs.push_back(MakeObjectRef(selectedId));
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (ImGui::BeginDragDropTarget()) {
|
||||
if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("SCENE_OBJECT")) {
|
||||
if (payload->Data && payload->DataSize == static_cast<int>(sizeof(int))) {
|
||||
const int droppedId = *static_cast<const int*>(payload->Data);
|
||||
refs.push_back(MakeObjectRef(droppedId));
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
ImGui::EndDragDropTarget();
|
||||
}
|
||||
|
||||
ImGui::TreePop();
|
||||
return changed;
|
||||
}
|
||||
|
||||
inline const char* LanguageLabel(Language language) {
|
||||
switch (language) {
|
||||
case Language::English: return "English";
|
||||
case Language::German: return "German";
|
||||
case Language::JapaneseKana: return "Japanese Kana";
|
||||
default: return "English";
|
||||
}
|
||||
}
|
||||
|
||||
inline bool DrawLanguageCombo(const char* label, Language& language) {
|
||||
bool changed = false;
|
||||
if (ImGui::BeginCombo(label, LanguageLabel(language))) {
|
||||
std::array<Language, 3> values = {
|
||||
Language::English,
|
||||
Language::German,
|
||||
Language::JapaneseKana
|
||||
};
|
||||
for (Language value : values) {
|
||||
const bool selected = (language == value);
|
||||
if (ImGui::Selectable(LanguageLabel(value), selected)) {
|
||||
language = value;
|
||||
changed = true;
|
||||
}
|
||||
if (selected) ImGui::SetItemDefaultFocus();
|
||||
}
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
inline bool IsRuntimeKeyDown(int glfwKey, ImGuiKey imguiKey) {
|
||||
if (ImGui::IsKeyDown(imguiKey)) return true;
|
||||
GLFWwindow* window = glfwGetCurrentContext();
|
||||
if (!window) return false;
|
||||
return glfwGetKey(window, glfwKey) == GLFW_PRESS;
|
||||
}
|
||||
|
||||
} // namespace DialoguePort
|
||||
769
Scripts/DialogueSystem.cpp
Normal file
769
Scripts/DialogueSystem.cpp
Normal file
@@ -0,0 +1,769 @@
|
||||
#include "DialoguePortShared.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cmath>
|
||||
#include <regex>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
using namespace DialoguePort;
|
||||
|
||||
struct DialogueConfig {
|
||||
std::string characterNameTextRef;
|
||||
std::string dialogueTextRef;
|
||||
std::string playerRef;
|
||||
|
||||
Language currentLanguage = Language::English;
|
||||
float pitchVariation = 0.1f;
|
||||
float openAnimationDelay = 0.3f;
|
||||
float closeAnimationDelay = 0.3f;
|
||||
float spacingFactor = 0.1f;
|
||||
float sizeMultiplier = 2.0f;
|
||||
|
||||
std::string characterSoundClip;
|
||||
std::string triggerSoundClip;
|
||||
std::string enterSoundClip;
|
||||
std::string exitSoundClip;
|
||||
std::string skipSoundClip;
|
||||
|
||||
bool rigidSimulated = true;
|
||||
bool disableSelfOnEnd = true;
|
||||
bool autoOpenOnBegin = false;
|
||||
|
||||
std::vector<std::string> itemsToEnable;
|
||||
std::vector<std::string> itemsToDisable;
|
||||
std::vector<DialogueLine> dialogueLines;
|
||||
};
|
||||
|
||||
struct DialogueRuntimeState {
|
||||
bool running = false;
|
||||
bool isTyping = false;
|
||||
bool isTextFullyDisplayed = false;
|
||||
bool autoOpened = false;
|
||||
|
||||
int index = 0;
|
||||
size_t revealedCharacters = 0;
|
||||
float typeAccumulator = 0.0f;
|
||||
float holdDelay = 0.0f;
|
||||
float effectTimer = 0.0f;
|
||||
float soundCooldown = 0.0f;
|
||||
float openDelayRemaining = 0.0f;
|
||||
float closeDelayRemaining = 0.0f;
|
||||
bool pendingClose = false;
|
||||
|
||||
int lastInteractionRequestSerial = 0;
|
||||
bool prevSubmitDown = false;
|
||||
|
||||
std::string currentCleanSentence;
|
||||
std::string currentPlayerRef;
|
||||
|
||||
std::vector<DialogueLine> activeLines;
|
||||
std::vector<std::string> endItemsToEnable;
|
||||
std::vector<std::string> endItemsToDisable;
|
||||
};
|
||||
|
||||
std::unordered_map<int, DialogueRuntimeState> g_runtimeStates;
|
||||
std::unordered_map<int, int> g_selectedLineByObject;
|
||||
|
||||
constexpr const char* kSettingCharacterNameRef = "dialogue.characterNameTextRef";
|
||||
constexpr const char* kSettingDialogueTextRef = "dialogue.dialogueTextRef";
|
||||
constexpr const char* kSettingPlayerRef = "dialogue.playerRef";
|
||||
constexpr const char* kSettingLanguage = "dialogue.language";
|
||||
constexpr const char* kSettingPitchVariation = "dialogue.pitchVariation";
|
||||
constexpr const char* kSettingOpenDelay = "dialogue.openAnimationDelay";
|
||||
constexpr const char* kSettingCloseDelay = "dialogue.closeAnimationDelay";
|
||||
constexpr const char* kSettingSpacingFactor = "dialogue.spacingFactor";
|
||||
constexpr const char* kSettingSizeMultiplier = "dialogue.sizeMultiplier";
|
||||
constexpr const char* kSettingCharacterSound = "dialogue.characterSoundClip";
|
||||
constexpr const char* kSettingTriggerSound = "dialogue.triggerSoundClip";
|
||||
constexpr const char* kSettingEnterSound = "dialogue.enterSoundClip";
|
||||
constexpr const char* kSettingExitSound = "dialogue.exitSoundClip";
|
||||
constexpr const char* kSettingSkipSound = "dialogue.skipSoundClip";
|
||||
constexpr const char* kSettingRigidSimulated = "dialogue.rigidSimulated";
|
||||
constexpr const char* kSettingDisableOnEnd = "dialogue.disableSelfOnEnd";
|
||||
constexpr const char* kSettingAutoOpenOnBegin = "dialogue.autoOpenOnBegin";
|
||||
constexpr const char* kSettingItemsEnable = "dialogue.itemsToEnable";
|
||||
constexpr const char* kSettingItemsDisable = "dialogue.itemsToDisable";
|
||||
constexpr const char* kSettingLines = "dialogue.lines";
|
||||
|
||||
constexpr const char* kInteractionRequestSerial = "interaction.requestSerial";
|
||||
constexpr const char* kInteractionRequestPending = "interaction.requestPending";
|
||||
constexpr const char* kInteractionOverrideLines = "interaction.overrideDialogueLines";
|
||||
constexpr const char* kInteractionEndEnable = "interaction.itemsToEnableOnEnd";
|
||||
constexpr const char* kInteractionEndDisable = "interaction.itemsToDisableOnEnd";
|
||||
constexpr const char* kInteractionPlayerRef = "interaction.playerRef";
|
||||
|
||||
bool setSettingIfChanged(ScriptContext& ctx, const std::string& key, const std::string& value) {
|
||||
if (ctx.GetSetting(key, "") == value) return false;
|
||||
ctx.SetSetting(key, value);
|
||||
return true;
|
||||
}
|
||||
|
||||
DialogueConfig loadConfig(ScriptContext& ctx) {
|
||||
DialogueConfig config;
|
||||
|
||||
config.characterNameTextRef = ctx.GetSetting(kSettingCharacterNameRef, "");
|
||||
config.dialogueTextRef = ctx.GetSetting(kSettingDialogueTextRef, "");
|
||||
config.playerRef = ctx.GetSetting(kSettingPlayerRef, "");
|
||||
|
||||
config.currentLanguage = static_cast<Language>(
|
||||
std::clamp(ParseInt(ctx.GetSetting(kSettingLanguage, "0"), 0), 0, 2));
|
||||
|
||||
config.pitchVariation = ParseFloat(ctx.GetSetting(kSettingPitchVariation, "0.1"), 0.1f);
|
||||
config.openAnimationDelay = std::max(0.0f, ParseFloat(ctx.GetSetting(kSettingOpenDelay, "0.3"), 0.3f));
|
||||
config.closeAnimationDelay = std::max(0.0f, ParseFloat(ctx.GetSetting(kSettingCloseDelay, "0.3"), 0.3f));
|
||||
config.spacingFactor = ParseFloat(ctx.GetSetting(kSettingSpacingFactor, "0.1"), 0.1f);
|
||||
config.sizeMultiplier = std::max(0.1f, ParseFloat(ctx.GetSetting(kSettingSizeMultiplier, "2.0"), 2.0f));
|
||||
|
||||
config.characterSoundClip = ctx.GetSetting(kSettingCharacterSound, "");
|
||||
config.triggerSoundClip = ctx.GetSetting(kSettingTriggerSound, "");
|
||||
config.enterSoundClip = ctx.GetSetting(kSettingEnterSound, "");
|
||||
config.exitSoundClip = ctx.GetSetting(kSettingExitSound, "");
|
||||
config.skipSoundClip = ctx.GetSetting(kSettingSkipSound, "");
|
||||
|
||||
config.rigidSimulated = ParseBool(ctx.GetSetting(kSettingRigidSimulated, "1"), true);
|
||||
config.disableSelfOnEnd = ParseBool(ctx.GetSetting(kSettingDisableOnEnd, "1"), true);
|
||||
config.autoOpenOnBegin = ParseBool(ctx.GetSetting(kSettingAutoOpenOnBegin, "0"), false);
|
||||
|
||||
config.itemsToEnable = DeserializeObjectRefs(ctx.GetSetting(kSettingItemsEnable, ""));
|
||||
config.itemsToDisable = DeserializeObjectRefs(ctx.GetSetting(kSettingItemsDisable, ""));
|
||||
config.dialogueLines = DeserializeDialogueLines(ctx.GetSetting(kSettingLines, ""));
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
void saveConfig(ScriptContext& ctx, const DialogueConfig& config) {
|
||||
setSettingIfChanged(ctx, kSettingCharacterNameRef, config.characterNameTextRef);
|
||||
setSettingIfChanged(ctx, kSettingDialogueTextRef, config.dialogueTextRef);
|
||||
setSettingIfChanged(ctx, kSettingPlayerRef, config.playerRef);
|
||||
|
||||
setSettingIfChanged(ctx, kSettingLanguage, std::to_string(static_cast<int>(config.currentLanguage)));
|
||||
setSettingIfChanged(ctx, kSettingPitchVariation, std::to_string(config.pitchVariation));
|
||||
setSettingIfChanged(ctx, kSettingOpenDelay, std::to_string(config.openAnimationDelay));
|
||||
setSettingIfChanged(ctx, kSettingCloseDelay, std::to_string(config.closeAnimationDelay));
|
||||
setSettingIfChanged(ctx, kSettingSpacingFactor, std::to_string(config.spacingFactor));
|
||||
setSettingIfChanged(ctx, kSettingSizeMultiplier, std::to_string(config.sizeMultiplier));
|
||||
|
||||
setSettingIfChanged(ctx, kSettingCharacterSound, config.characterSoundClip);
|
||||
setSettingIfChanged(ctx, kSettingTriggerSound, config.triggerSoundClip);
|
||||
setSettingIfChanged(ctx, kSettingEnterSound, config.enterSoundClip);
|
||||
setSettingIfChanged(ctx, kSettingExitSound, config.exitSoundClip);
|
||||
setSettingIfChanged(ctx, kSettingSkipSound, config.skipSoundClip);
|
||||
|
||||
setSettingIfChanged(ctx, kSettingRigidSimulated, config.rigidSimulated ? "1" : "0");
|
||||
setSettingIfChanged(ctx, kSettingDisableOnEnd, config.disableSelfOnEnd ? "1" : "0");
|
||||
setSettingIfChanged(ctx, kSettingAutoOpenOnBegin, config.autoOpenOnBegin ? "1" : "0");
|
||||
|
||||
setSettingIfChanged(ctx, kSettingItemsEnable, SerializeObjectRefs(config.itemsToEnable));
|
||||
setSettingIfChanged(ctx, kSettingItemsDisable, SerializeObjectRefs(config.itemsToDisable));
|
||||
setSettingIfChanged(ctx, kSettingLines, SerializeDialogueLines(config.dialogueLines));
|
||||
}
|
||||
|
||||
bool hasDialogueLines(const std::vector<DialogueLine>& lines) {
|
||||
return !lines.empty();
|
||||
}
|
||||
|
||||
std::string getSentenceForLanguage(const DialogueLine& line, Language language) {
|
||||
switch (language) {
|
||||
case Language::English:
|
||||
return line.sentence;
|
||||
case Language::German:
|
||||
return line.sentenceGerman.empty() ? line.sentence : line.sentenceGerman;
|
||||
case Language::JapaneseKana:
|
||||
return line.sentenceJapaneseKana.empty() ? line.sentence : line.sentenceJapaneseKana;
|
||||
default:
|
||||
return line.sentence;
|
||||
}
|
||||
}
|
||||
|
||||
std::string parseSentenceForDisplay(const std::string& sentence) {
|
||||
static const std::regex taggedWordPattern(R"(\(\[(\w+),\s*([\d.]+),\s*([\d.]+),\](.*?)\))");
|
||||
|
||||
std::string clean;
|
||||
clean.reserve(sentence.size());
|
||||
|
||||
size_t lastIndex = 0;
|
||||
for (std::sregex_iterator it(sentence.begin(), sentence.end(), taggedWordPattern), end; it != end; ++it) {
|
||||
const std::smatch& match = *it;
|
||||
const size_t matchPos = static_cast<size_t>(match.position());
|
||||
const size_t matchLen = static_cast<size_t>(match.length());
|
||||
|
||||
if (matchPos > lastIndex) {
|
||||
clean += sentence.substr(lastIndex, matchPos - lastIndex);
|
||||
}
|
||||
|
||||
if (match.size() >= 5) {
|
||||
clean += match[4].str();
|
||||
}
|
||||
|
||||
lastIndex = matchPos + matchLen;
|
||||
}
|
||||
|
||||
if (lastIndex < sentence.size()) {
|
||||
clean += sentence.substr(lastIndex);
|
||||
}
|
||||
|
||||
return clean;
|
||||
}
|
||||
|
||||
std::string toLowerAscii(std::string value) {
|
||||
std::transform(value.begin(), value.end(), value.begin(), [](unsigned char c) {
|
||||
return static_cast<char>(std::tolower(c));
|
||||
});
|
||||
return value;
|
||||
}
|
||||
|
||||
int findAnimationClipIndexByName(SceneObject& object, const std::string& desiredName) {
|
||||
if (!object.hasAnimation || !object.animation.enabled) return -1;
|
||||
NormalizeAnimationClipSlots(object.animation);
|
||||
if (object.animation.clips.empty()) return -1;
|
||||
|
||||
const std::string needle = toLowerAscii(Trim(desiredName));
|
||||
for (int i = 0; i < static_cast<int>(object.animation.clips.size()); ++i) {
|
||||
const AnimationClipSlot& clip = object.animation.clips[static_cast<size_t>(i)];
|
||||
std::string clipName = clip.name.empty() ? AnimationClipNameFromPath(clip.assetPath) : clip.name;
|
||||
if (toLowerAscii(Trim(clipName)) == needle) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
bool playDialogueStateAnimation(ScriptContext& ctx, const std::string& clipName) {
|
||||
if (!ctx.object || !ctx.object->hasAnimation || !ctx.object->animation.enabled) return false;
|
||||
SceneObject& object = *ctx.object;
|
||||
int clipIndex = findAnimationClipIndexByName(object, clipName);
|
||||
if (clipIndex < 0) return false;
|
||||
|
||||
object.animation.activeClipIndex = clipIndex;
|
||||
object.animation.clipAssetPath = object.animation.clips[static_cast<size_t>(clipIndex)].assetPath;
|
||||
object.animation.runtimeClipPath.clear();
|
||||
object.animation.runtimeInitialized = false;
|
||||
object.animation.runtimePaused = false;
|
||||
object.animation.runtimeDirection = 1.0f;
|
||||
return ctx.PlayAnimation(true);
|
||||
}
|
||||
|
||||
enum class MouthState {
|
||||
TalkingOpen,
|
||||
TalkingClosed,
|
||||
NotTalking
|
||||
};
|
||||
|
||||
void applyMouthState(ScriptContext& ctx, const DialogueLine& line, MouthState mouthState) {
|
||||
const bool isOpen = (mouthState == MouthState::TalkingOpen);
|
||||
const bool isClosed = (mouthState == MouthState::TalkingClosed);
|
||||
const bool isIdle = (mouthState == MouthState::NotTalking);
|
||||
|
||||
SceneObject* openObj = ResolveSceneObjectRef(ctx, line.openMouthObjectRef);
|
||||
SceneObject* closedObj = ResolveSceneObjectRef(ctx, line.closedMouthObjectRef);
|
||||
SceneObject* idleObj = ResolveSceneObjectRef(ctx, line.notTalkingObjectRef);
|
||||
|
||||
SetObjectEnabledState(ctx, openObj, isOpen);
|
||||
SetObjectEnabledState(ctx, closedObj, isClosed);
|
||||
SetObjectEnabledState(ctx, idleObj, isIdle);
|
||||
}
|
||||
|
||||
void resetTextWidgets(ScriptContext& ctx, const DialogueConfig& config) {
|
||||
SetUITextLabel(ctx, config.characterNameTextRef, "");
|
||||
SetUITextLabel(ctx, config.dialogueTextRef, "");
|
||||
SetUITextEffects(ctx, config.dialogueTextRef, TextEffectType::None, 1.0f, 0.0f);
|
||||
}
|
||||
|
||||
void revealDialogueText(ScriptContext& ctx, const DialogueConfig& config, DialogueRuntimeState& state) {
|
||||
const size_t shown = std::min(state.revealedCharacters, state.currentCleanSentence.size());
|
||||
SetUITextLabel(ctx, config.dialogueTextRef, state.currentCleanSentence.substr(0, shown));
|
||||
}
|
||||
|
||||
void startLine(ScriptContext& ctx, DialogueRuntimeState& state, const DialogueConfig& config) {
|
||||
if (state.index < 0 || state.index >= static_cast<int>(state.activeLines.size())) {
|
||||
state.running = false;
|
||||
state.isTyping = false;
|
||||
state.isTextFullyDisplayed = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const DialogueLine& line = state.activeLines[static_cast<size_t>(state.index)];
|
||||
SetObjectsEnabledState(ctx, line.itemsToDisable, false);
|
||||
SetObjectsEnabledState(ctx, line.itemsToEnable, true);
|
||||
|
||||
SetUITextLabel(ctx, config.characterNameTextRef, line.characterName);
|
||||
|
||||
state.currentCleanSentence = parseSentenceForDisplay(getSentenceForLanguage(line, config.currentLanguage));
|
||||
state.revealedCharacters = 0;
|
||||
state.typeAccumulator = 0.0f;
|
||||
state.holdDelay = 0.0f;
|
||||
state.soundCooldown = 0.0f;
|
||||
state.isTyping = true;
|
||||
state.isTextFullyDisplayed = false;
|
||||
|
||||
revealDialogueText(ctx, config, state);
|
||||
SetUITextEffects(ctx, config.dialogueTextRef, line.textEffect, line.animationSpeed, line.effectIntensity);
|
||||
applyMouthState(ctx, line, MouthState::TalkingClosed);
|
||||
}
|
||||
|
||||
void completeTyping(ScriptContext& ctx, DialogueRuntimeState& state, const DialogueConfig& config) {
|
||||
if (state.index < 0 || state.index >= static_cast<int>(state.activeLines.size())) return;
|
||||
|
||||
const DialogueLine& line = state.activeLines[static_cast<size_t>(state.index)];
|
||||
state.revealedCharacters = state.currentCleanSentence.size();
|
||||
state.isTyping = false;
|
||||
state.isTextFullyDisplayed = true;
|
||||
state.holdDelay = 0.0f;
|
||||
state.typeAccumulator = 0.0f;
|
||||
|
||||
revealDialogueText(ctx, config, state);
|
||||
applyMouthState(ctx, line, MouthState::NotTalking);
|
||||
}
|
||||
|
||||
void finalizeDialogueEnd(ScriptContext& ctx, DialogueRuntimeState& state, const DialogueConfig& config) {
|
||||
SetObjectsEnabledState(ctx, state.endItemsToDisable, false);
|
||||
SetObjectsEnabledState(ctx, state.endItemsToEnable, true);
|
||||
|
||||
SetRigidbody2DSimulated(ctx, state.currentPlayerRef, config.rigidSimulated);
|
||||
|
||||
resetTextWidgets(ctx, config);
|
||||
|
||||
state.running = false;
|
||||
state.pendingClose = false;
|
||||
state.closeDelayRemaining = 0.0f;
|
||||
state.isTyping = false;
|
||||
state.isTextFullyDisplayed = false;
|
||||
state.revealedCharacters = 0;
|
||||
state.currentCleanSentence.clear();
|
||||
if (config.disableSelfOnEnd) {
|
||||
state.autoOpened = false;
|
||||
}
|
||||
|
||||
if (config.disableSelfOnEnd) {
|
||||
ctx.SetObjectEnabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
void beginDialogueClose(ScriptContext& ctx, DialogueRuntimeState& state, const DialogueConfig& config) {
|
||||
if (state.pendingClose) return;
|
||||
state.pendingClose = true;
|
||||
state.closeDelayRemaining = std::max(0.0f, config.closeAnimationDelay);
|
||||
playDialogueStateAnimation(ctx, "DialogueStateClose");
|
||||
|
||||
if (state.closeDelayRemaining <= 0.0f) {
|
||||
finalizeDialogueEnd(ctx, state, config);
|
||||
}
|
||||
}
|
||||
|
||||
void openDialogue(ScriptContext& ctx,
|
||||
DialogueRuntimeState& state,
|
||||
const DialogueConfig& config,
|
||||
const std::vector<DialogueLine>& lines,
|
||||
const std::vector<std::string>& itemsToEnableOnEnd,
|
||||
const std::vector<std::string>& itemsToDisableOnEnd,
|
||||
const std::string& playerRef) {
|
||||
if (!hasDialogueLines(lines)) {
|
||||
ctx.AddConsoleMessage("DialogueSystem: no dialogue lines configured.", ConsoleMessageType::Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
state.activeLines = lines;
|
||||
state.endItemsToEnable = itemsToEnableOnEnd;
|
||||
state.endItemsToDisable = itemsToDisableOnEnd;
|
||||
state.currentPlayerRef = playerRef.empty() ? config.playerRef : playerRef;
|
||||
|
||||
state.running = true;
|
||||
state.index = 0;
|
||||
state.revealedCharacters = 0;
|
||||
state.typeAccumulator = 0.0f;
|
||||
state.holdDelay = 0.0f;
|
||||
state.soundCooldown = 0.0f;
|
||||
state.effectTimer = 0.0f;
|
||||
state.isTyping = false;
|
||||
state.isTextFullyDisplayed = false;
|
||||
state.pendingClose = false;
|
||||
state.closeDelayRemaining = 0.0f;
|
||||
state.currentCleanSentence.clear();
|
||||
state.openDelayRemaining = config.openAnimationDelay;
|
||||
|
||||
resetTextWidgets(ctx, config);
|
||||
SetRigidbody2DSimulated(ctx, state.currentPlayerRef, false);
|
||||
playDialogueStateAnimation(ctx, "DialogueStateOpen");
|
||||
|
||||
if (!config.triggerSoundClip.empty()) {
|
||||
ctx.PlayAudioOneShot(config.triggerSoundClip);
|
||||
}
|
||||
|
||||
if (state.openDelayRemaining <= 0.0f) {
|
||||
startLine(ctx, state, config);
|
||||
}
|
||||
}
|
||||
|
||||
bool isSubmitDown() {
|
||||
return IsRuntimeKeyDown(GLFW_KEY_ENTER, ImGuiKey_Enter) ||
|
||||
IsRuntimeKeyDown(GLFW_KEY_KP_ENTER, ImGuiKey_KeypadEnter);
|
||||
}
|
||||
|
||||
void tickTyping(ScriptContext& ctx, DialogueRuntimeState& state, const DialogueConfig& config, float deltaTime) {
|
||||
if (!state.isTyping) return;
|
||||
if (state.index < 0 || state.index >= static_cast<int>(state.activeLines.size())) return;
|
||||
|
||||
const DialogueLine& line = state.activeLines[static_cast<size_t>(state.index)];
|
||||
|
||||
if (state.currentCleanSentence.empty()) {
|
||||
completeTyping(ctx, state, config);
|
||||
return;
|
||||
}
|
||||
|
||||
const float typingSpeed = std::max(0.001f, line.typingSpeed);
|
||||
state.effectTimer += deltaTime;
|
||||
state.soundCooldown = std::max(0.0f, state.soundCooldown - deltaTime);
|
||||
|
||||
float remaining = deltaTime;
|
||||
while (remaining > 0.0f && state.isTyping) {
|
||||
if (state.holdDelay > 0.0f) {
|
||||
const float consume = std::min(remaining, state.holdDelay);
|
||||
state.holdDelay -= consume;
|
||||
remaining -= consume;
|
||||
continue;
|
||||
}
|
||||
|
||||
state.typeAccumulator += remaining;
|
||||
remaining = 0.0f;
|
||||
|
||||
bool revealedAny = false;
|
||||
while (state.typeAccumulator >= typingSpeed && state.revealedCharacters < state.currentCleanSentence.size()) {
|
||||
state.typeAccumulator -= typingSpeed;
|
||||
++state.revealedCharacters;
|
||||
revealedAny = true;
|
||||
|
||||
const size_t charIndex = state.revealedCharacters - 1;
|
||||
const char character = state.currentCleanSentence[charIndex];
|
||||
|
||||
if (std::isalnum(static_cast<unsigned char>(character)) != 0) {
|
||||
const std::string& clip = line.characterSoundClip.empty() ? config.characterSoundClip : line.characterSoundClip;
|
||||
if (!clip.empty() && state.soundCooldown <= 0.0f) {
|
||||
ctx.PlayAudioOneShot(clip);
|
||||
state.soundCooldown = std::max(0.01f, typingSpeed * 0.6f);
|
||||
}
|
||||
}
|
||||
|
||||
if ((charIndex % 2U) == 0U) {
|
||||
applyMouthState(ctx, line, MouthState::TalkingOpen);
|
||||
} else {
|
||||
applyMouthState(ctx, line, MouthState::TalkingClosed);
|
||||
}
|
||||
|
||||
if (character == ',' || character == ';') {
|
||||
state.holdDelay += typingSpeed * 3.0f;
|
||||
} else if (character == '.' || character == '!' || character == '?') {
|
||||
if (character == '.' && charIndex + 2 < state.currentCleanSentence.size() &&
|
||||
state.currentCleanSentence[charIndex + 1] == '.' &&
|
||||
state.currentCleanSentence[charIndex + 2] == '.') {
|
||||
state.holdDelay += typingSpeed * 5.0f;
|
||||
} else {
|
||||
state.holdDelay += typingSpeed * 5.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (revealedAny) {
|
||||
revealDialogueText(ctx, config, state);
|
||||
}
|
||||
|
||||
if (state.revealedCharacters >= state.currentCleanSentence.size()) {
|
||||
completeTyping(ctx, state, config);
|
||||
break;
|
||||
}
|
||||
|
||||
if (state.typeAccumulator < typingSpeed) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void drawTextEffectEditor(TextEffectType& effect) {
|
||||
int flags = static_cast<int>(effect);
|
||||
|
||||
bool wave = (flags & static_cast<int>(TextEffectType::Wave)) != 0;
|
||||
bool shake = (flags & static_cast<int>(TextEffectType::Shake)) != 0;
|
||||
bool bounce = (flags & static_cast<int>(TextEffectType::Bounce)) != 0;
|
||||
bool rotate = (flags & static_cast<int>(TextEffectType::Rotate)) != 0;
|
||||
bool fade = (flags & static_cast<int>(TextEffectType::Fade)) != 0;
|
||||
|
||||
if (ImGui::Checkbox("Wave", &wave)) {
|
||||
if (wave) flags |= static_cast<int>(TextEffectType::Wave);
|
||||
else flags &= ~static_cast<int>(TextEffectType::Wave);
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Checkbox("Shake", &shake)) {
|
||||
if (shake) flags |= static_cast<int>(TextEffectType::Shake);
|
||||
else flags &= ~static_cast<int>(TextEffectType::Shake);
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Checkbox("Bounce", &bounce)) {
|
||||
if (bounce) flags |= static_cast<int>(TextEffectType::Bounce);
|
||||
else flags &= ~static_cast<int>(TextEffectType::Bounce);
|
||||
}
|
||||
|
||||
if (ImGui::Checkbox("Rotate", &rotate)) {
|
||||
if (rotate) flags |= static_cast<int>(TextEffectType::Rotate);
|
||||
else flags &= ~static_cast<int>(TextEffectType::Rotate);
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Checkbox("Fade", &fade)) {
|
||||
if (fade) flags |= static_cast<int>(TextEffectType::Fade);
|
||||
else flags &= ~static_cast<int>(TextEffectType::Fade);
|
||||
}
|
||||
|
||||
effect = static_cast<TextEffectType>(flags);
|
||||
}
|
||||
|
||||
bool drawDialogueLineEditor(ScriptContext& ctx, std::vector<DialogueLine>& lines, int& selectedIndex) {
|
||||
bool changed = false;
|
||||
|
||||
if (ImGui::Button("Add Line")) {
|
||||
if (!lines.empty()) {
|
||||
lines.push_back(lines.back());
|
||||
} else {
|
||||
lines.emplace_back();
|
||||
}
|
||||
selectedIndex = static_cast<int>(lines.size()) - 1;
|
||||
changed = true;
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Remove Line") && selectedIndex >= 0 && selectedIndex < static_cast<int>(lines.size())) {
|
||||
lines.erase(lines.begin() + static_cast<std::ptrdiff_t>(selectedIndex));
|
||||
if (lines.empty()) selectedIndex = -1;
|
||||
else selectedIndex = std::clamp(selectedIndex, 0, static_cast<int>(lines.size()) - 1);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (lines.empty()) {
|
||||
ImGui::TextDisabled("No dialogue lines configured.");
|
||||
return changed;
|
||||
}
|
||||
|
||||
selectedIndex = std::clamp(selectedIndex, 0, static_cast<int>(lines.size()) - 1);
|
||||
|
||||
ImGui::BeginChild("DialogueLinesList", ImVec2(230.0f, 220.0f), true);
|
||||
for (size_t i = 0; i < lines.size(); ++i) {
|
||||
std::string label = std::to_string(i + 1) + ". " +
|
||||
(lines[i].characterName.empty() ? std::string("<Unnamed>") : lines[i].characterName);
|
||||
if (ImGui::Selectable(label.c_str(), selectedIndex == static_cast<int>(i))) {
|
||||
selectedIndex = static_cast<int>(i);
|
||||
}
|
||||
}
|
||||
ImGui::EndChild();
|
||||
|
||||
ImGui::SameLine();
|
||||
ImGui::BeginGroup();
|
||||
DialogueLine& line = lines[static_cast<size_t>(selectedIndex)];
|
||||
|
||||
changed |= DrawStdStringInput("Character Name", line.characterName, 256);
|
||||
changed |= DrawStdStringInput("Sentence (EN)", line.sentence, 2048, 0, true, 60.0f);
|
||||
changed |= DrawStdStringInput("Sentence (DE)", line.sentenceGerman, 2048, 0, true, 60.0f);
|
||||
changed |= DrawStdStringInput("Sentence (JP Kana)", line.sentenceJapaneseKana, 2048, 0, true, 60.0f);
|
||||
|
||||
changed |= DrawAudioClipInput("Character Voice Clip", line.characterSoundClip, 512);
|
||||
changed |= ImGui::DragFloat("Typing Speed", &line.typingSpeed, 0.001f, 0.001f, 1.0f, "%.3f");
|
||||
changed |= ImGui::DragFloat("Animation Speed", &line.animationSpeed, 0.01f, 0.01f, 10.0f, "%.2f");
|
||||
changed |= ImGui::DragFloat("Effect Intensity", &line.effectIntensity, 0.01f, 0.0f, 10.0f, "%.2f");
|
||||
|
||||
ImGui::TextUnformatted("Text Effects");
|
||||
TextEffectType before = line.textEffect;
|
||||
drawTextEffectEditor(line.textEffect);
|
||||
changed |= (before != line.textEffect);
|
||||
|
||||
changed |= DrawObjectRefInput(ctx, "Not Talking Object", line.notTalkingObjectRef);
|
||||
changed |= DrawObjectRefInput(ctx, "Open Mouth Object", line.openMouthObjectRef);
|
||||
changed |= DrawObjectRefInput(ctx, "Closed Mouth Object", line.closedMouthObjectRef);
|
||||
|
||||
changed |= DrawObjectRefListEditor(ctx, "Line Items To Enable", line.itemsToEnable);
|
||||
changed |= DrawObjectRefListEditor(ctx, "Line Items To Disable", line.itemsToDisable);
|
||||
|
||||
ImGui::EndGroup();
|
||||
return changed;
|
||||
}
|
||||
|
||||
void drawRuntimeStatus(const DialogueRuntimeState* state) {
|
||||
if (!state) {
|
||||
ImGui::TextDisabled("Runtime: idle");
|
||||
return;
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
ImGui::Text("Runtime");
|
||||
ImGui::TextDisabled("Running: %s", state->running ? "Yes" : "No");
|
||||
ImGui::TextDisabled("Typing: %s", state->isTyping ? "Yes" : "No");
|
||||
ImGui::TextDisabled("Line: %d", state->index + 1);
|
||||
ImGui::TextDisabled("Chars: %zu / %zu", state->revealedCharacters, state->currentCleanSentence.size());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" void Script_OnInspector(ScriptContext& ctx) {
|
||||
DialogueConfig config = loadConfig(ctx);
|
||||
bool changed = false;
|
||||
|
||||
ImGui::TextUnformatted("DialogueSystem (Unity Port)");
|
||||
ImGui::Separator();
|
||||
|
||||
changed |= DrawObjectRefInput(ctx, "Character Name Text Ref", config.characterNameTextRef);
|
||||
changed |= DrawObjectRefInput(ctx, "Dialogue Text Ref", config.dialogueTextRef);
|
||||
changed |= DrawObjectRefInput(ctx, "Player Ref", config.playerRef);
|
||||
|
||||
changed |= DrawLanguageCombo("Language", config.currentLanguage);
|
||||
changed |= ImGui::DragFloat("Pitch Variation", &config.pitchVariation, 0.01f, 0.0f, 2.0f, "%.2f");
|
||||
changed |= ImGui::DragFloat("Open Animation Delay", &config.openAnimationDelay, 0.01f, 0.0f, 10.0f, "%.2f");
|
||||
changed |= ImGui::DragFloat("Close Animation Delay", &config.closeAnimationDelay, 0.01f, 0.0f, 10.0f, "%.2f");
|
||||
changed |= ImGui::DragFloat("Spacing Factor", &config.spacingFactor, 0.01f, 0.0f, 2.0f, "%.2f");
|
||||
changed |= ImGui::DragFloat("Size Multiplier", &config.sizeMultiplier, 0.01f, 0.1f, 10.0f, "%.2f");
|
||||
|
||||
changed |= DrawAudioClipInput("Character Sound Clip", config.characterSoundClip, 512);
|
||||
changed |= DrawAudioClipInput("Trigger Sound Clip", config.triggerSoundClip, 512);
|
||||
changed |= DrawAudioClipInput("Enter Sound Clip", config.enterSoundClip, 512);
|
||||
changed |= DrawAudioClipInput("Exit Sound Clip", config.exitSoundClip, 512);
|
||||
changed |= DrawAudioClipInput("Skip Sound Clip", config.skipSoundClip, 512);
|
||||
|
||||
changed |= ImGui::Checkbox("Player Simulated On End", &config.rigidSimulated);
|
||||
changed |= ImGui::Checkbox("Disable Self On End", &config.disableSelfOnEnd);
|
||||
changed |= ImGui::Checkbox("Auto Open On Begin", &config.autoOpenOnBegin);
|
||||
|
||||
changed |= DrawObjectRefListEditor(ctx, "Items To Enable On End", config.itemsToEnable);
|
||||
changed |= DrawObjectRefListEditor(ctx, "Items To Disable On End", config.itemsToDisable);
|
||||
|
||||
ImGui::Separator();
|
||||
ImGui::TextUnformatted("Dialogue Lines");
|
||||
|
||||
const int objectId = ctx.object ? ctx.object->id : -1;
|
||||
int& selectedLine = g_selectedLineByObject[objectId];
|
||||
changed |= drawDialogueLineEditor(ctx, config.dialogueLines, selectedLine);
|
||||
|
||||
if (changed) {
|
||||
saveConfig(ctx, config);
|
||||
}
|
||||
|
||||
auto stateIt = g_runtimeStates.find(objectId);
|
||||
drawRuntimeStatus(stateIt != g_runtimeStates.end() ? &stateIt->second : nullptr);
|
||||
}
|
||||
|
||||
void Begin(ScriptContext& ctx, float /*deltaTime*/) {
|
||||
if (!ctx.object) return;
|
||||
|
||||
DialogueConfig config = loadConfig(ctx);
|
||||
DialogueRuntimeState& state = g_runtimeStates[ctx.object->id];
|
||||
|
||||
state.running = false;
|
||||
state.isTyping = false;
|
||||
state.isTextFullyDisplayed = false;
|
||||
state.autoOpened = false;
|
||||
state.index = 0;
|
||||
state.revealedCharacters = 0;
|
||||
state.typeAccumulator = 0.0f;
|
||||
state.holdDelay = 0.0f;
|
||||
state.effectTimer = 0.0f;
|
||||
state.soundCooldown = 0.0f;
|
||||
state.openDelayRemaining = 0.0f;
|
||||
state.closeDelayRemaining = 0.0f;
|
||||
state.pendingClose = false;
|
||||
state.prevSubmitDown = false;
|
||||
const int interactionSerial = ParseInt(ctx.GetSetting(kInteractionRequestSerial, "0"), 0);
|
||||
const bool interactionPending = ParseBool(ctx.GetSetting(kInteractionRequestPending, "0"), false);
|
||||
state.lastInteractionRequestSerial = interactionPending ? (interactionSerial - 1) : interactionSerial;
|
||||
state.currentCleanSentence.clear();
|
||||
state.currentPlayerRef = config.playerRef;
|
||||
state.activeLines.clear();
|
||||
state.endItemsToEnable.clear();
|
||||
state.endItemsToDisable.clear();
|
||||
|
||||
resetTextWidgets(ctx, config);
|
||||
|
||||
if (config.autoOpenOnBegin && hasDialogueLines(config.dialogueLines)) {
|
||||
openDialogue(ctx, state, config, config.dialogueLines, config.itemsToEnable, config.itemsToDisable, config.playerRef);
|
||||
state.autoOpened = true;
|
||||
}
|
||||
}
|
||||
|
||||
void TickUpdate(ScriptContext& ctx, float deltaTime) {
|
||||
if (!ctx.object) return;
|
||||
|
||||
DialogueConfig config = loadConfig(ctx);
|
||||
DialogueRuntimeState& state = g_runtimeStates[ctx.object->id];
|
||||
|
||||
const int interactionSerial = ParseInt(ctx.GetSetting(kInteractionRequestSerial, "0"), 0);
|
||||
const bool interactionPending = ParseBool(ctx.GetSetting(kInteractionRequestPending, "0"), false);
|
||||
if (interactionSerial != state.lastInteractionRequestSerial || interactionPending) {
|
||||
state.lastInteractionRequestSerial = interactionSerial;
|
||||
if (interactionPending) {
|
||||
ctx.SetSetting(kInteractionRequestPending, "0");
|
||||
}
|
||||
|
||||
std::vector<DialogueLine> overrideLines = DeserializeDialogueLines(ctx.GetSetting(kInteractionOverrideLines, ""));
|
||||
std::vector<std::string> overrideEnable = DeserializeObjectRefs(ctx.GetSetting(kInteractionEndEnable, ""));
|
||||
std::vector<std::string> overrideDisable = DeserializeObjectRefs(ctx.GetSetting(kInteractionEndDisable, ""));
|
||||
std::string overridePlayerRef = ctx.GetSetting(kInteractionPlayerRef, "");
|
||||
|
||||
if (overrideLines.empty()) overrideLines = config.dialogueLines;
|
||||
if (overrideEnable.empty()) overrideEnable = config.itemsToEnable;
|
||||
if (overrideDisable.empty()) overrideDisable = config.itemsToDisable;
|
||||
if (overridePlayerRef.empty()) overridePlayerRef = config.playerRef;
|
||||
|
||||
openDialogue(ctx, state, config, overrideLines, overrideEnable, overrideDisable, overridePlayerRef);
|
||||
}
|
||||
|
||||
if (!state.running && config.autoOpenOnBegin && !state.autoOpened && hasDialogueLines(config.dialogueLines)) {
|
||||
openDialogue(ctx, state, config, config.dialogueLines, config.itemsToEnable, config.itemsToDisable, config.playerRef);
|
||||
state.autoOpened = true;
|
||||
}
|
||||
|
||||
if (!state.running) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.pendingClose) {
|
||||
state.closeDelayRemaining -= std::max(0.0f, deltaTime);
|
||||
if (state.closeDelayRemaining <= 0.0f) {
|
||||
finalizeDialogueEnd(ctx, state, config);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const bool submitDown = isSubmitDown();
|
||||
const bool submitPressed = submitDown && !state.prevSubmitDown;
|
||||
state.prevSubmitDown = submitDown;
|
||||
|
||||
if (state.openDelayRemaining > 0.0f) {
|
||||
state.openDelayRemaining -= std::max(0.0f, deltaTime);
|
||||
if (state.openDelayRemaining <= 0.0f) {
|
||||
if (!config.enterSoundClip.empty()) {
|
||||
ctx.PlayAudioOneShot(config.enterSoundClip);
|
||||
}
|
||||
startLine(ctx, state, config);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
tickTyping(ctx, state, config, std::max(0.0f, deltaTime));
|
||||
|
||||
if (submitPressed) {
|
||||
if (state.isTyping) {
|
||||
completeTyping(ctx, state, config);
|
||||
if (!config.skipSoundClip.empty()) {
|
||||
ctx.PlayAudioOneShot(config.skipSoundClip);
|
||||
}
|
||||
} else if (state.isTextFullyDisplayed) {
|
||||
if (state.index < static_cast<int>(state.activeLines.size()) - 1) {
|
||||
++state.index;
|
||||
if (!config.enterSoundClip.empty()) {
|
||||
ctx.PlayAudioOneShot(config.enterSoundClip);
|
||||
}
|
||||
startLine(ctx, state, config);
|
||||
} else {
|
||||
if (!config.exitSoundClip.empty()) {
|
||||
ctx.PlayAudioOneShot(config.exitSoundClip);
|
||||
}
|
||||
beginDialogueClose(ctx, state, config);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
687
Scripts/InteractableObject.cpp
Normal file
687
Scripts/InteractableObject.cpp
Normal file
@@ -0,0 +1,687 @@
|
||||
#include "DialoguePortShared.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
using namespace DialoguePort;
|
||||
|
||||
enum class InteractionType {
|
||||
Dialogue = 0,
|
||||
ToggleObjects = 1
|
||||
};
|
||||
|
||||
struct InteractionOption {
|
||||
std::string optionName = "New Option";
|
||||
InteractionType interactionType = InteractionType::Dialogue;
|
||||
|
||||
std::string dialogueSystemRef;
|
||||
std::vector<DialogueLine> dialogueLines;
|
||||
std::vector<std::string> dialogueItemsToEnableOnEnd;
|
||||
std::vector<std::string> dialogueItemsToDisableOnEnd;
|
||||
|
||||
std::vector<std::string> itemsToEnable;
|
||||
std::vector<std::string> itemsToDisable;
|
||||
};
|
||||
|
||||
struct InteractableConfig {
|
||||
bool canInteract = true;
|
||||
bool oneTimeUse = false;
|
||||
int selectedOptionIndex = 0;
|
||||
std::vector<InteractionOption> options;
|
||||
|
||||
std::string selectionNameOverride;
|
||||
bool isSelected = false;
|
||||
std::vector<std::string> selectedStateEnable;
|
||||
std::vector<std::string> selectedStateDisable;
|
||||
|
||||
bool interactOnKeyPress = true;
|
||||
bool requirePlayerInRange = false;
|
||||
std::string playerRef;
|
||||
float interactDistance = 2.5f;
|
||||
bool debugRange = false;
|
||||
};
|
||||
|
||||
struct InteractableRuntimeState {
|
||||
bool prevInteractDown = false;
|
||||
bool hasSelectionState = false;
|
||||
bool lastSelectionState = false;
|
||||
float lastRangeDistance = -1.0f;
|
||||
bool lastRangeInRange = false;
|
||||
bool lastRangeHasPlayer = false;
|
||||
glm::vec3 lastPlayerWorldPos = glm::vec3(0.0f);
|
||||
glm::vec3 lastSelfWorldPos = glm::vec3(0.0f);
|
||||
};
|
||||
|
||||
std::unordered_map<int, InteractableRuntimeState> g_runtimeStates;
|
||||
std::unordered_map<int, int> g_selectedOptionEditorIndex;
|
||||
std::unordered_map<int, int> g_selectedDialogueLineEditorIndex;
|
||||
|
||||
constexpr const char* kSettingCanInteract = "interactable.canInteract";
|
||||
constexpr const char* kSettingOneTimeUse = "interactable.oneTimeUse";
|
||||
constexpr const char* kSettingSelectedOption = "interactable.selectedOptionIndex";
|
||||
constexpr const char* kSettingOptions = "interactable.options";
|
||||
constexpr const char* kSettingSelectionName = "interactable.selectionNameOverride";
|
||||
constexpr const char* kSettingIsSelected = "interactable.isSelected";
|
||||
constexpr const char* kSettingSelectedEnable = "interactable.selectedStateEnable";
|
||||
constexpr const char* kSettingSelectedDisable = "interactable.selectedStateDisable";
|
||||
constexpr const char* kSettingInteractOnKey = "interactable.interactOnKeyPress";
|
||||
constexpr const char* kSettingRequireRange = "interactable.requirePlayerInRange";
|
||||
constexpr const char* kSettingPlayerRef = "interactable.playerRef";
|
||||
constexpr const char* kSettingDistance = "interactable.interactDistance";
|
||||
constexpr const char* kSettingDebugRange = "interactable.debugRange";
|
||||
|
||||
constexpr const char* kInteractionRequestSerial = "interaction.requestSerial";
|
||||
constexpr const char* kInteractionRequestPending = "interaction.requestPending";
|
||||
constexpr const char* kInteractionOverrideLines = "interaction.overrideDialogueLines";
|
||||
constexpr const char* kInteractionEndEnable = "interaction.itemsToEnableOnEnd";
|
||||
constexpr const char* kInteractionEndDisable = "interaction.itemsToDisableOnEnd";
|
||||
constexpr const char* kInteractionPlayerRef = "interaction.playerRef";
|
||||
|
||||
bool setSettingIfChanged(ScriptContext& ctx, const std::string& key, const std::string& value) {
|
||||
if (ctx.GetSetting(key, "") == value) return false;
|
||||
ctx.SetSetting(key, value);
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string serializeOptions(const std::vector<InteractionOption>& options) {
|
||||
std::vector<std::string> encodedOptions;
|
||||
encodedOptions.reserve(options.size());
|
||||
for (const InteractionOption& option : options) {
|
||||
std::vector<std::string> fields;
|
||||
fields.reserve(8);
|
||||
fields.push_back(option.optionName);
|
||||
fields.push_back(std::to_string(static_cast<int>(option.interactionType)));
|
||||
fields.push_back(option.dialogueSystemRef);
|
||||
fields.push_back(SerializeDialogueLines(option.dialogueLines));
|
||||
fields.push_back(SerializeObjectRefs(option.dialogueItemsToEnableOnEnd));
|
||||
fields.push_back(SerializeObjectRefs(option.dialogueItemsToDisableOnEnd));
|
||||
fields.push_back(SerializeObjectRefs(option.itemsToEnable));
|
||||
fields.push_back(SerializeObjectRefs(option.itemsToDisable));
|
||||
encodedOptions.push_back(JoinEscaped(fields, '|'));
|
||||
}
|
||||
// Keep serialized options single-line for scene save parsing.
|
||||
return JoinEscaped(encodedOptions, '\t');
|
||||
}
|
||||
|
||||
std::vector<InteractionOption> deserializeOptions(const std::string& encoded) {
|
||||
std::vector<InteractionOption> options;
|
||||
if (encoded.empty()) return options;
|
||||
|
||||
std::vector<std::string> rawOptions = SplitEscaped(encoded, '\t');
|
||||
if (rawOptions.size() <= 1 && encoded.find('\t') == std::string::npos &&
|
||||
encoded.find('\n') != std::string::npos) {
|
||||
rawOptions = SplitEscaped(encoded, '\n');
|
||||
}
|
||||
options.reserve(rawOptions.size());
|
||||
|
||||
for (const std::string& raw : rawOptions) {
|
||||
if (Trim(raw).empty()) continue;
|
||||
|
||||
std::vector<std::string> fields = SplitEscaped(raw, '|');
|
||||
if (fields.empty()) continue;
|
||||
|
||||
InteractionOption option;
|
||||
if (fields.size() > 0) option.optionName = fields[0];
|
||||
if (fields.size() > 1) {
|
||||
option.interactionType = static_cast<InteractionType>(
|
||||
std::clamp(ParseInt(fields[1], 0), 0, 1));
|
||||
}
|
||||
if (fields.size() > 2) option.dialogueSystemRef = fields[2];
|
||||
if (fields.size() > 3) option.dialogueLines = DeserializeDialogueLines(fields[3]);
|
||||
if (fields.size() > 4) option.dialogueItemsToEnableOnEnd = DeserializeObjectRefs(fields[4]);
|
||||
if (fields.size() > 5) option.dialogueItemsToDisableOnEnd = DeserializeObjectRefs(fields[5]);
|
||||
if (fields.size() > 6) option.itemsToEnable = DeserializeObjectRefs(fields[6]);
|
||||
if (fields.size() > 7) option.itemsToDisable = DeserializeObjectRefs(fields[7]);
|
||||
|
||||
options.push_back(std::move(option));
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
InteractableConfig loadConfig(ScriptContext& ctx) {
|
||||
InteractableConfig config;
|
||||
config.canInteract = ParseBool(ctx.GetSetting(kSettingCanInteract, "1"), true);
|
||||
config.oneTimeUse = ParseBool(ctx.GetSetting(kSettingOneTimeUse, "0"), false);
|
||||
config.selectedOptionIndex = std::max(0, ParseInt(ctx.GetSetting(kSettingSelectedOption, "0"), 0));
|
||||
config.options = deserializeOptions(ctx.GetSetting(kSettingOptions, ""));
|
||||
|
||||
config.selectionNameOverride = ctx.GetSetting(kSettingSelectionName, "");
|
||||
config.isSelected = ParseBool(ctx.GetSetting(kSettingIsSelected, "0"), false);
|
||||
config.selectedStateEnable = DeserializeObjectRefs(ctx.GetSetting(kSettingSelectedEnable, ""));
|
||||
config.selectedStateDisable = DeserializeObjectRefs(ctx.GetSetting(kSettingSelectedDisable, ""));
|
||||
|
||||
config.interactOnKeyPress = ParseBool(ctx.GetSetting(kSettingInteractOnKey, "1"), true);
|
||||
config.requirePlayerInRange = ParseBool(ctx.GetSetting(kSettingRequireRange, "0"), false);
|
||||
config.playerRef = ctx.GetSetting(kSettingPlayerRef, "");
|
||||
config.interactDistance = std::max(0.0f, ParseFloat(ctx.GetSetting(kSettingDistance, "2.5"), 2.5f));
|
||||
config.debugRange = ParseBool(ctx.GetSetting(kSettingDebugRange, "0"), false);
|
||||
return config;
|
||||
}
|
||||
|
||||
void saveConfig(ScriptContext& ctx, const InteractableConfig& config) {
|
||||
setSettingIfChanged(ctx, kSettingCanInteract, config.canInteract ? "1" : "0");
|
||||
setSettingIfChanged(ctx, kSettingOneTimeUse, config.oneTimeUse ? "1" : "0");
|
||||
setSettingIfChanged(ctx, kSettingSelectedOption, std::to_string(std::max(0, config.selectedOptionIndex)));
|
||||
setSettingIfChanged(ctx, kSettingOptions, serializeOptions(config.options));
|
||||
|
||||
setSettingIfChanged(ctx, kSettingSelectionName, config.selectionNameOverride);
|
||||
setSettingIfChanged(ctx, kSettingIsSelected, config.isSelected ? "1" : "0");
|
||||
setSettingIfChanged(ctx, kSettingSelectedEnable, SerializeObjectRefs(config.selectedStateEnable));
|
||||
setSettingIfChanged(ctx, kSettingSelectedDisable, SerializeObjectRefs(config.selectedStateDisable));
|
||||
|
||||
setSettingIfChanged(ctx, kSettingInteractOnKey, config.interactOnKeyPress ? "1" : "0");
|
||||
setSettingIfChanged(ctx, kSettingRequireRange, config.requirePlayerInRange ? "1" : "0");
|
||||
setSettingIfChanged(ctx, kSettingPlayerRef, config.playerRef);
|
||||
setSettingIfChanged(ctx, kSettingDistance, std::to_string(config.interactDistance));
|
||||
setSettingIfChanged(ctx, kSettingDebugRange, config.debugRange ? "1" : "0");
|
||||
}
|
||||
|
||||
bool canInteract(const InteractableConfig& config) {
|
||||
return config.canInteract && !config.options.empty();
|
||||
}
|
||||
|
||||
const char* interactionTypeLabel(InteractionType type) {
|
||||
switch (type) {
|
||||
case InteractionType::Dialogue: return "Dialogue";
|
||||
case InteractionType::ToggleObjects: return "Toggle Objects";
|
||||
default: return "Dialogue";
|
||||
}
|
||||
}
|
||||
|
||||
bool drawInteractionTypeCombo(InteractionType& type) {
|
||||
bool changed = false;
|
||||
if (ImGui::BeginCombo("Interaction Type", interactionTypeLabel(type))) {
|
||||
const bool dialogueSelected = (type == InteractionType::Dialogue);
|
||||
if (ImGui::Selectable("Dialogue", dialogueSelected)) {
|
||||
type = InteractionType::Dialogue;
|
||||
changed = true;
|
||||
}
|
||||
if (dialogueSelected) ImGui::SetItemDefaultFocus();
|
||||
|
||||
const bool toggleSelected = (type == InteractionType::ToggleObjects);
|
||||
if (ImGui::Selectable("Toggle Objects", toggleSelected)) {
|
||||
type = InteractionType::ToggleObjects;
|
||||
changed = true;
|
||||
}
|
||||
if (toggleSelected) ImGui::SetItemDefaultFocus();
|
||||
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
void drawTextEffectEditor(TextEffectType& effect) {
|
||||
int flags = static_cast<int>(effect);
|
||||
|
||||
bool wave = (flags & static_cast<int>(TextEffectType::Wave)) != 0;
|
||||
bool shake = (flags & static_cast<int>(TextEffectType::Shake)) != 0;
|
||||
bool bounce = (flags & static_cast<int>(TextEffectType::Bounce)) != 0;
|
||||
bool rotate = (flags & static_cast<int>(TextEffectType::Rotate)) != 0;
|
||||
bool fade = (flags & static_cast<int>(TextEffectType::Fade)) != 0;
|
||||
|
||||
if (ImGui::Checkbox("Wave", &wave)) {
|
||||
if (wave) flags |= static_cast<int>(TextEffectType::Wave);
|
||||
else flags &= ~static_cast<int>(TextEffectType::Wave);
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Checkbox("Shake", &shake)) {
|
||||
if (shake) flags |= static_cast<int>(TextEffectType::Shake);
|
||||
else flags &= ~static_cast<int>(TextEffectType::Shake);
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Checkbox("Bounce", &bounce)) {
|
||||
if (bounce) flags |= static_cast<int>(TextEffectType::Bounce);
|
||||
else flags &= ~static_cast<int>(TextEffectType::Bounce);
|
||||
}
|
||||
|
||||
if (ImGui::Checkbox("Rotate", &rotate)) {
|
||||
if (rotate) flags |= static_cast<int>(TextEffectType::Rotate);
|
||||
else flags &= ~static_cast<int>(TextEffectType::Rotate);
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Checkbox("Fade", &fade)) {
|
||||
if (fade) flags |= static_cast<int>(TextEffectType::Fade);
|
||||
else flags &= ~static_cast<int>(TextEffectType::Fade);
|
||||
}
|
||||
|
||||
effect = static_cast<TextEffectType>(flags);
|
||||
}
|
||||
|
||||
bool drawDialogueLineEditor(ScriptContext& ctx, std::vector<DialogueLine>& lines, int& selectedIndex) {
|
||||
bool changed = false;
|
||||
|
||||
if (ImGui::Button("Add Dialogue Line")) {
|
||||
if (!lines.empty()) {
|
||||
lines.push_back(lines.back());
|
||||
} else {
|
||||
lines.emplace_back();
|
||||
}
|
||||
selectedIndex = static_cast<int>(lines.size()) - 1;
|
||||
changed = true;
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Remove Dialogue Line") && selectedIndex >= 0 && selectedIndex < static_cast<int>(lines.size())) {
|
||||
lines.erase(lines.begin() + static_cast<std::ptrdiff_t>(selectedIndex));
|
||||
if (lines.empty()) selectedIndex = -1;
|
||||
else selectedIndex = std::clamp(selectedIndex, 0, static_cast<int>(lines.size()) - 1);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (lines.empty()) {
|
||||
ImGui::TextDisabled("No override dialogue lines configured.");
|
||||
return changed;
|
||||
}
|
||||
|
||||
selectedIndex = std::clamp(selectedIndex, 0, static_cast<int>(lines.size()) - 1);
|
||||
|
||||
ImGui::BeginChild("DialogueLinesList", ImVec2(230.0f, 220.0f), true);
|
||||
for (size_t i = 0; i < lines.size(); ++i) {
|
||||
std::string label = std::to_string(i + 1) + ". " +
|
||||
(lines[i].characterName.empty() ? std::string("<Unnamed>") : lines[i].characterName);
|
||||
if (ImGui::Selectable(label.c_str(), selectedIndex == static_cast<int>(i))) {
|
||||
selectedIndex = static_cast<int>(i);
|
||||
}
|
||||
}
|
||||
ImGui::EndChild();
|
||||
|
||||
ImGui::SameLine();
|
||||
ImGui::BeginGroup();
|
||||
DialogueLine& line = lines[static_cast<size_t>(selectedIndex)];
|
||||
|
||||
changed |= DrawStdStringInput("Character Name", line.characterName, 256);
|
||||
changed |= DrawStdStringInput("Sentence (EN)", line.sentence, 2048, 0, true, 60.0f);
|
||||
changed |= DrawStdStringInput("Sentence (DE)", line.sentenceGerman, 2048, 0, true, 60.0f);
|
||||
changed |= DrawStdStringInput("Sentence (JP Kana)", line.sentenceJapaneseKana, 2048, 0, true, 60.0f);
|
||||
|
||||
changed |= DrawAudioClipInput("Character Voice Clip", line.characterSoundClip, 512);
|
||||
changed |= ImGui::DragFloat("Typing Speed", &line.typingSpeed, 0.001f, 0.001f, 1.0f, "%.3f");
|
||||
changed |= ImGui::DragFloat("Animation Speed", &line.animationSpeed, 0.01f, 0.01f, 10.0f, "%.2f");
|
||||
changed |= ImGui::DragFloat("Effect Intensity", &line.effectIntensity, 0.01f, 0.0f, 10.0f, "%.2f");
|
||||
|
||||
ImGui::TextUnformatted("Text Effects");
|
||||
TextEffectType before = line.textEffect;
|
||||
drawTextEffectEditor(line.textEffect);
|
||||
changed |= (before != line.textEffect);
|
||||
|
||||
changed |= DrawObjectRefInput(ctx, "Not Talking Object", line.notTalkingObjectRef);
|
||||
changed |= DrawObjectRefInput(ctx, "Open Mouth Object", line.openMouthObjectRef);
|
||||
changed |= DrawObjectRefInput(ctx, "Closed Mouth Object", line.closedMouthObjectRef);
|
||||
|
||||
changed |= DrawObjectRefListEditor(ctx, "Line Items To Enable", line.itemsToEnable);
|
||||
changed |= DrawObjectRefListEditor(ctx, "Line Items To Disable", line.itemsToDisable);
|
||||
|
||||
ImGui::EndGroup();
|
||||
return changed;
|
||||
}
|
||||
|
||||
void applySelectedState(ScriptContext& ctx, const InteractableConfig& config) {
|
||||
SetObjectsEnabledState(ctx, config.selectedStateEnable, config.isSelected);
|
||||
SetObjectsEnabledState(ctx, config.selectedStateDisable, !config.isSelected);
|
||||
}
|
||||
|
||||
bool isInteractDown() {
|
||||
return IsRuntimeKeyDown(GLFW_KEY_E, ImGuiKey_E);
|
||||
}
|
||||
|
||||
glm::vec3 getObjectReferencePosition(ScriptContext& ctx, const SceneObject& object) {
|
||||
if (HasUIComponent(object) && object.ui.type != UIElementType::None) {
|
||||
glm::vec2 worldUiPos(object.ui.position.x, object.ui.position.y);
|
||||
int parentId = object.parentId;
|
||||
int guard = 0;
|
||||
while (parentId >= 0 && guard < 256) {
|
||||
++guard;
|
||||
SceneObject* parent = ctx.FindObjectById(parentId);
|
||||
if (!parent) break;
|
||||
if (HasUIComponent(*parent) && parent->ui.type != UIElementType::None) {
|
||||
worldUiPos += parent->ui.position;
|
||||
parentId = parent->parentId;
|
||||
continue;
|
||||
}
|
||||
worldUiPos += glm::vec2(parent->position.x, parent->position.y);
|
||||
break;
|
||||
}
|
||||
return glm::vec3(worldUiPos.x, worldUiPos.y, object.position.z);
|
||||
}
|
||||
return object.position;
|
||||
}
|
||||
|
||||
bool isPlayerInRange(ScriptContext& ctx, const InteractableConfig& config,
|
||||
InteractableRuntimeState* runtimeState = nullptr) {
|
||||
if (!ctx.object) return false;
|
||||
|
||||
SceneObject* player = ResolveSceneObjectRef(ctx, config.playerRef);
|
||||
if (!player) {
|
||||
if (runtimeState) {
|
||||
runtimeState->lastRangeHasPlayer = false;
|
||||
runtimeState->lastRangeInRange = false;
|
||||
runtimeState->lastRangeDistance = -1.0f;
|
||||
runtimeState->lastSelfWorldPos = getObjectReferencePosition(ctx, *ctx.object);
|
||||
runtimeState->lastPlayerWorldPos = glm::vec3(0.0f);
|
||||
}
|
||||
return !config.requirePlayerInRange;
|
||||
}
|
||||
|
||||
const glm::vec3 selfPos = getObjectReferencePosition(ctx, *ctx.object);
|
||||
const glm::vec3 playerPos = getObjectReferencePosition(ctx, *player);
|
||||
const glm::vec3 delta = playerPos - selfPos;
|
||||
const float range = std::max(0.0f, config.interactDistance);
|
||||
const float distanceSq = glm::dot(delta, delta);
|
||||
const bool inRange = distanceSq <= (range * range);
|
||||
|
||||
if (runtimeState) {
|
||||
runtimeState->lastRangeHasPlayer = true;
|
||||
runtimeState->lastRangeInRange = inRange;
|
||||
runtimeState->lastRangeDistance = std::sqrt(std::max(0.0f, distanceSq));
|
||||
runtimeState->lastSelfWorldPos = selfPos;
|
||||
runtimeState->lastPlayerWorldPos = playerPos;
|
||||
}
|
||||
|
||||
return !config.requirePlayerInRange || inRange;
|
||||
}
|
||||
|
||||
ScriptComponent* findDialogueScript(SceneObject* object) {
|
||||
if (!object) return nullptr;
|
||||
|
||||
for (ScriptComponent& script : object->scripts) {
|
||||
if (script.path.find("DialogueSystem") != std::string::npos) {
|
||||
return &script;
|
||||
}
|
||||
}
|
||||
|
||||
for (ScriptComponent& script : object->scripts) {
|
||||
if (script.language == ScriptLanguage::Cpp) {
|
||||
return &script;
|
||||
}
|
||||
}
|
||||
|
||||
if (!object->scripts.empty()) {
|
||||
return &object->scripts.front();
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool executeDialogueOption(ScriptContext& ctx,
|
||||
const InteractionOption& option,
|
||||
const InteractableConfig& config) {
|
||||
SceneObject* dialogueObject = ResolveSceneObjectRef(ctx, option.dialogueSystemRef);
|
||||
if (!dialogueObject) {
|
||||
ctx.AddConsoleMessage("InteractableObject: dialogue option is missing a valid DialogueSystem object reference.",
|
||||
ConsoleMessageType::Warning);
|
||||
return false;
|
||||
}
|
||||
|
||||
ScriptComponent* dialogueScript = findDialogueScript(dialogueObject);
|
||||
if (!dialogueScript) {
|
||||
ctx.AddConsoleMessage("InteractableObject: target DialogueSystem object has no script component.",
|
||||
ConsoleMessageType::Warning);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool changed = false;
|
||||
changed |= SetScriptSetting(dialogueScript, kInteractionRequestPending, "1");
|
||||
changed |= SetScriptSetting(dialogueScript, kInteractionOverrideLines, SerializeDialogueLines(option.dialogueLines));
|
||||
changed |= SetScriptSetting(dialogueScript, kInteractionEndEnable, SerializeObjectRefs(option.dialogueItemsToEnableOnEnd));
|
||||
changed |= SetScriptSetting(dialogueScript, kInteractionEndDisable, SerializeObjectRefs(option.dialogueItemsToDisableOnEnd));
|
||||
changed |= SetScriptSetting(dialogueScript, kInteractionPlayerRef, config.playerRef);
|
||||
|
||||
const int currentSerial = ParseInt(GetScriptSetting(dialogueScript, kInteractionRequestSerial, "0"), 0);
|
||||
changed |= SetScriptSetting(dialogueScript, kInteractionRequestSerial, std::to_string(currentSerial + 1));
|
||||
|
||||
if (!dialogueObject->enabled) {
|
||||
dialogueObject->enabled = true;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
ctx.MarkDirty();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool executeOption(ScriptContext& ctx,
|
||||
const InteractionOption& option,
|
||||
const InteractableConfig& config) {
|
||||
if (option.interactionType == InteractionType::Dialogue) {
|
||||
return executeDialogueOption(ctx, option, config);
|
||||
}
|
||||
|
||||
SetObjectsEnabledState(ctx, option.itemsToEnable, true);
|
||||
SetObjectsEnabledState(ctx, option.itemsToDisable, false);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool interact(ScriptContext& ctx, InteractableConfig& config, InteractableRuntimeState* runtimeState = nullptr) {
|
||||
if (!canInteract(config)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isPlayerInRange(ctx, config, runtimeState)) {
|
||||
if (config.requirePlayerInRange && config.debugRange) {
|
||||
if (runtimeState && runtimeState->lastRangeHasPlayer) {
|
||||
char msg[256];
|
||||
std::snprintf(msg, sizeof(msg),
|
||||
"InteractableObject: out of range (distance %.2f > %.2f).",
|
||||
runtimeState->lastRangeDistance,
|
||||
std::max(0.0f, config.interactDistance));
|
||||
ctx.AddConsoleMessage(msg, ConsoleMessageType::Info);
|
||||
} else {
|
||||
ctx.AddConsoleMessage("InteractableObject: player reference is missing/unresolved for range check.",
|
||||
ConsoleMessageType::Warning);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const int optionIndex = std::clamp(config.selectedOptionIndex, 0,
|
||||
static_cast<int>(config.options.size()) - 1);
|
||||
config.selectedOptionIndex = optionIndex;
|
||||
const InteractionOption& option = config.options[static_cast<size_t>(optionIndex)];
|
||||
|
||||
const bool executed = executeOption(ctx, option, config);
|
||||
if (executed && config.oneTimeUse) {
|
||||
config.canInteract = false;
|
||||
}
|
||||
|
||||
return executed;
|
||||
}
|
||||
|
||||
const char* selectionName(const InteractableConfig& config, const SceneObject* object) {
|
||||
if (!Trim(config.selectionNameOverride).empty()) {
|
||||
return config.selectionNameOverride.c_str();
|
||||
}
|
||||
if (object) return object->name.c_str();
|
||||
return "Interactable";
|
||||
}
|
||||
|
||||
void drawRuntimeStatus(const InteractableConfig& config, const InteractableRuntimeState* runtimeState) {
|
||||
ImGui::Separator();
|
||||
ImGui::TextUnformatted("Runtime");
|
||||
ImGui::TextDisabled("Can Interact: %s", config.canInteract ? "Yes" : "No");
|
||||
ImGui::TextDisabled("Options: %zu", config.options.size());
|
||||
ImGui::TextDisabled("Selected Option: %d", config.selectedOptionIndex);
|
||||
if (!runtimeState) return;
|
||||
if (config.requirePlayerInRange || config.debugRange) {
|
||||
if (!runtimeState->lastRangeHasPlayer) {
|
||||
ImGui::TextDisabled("Range: player not found");
|
||||
} else {
|
||||
ImGui::TextDisabled("Range Distance: %.2f / %.2f",
|
||||
runtimeState->lastRangeDistance,
|
||||
std::max(0.0f, config.interactDistance));
|
||||
ImGui::TextDisabled("In Range: %s", runtimeState->lastRangeInRange ? "Yes" : "No");
|
||||
if (config.debugRange) {
|
||||
ImGui::TextDisabled("Self Pos: (%.2f, %.2f, %.2f)",
|
||||
runtimeState->lastSelfWorldPos.x,
|
||||
runtimeState->lastSelfWorldPos.y,
|
||||
runtimeState->lastSelfWorldPos.z);
|
||||
ImGui::TextDisabled("Player Pos: (%.2f, %.2f, %.2f)",
|
||||
runtimeState->lastPlayerWorldPos.x,
|
||||
runtimeState->lastPlayerWorldPos.y,
|
||||
runtimeState->lastPlayerWorldPos.z);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" void Script_OnInspector(ScriptContext& ctx) {
|
||||
InteractableConfig config = loadConfig(ctx);
|
||||
bool changed = false;
|
||||
const int objectId = ctx.object ? ctx.object->id : -1;
|
||||
InteractableRuntimeState& runtimeState = g_runtimeStates[objectId];
|
||||
isPlayerInRange(ctx, config, &runtimeState);
|
||||
|
||||
ImGui::TextUnformatted("InteractableObject (Unity Port)");
|
||||
ImGui::Separator();
|
||||
|
||||
ImGui::TextDisabled("Selection Name: %s", selectionName(config, ctx.object));
|
||||
|
||||
changed |= ImGui::Checkbox("Can Interact", &config.canInteract);
|
||||
changed |= ImGui::Checkbox("One Time Use", &config.oneTimeUse);
|
||||
changed |= ImGui::Checkbox("Interact On E", &config.interactOnKeyPress);
|
||||
changed |= ImGui::Checkbox("Require Player In Range", &config.requirePlayerInRange);
|
||||
changed |= ImGui::DragFloat("Interaction Distance", &config.interactDistance, 0.05f, 0.0f, 100.0f, "%.2f");
|
||||
changed |= ImGui::Checkbox("Debug Range", &config.debugRange);
|
||||
|
||||
changed |= DrawObjectRefInput(ctx, "Player Ref", config.playerRef);
|
||||
changed |= DrawStdStringInput("Selection Name Override", config.selectionNameOverride, 256);
|
||||
|
||||
if (ImGui::Checkbox("Is Selected", &config.isSelected)) {
|
||||
applySelectedState(ctx, config);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
changed |= DrawObjectRefListEditor(ctx, "Selected State Enable", config.selectedStateEnable);
|
||||
changed |= DrawObjectRefListEditor(ctx, "Selected State Disable", config.selectedStateDisable);
|
||||
|
||||
if (ImGui::Button("Interact Now")) {
|
||||
if (interact(ctx, config, &runtimeState)) {
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
ImGui::TextUnformatted("Interaction Options");
|
||||
|
||||
if (ImGui::Button("Add Option")) {
|
||||
config.options.emplace_back();
|
||||
const int objectId = ctx.object ? ctx.object->id : -1;
|
||||
g_selectedOptionEditorIndex[objectId] = static_cast<int>(config.options.size()) - 1;
|
||||
changed = true;
|
||||
}
|
||||
ImGui::SameLine();
|
||||
{
|
||||
const int objectId = ctx.object ? ctx.object->id : -1;
|
||||
int& selected = g_selectedOptionEditorIndex[objectId];
|
||||
if (ImGui::Button("Remove Option") && selected >= 0 && selected < static_cast<int>(config.options.size())) {
|
||||
config.options.erase(config.options.begin() + static_cast<std::ptrdiff_t>(selected));
|
||||
if (config.options.empty()) selected = -1;
|
||||
else selected = std::clamp(selected, 0, static_cast<int>(config.options.size()) - 1);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!config.options.empty()) {
|
||||
config.selectedOptionIndex = std::clamp(config.selectedOptionIndex, 0, static_cast<int>(config.options.size()) - 1);
|
||||
changed |= ImGui::SliderInt("Selected Option Index", &config.selectedOptionIndex, 0,
|
||||
static_cast<int>(config.options.size()) - 1);
|
||||
|
||||
const int objectId = ctx.object ? ctx.object->id : -1;
|
||||
int& selected = g_selectedOptionEditorIndex[objectId];
|
||||
selected = std::clamp(selected, 0, static_cast<int>(config.options.size()) - 1);
|
||||
|
||||
ImGui::BeginChild("OptionList", ImVec2(230.0f, 180.0f), true);
|
||||
for (size_t i = 0; i < config.options.size(); ++i) {
|
||||
const std::string label = std::to_string(i + 1) + ". " +
|
||||
(config.options[i].optionName.empty() ? std::string("<Unnamed>") : config.options[i].optionName);
|
||||
if (ImGui::Selectable(label.c_str(), selected == static_cast<int>(i))) {
|
||||
selected = static_cast<int>(i);
|
||||
}
|
||||
}
|
||||
ImGui::EndChild();
|
||||
|
||||
ImGui::SameLine();
|
||||
ImGui::BeginGroup();
|
||||
|
||||
InteractionOption& option = config.options[static_cast<size_t>(selected)];
|
||||
changed |= DrawStdStringInput("Option Name", option.optionName, 256);
|
||||
changed |= drawInteractionTypeCombo(option.interactionType);
|
||||
|
||||
if (option.interactionType == InteractionType::Dialogue) {
|
||||
ImGui::Separator();
|
||||
ImGui::TextUnformatted("Dialogue Settings");
|
||||
changed |= DrawObjectRefInput(ctx, "Dialogue System Ref", option.dialogueSystemRef);
|
||||
|
||||
int& selectedLine = g_selectedDialogueLineEditorIndex[objectId];
|
||||
changed |= drawDialogueLineEditor(ctx, option.dialogueLines, selectedLine);
|
||||
|
||||
changed |= DrawObjectRefListEditor(ctx, "Dialogue End Enable", option.dialogueItemsToEnableOnEnd);
|
||||
changed |= DrawObjectRefListEditor(ctx, "Dialogue End Disable", option.dialogueItemsToDisableOnEnd);
|
||||
} else {
|
||||
ImGui::Separator();
|
||||
ImGui::TextUnformatted("Object Toggle Settings");
|
||||
changed |= DrawObjectRefListEditor(ctx, "Items To Enable", option.itemsToEnable);
|
||||
changed |= DrawObjectRefListEditor(ctx, "Items To Disable", option.itemsToDisable);
|
||||
}
|
||||
|
||||
ImGui::EndGroup();
|
||||
} else {
|
||||
ImGui::TextDisabled("No options configured.");
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
saveConfig(ctx, config);
|
||||
}
|
||||
|
||||
drawRuntimeStatus(config, &runtimeState);
|
||||
}
|
||||
|
||||
void Begin(ScriptContext& ctx, float /*deltaTime*/) {
|
||||
if (!ctx.object) return;
|
||||
|
||||
const InteractableConfig config = loadConfig(ctx);
|
||||
InteractableRuntimeState& state = g_runtimeStates[ctx.object->id];
|
||||
state.prevInteractDown = false;
|
||||
state.hasSelectionState = true;
|
||||
state.lastSelectionState = config.isSelected;
|
||||
state.lastRangeDistance = -1.0f;
|
||||
state.lastRangeInRange = false;
|
||||
state.lastRangeHasPlayer = false;
|
||||
state.lastPlayerWorldPos = glm::vec3(0.0f);
|
||||
state.lastSelfWorldPos = ctx.object ? getObjectReferencePosition(ctx, *ctx.object) : glm::vec3(0.0f);
|
||||
|
||||
applySelectedState(ctx, config);
|
||||
}
|
||||
|
||||
void TickUpdate(ScriptContext& ctx, float /*deltaTime*/) {
|
||||
if (!ctx.object) return;
|
||||
|
||||
InteractableConfig config = loadConfig(ctx);
|
||||
InteractableRuntimeState& state = g_runtimeStates[ctx.object->id];
|
||||
|
||||
if (!state.hasSelectionState || state.lastSelectionState != config.isSelected) {
|
||||
applySelectedState(ctx, config);
|
||||
state.hasSelectionState = true;
|
||||
state.lastSelectionState = config.isSelected;
|
||||
}
|
||||
|
||||
isPlayerInRange(ctx, config, &state);
|
||||
|
||||
const bool interactDown = isInteractDown();
|
||||
const bool interactPressed = interactDown && !state.prevInteractDown;
|
||||
state.prevInteractDown = interactDown;
|
||||
|
||||
if (!config.interactOnKeyPress || !interactPressed) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (interact(ctx, config, &state)) {
|
||||
saveConfig(ctx, config);
|
||||
}
|
||||
}
|
||||
341
Scripts/MainMenuController.cpp
Normal file
341
Scripts/MainMenuController.cpp
Normal file
@@ -0,0 +1,341 @@
|
||||
#include "DialoguePortShared.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
using namespace DialoguePort;
|
||||
|
||||
enum class MenuOrientation {
|
||||
Vertical = 0,
|
||||
Horizontal = 1
|
||||
};
|
||||
|
||||
struct MenuAction {
|
||||
std::vector<std::string> enableRefs;
|
||||
std::vector<std::string> disableRefs;
|
||||
};
|
||||
|
||||
struct MenuConfig {
|
||||
MenuOrientation orientation = MenuOrientation::Vertical;
|
||||
std::string heartRef;
|
||||
std::vector<std::string> menuItemRefs;
|
||||
std::string moveSoundRef;
|
||||
std::string selectSoundRef;
|
||||
float moveSpeed = 0.3f;
|
||||
float offset = 20.0f;
|
||||
float inputDelay = 0.2f;
|
||||
float initialDelay = 0.2f;
|
||||
std::vector<MenuAction> actions;
|
||||
};
|
||||
|
||||
struct MenuRuntimeState {
|
||||
int currentIndex = 0;
|
||||
float nextInputTime = 0.0f;
|
||||
float startupDelayRemaining = 0.0f;
|
||||
float elapsed = 0.0f;
|
||||
glm::vec2 heartVisualPos = glm::vec2(0.0f);
|
||||
bool initialized = false;
|
||||
};
|
||||
|
||||
std::unordered_map<int, MenuRuntimeState> g_runtimeStates;
|
||||
std::unordered_map<int, int> g_selectedActionByObject;
|
||||
|
||||
constexpr const char* kSettingOrientation = "menu.orientation";
|
||||
constexpr const char* kSettingHeartRef = "menu.heartRef";
|
||||
constexpr const char* kSettingMenuItems = "menu.itemRefs";
|
||||
constexpr const char* kSettingMoveSoundRef = "menu.moveSoundRef";
|
||||
constexpr const char* kSettingSelectSoundRef = "menu.selectSoundRef";
|
||||
constexpr const char* kSettingMoveSpeed = "menu.moveSpeed";
|
||||
constexpr const char* kSettingOffset = "menu.offset";
|
||||
constexpr const char* kSettingInputDelay = "menu.inputDelay";
|
||||
constexpr const char* kSettingInitialDelay = "menu.initialDelay";
|
||||
constexpr const char* kSettingActions = "menu.actions";
|
||||
|
||||
bool setSettingIfChanged(ScriptContext& ctx, const std::string& key, const std::string& value) {
|
||||
if (ctx.GetSetting(key, "") == value) return false;
|
||||
ctx.SetSetting(key, value);
|
||||
return true;
|
||||
}
|
||||
|
||||
void normalizeActionCount(MenuConfig& config) {
|
||||
if (config.actions.size() < config.menuItemRefs.size()) {
|
||||
config.actions.resize(config.menuItemRefs.size());
|
||||
} else if (config.actions.size() > config.menuItemRefs.size()) {
|
||||
config.actions.resize(config.menuItemRefs.size());
|
||||
}
|
||||
}
|
||||
|
||||
std::string serializeActions(const std::vector<MenuAction>& actions) {
|
||||
std::vector<std::string> encoded;
|
||||
encoded.reserve(actions.size());
|
||||
for (const MenuAction& action : actions) {
|
||||
std::vector<std::string> fields;
|
||||
fields.reserve(2);
|
||||
fields.push_back(SerializeObjectRefs(action.enableRefs));
|
||||
fields.push_back(SerializeObjectRefs(action.disableRefs));
|
||||
encoded.push_back(JoinEscaped(fields, '|'));
|
||||
}
|
||||
return JoinEscaped(encoded, '\t');
|
||||
}
|
||||
|
||||
std::vector<MenuAction> deserializeActions(const std::string& encoded) {
|
||||
std::vector<MenuAction> actions;
|
||||
if (encoded.empty()) return actions;
|
||||
|
||||
char outerDelimiter = '\t';
|
||||
if (encoded.find('\t') == std::string::npos && encoded.find('\n') != std::string::npos) {
|
||||
outerDelimiter = '\n';
|
||||
}
|
||||
|
||||
std::vector<std::string> entries = SplitEscaped(encoded, outerDelimiter);
|
||||
actions.reserve(entries.size());
|
||||
for (const std::string& entry : entries) {
|
||||
if (Trim(entry).empty()) continue;
|
||||
std::vector<std::string> fields = SplitEscaped(entry, '|');
|
||||
MenuAction action;
|
||||
if (fields.size() > 0) action.enableRefs = DeserializeObjectRefs(fields[0]);
|
||||
if (fields.size() > 1) action.disableRefs = DeserializeObjectRefs(fields[1]);
|
||||
actions.push_back(std::move(action));
|
||||
}
|
||||
return actions;
|
||||
}
|
||||
|
||||
MenuConfig loadConfig(ScriptContext& ctx) {
|
||||
MenuConfig config;
|
||||
|
||||
config.orientation = static_cast<MenuOrientation>(
|
||||
std::clamp(ParseInt(ctx.GetSetting(kSettingOrientation, "0"), 0), 0, 1));
|
||||
config.heartRef = ctx.GetSetting(kSettingHeartRef, "");
|
||||
config.menuItemRefs = DeserializeObjectRefs(ctx.GetSetting(kSettingMenuItems, ""));
|
||||
config.moveSoundRef = ctx.GetSetting(kSettingMoveSoundRef, "");
|
||||
config.selectSoundRef = ctx.GetSetting(kSettingSelectSoundRef, "");
|
||||
config.moveSpeed = std::max(0.0f, ParseFloat(ctx.GetSetting(kSettingMoveSpeed, "0.3"), 0.3f));
|
||||
config.offset = ParseFloat(ctx.GetSetting(kSettingOffset, "20"), 20.0f);
|
||||
config.inputDelay = std::max(0.0f, ParseFloat(ctx.GetSetting(kSettingInputDelay, "0.2"), 0.2f));
|
||||
config.initialDelay = std::max(0.0f, ParseFloat(ctx.GetSetting(kSettingInitialDelay, "0.2"), 0.2f));
|
||||
config.actions = deserializeActions(ctx.GetSetting(kSettingActions, ""));
|
||||
normalizeActionCount(config);
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
void saveConfig(ScriptContext& ctx, MenuConfig& config) {
|
||||
normalizeActionCount(config);
|
||||
setSettingIfChanged(ctx, kSettingOrientation, std::to_string(static_cast<int>(config.orientation)));
|
||||
setSettingIfChanged(ctx, kSettingHeartRef, config.heartRef);
|
||||
setSettingIfChanged(ctx, kSettingMenuItems, SerializeObjectRefs(config.menuItemRefs));
|
||||
setSettingIfChanged(ctx, kSettingMoveSoundRef, config.moveSoundRef);
|
||||
setSettingIfChanged(ctx, kSettingSelectSoundRef, config.selectSoundRef);
|
||||
setSettingIfChanged(ctx, kSettingMoveSpeed, std::to_string(config.moveSpeed));
|
||||
setSettingIfChanged(ctx, kSettingOffset, std::to_string(config.offset));
|
||||
setSettingIfChanged(ctx, kSettingInputDelay, std::to_string(config.inputDelay));
|
||||
setSettingIfChanged(ctx, kSettingInitialDelay, std::to_string(config.initialDelay));
|
||||
setSettingIfChanged(ctx, kSettingActions, serializeActions(config.actions));
|
||||
}
|
||||
|
||||
void playSoundFromRef(ScriptContext& ctx, const std::string& objectRef) {
|
||||
SceneObject* source = ResolveSceneObjectRef(ctx, objectRef);
|
||||
if (!source || !source->hasAudioSource || source->audioSource.clipPath.empty()) return;
|
||||
ctx.PlayAudioOneShot(source->audioSource.clipPath);
|
||||
}
|
||||
|
||||
int getDirectionInput(MenuOrientation orientation) {
|
||||
if (orientation == MenuOrientation::Vertical) {
|
||||
if (IsRuntimeKeyDown(GLFW_KEY_DOWN, ImGuiKey_DownArrow) ||
|
||||
IsRuntimeKeyDown(GLFW_KEY_S, ImGuiKey_S)) {
|
||||
return 1;
|
||||
}
|
||||
if (IsRuntimeKeyDown(GLFW_KEY_UP, ImGuiKey_UpArrow) ||
|
||||
IsRuntimeKeyDown(GLFW_KEY_W, ImGuiKey_W)) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (IsRuntimeKeyDown(GLFW_KEY_RIGHT, ImGuiKey_RightArrow) ||
|
||||
IsRuntimeKeyDown(GLFW_KEY_D, ImGuiKey_D)) {
|
||||
return 1;
|
||||
}
|
||||
if (IsRuntimeKeyDown(GLFW_KEY_LEFT, ImGuiKey_LeftArrow) ||
|
||||
IsRuntimeKeyDown(GLFW_KEY_A, ImGuiKey_A)) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool isSubmitPressed() {
|
||||
return IsRuntimeKeyDown(GLFW_KEY_ENTER, ImGuiKey_Enter) ||
|
||||
IsRuntimeKeyDown(GLFW_KEY_KP_ENTER, ImGuiKey_KeypadEnter);
|
||||
}
|
||||
|
||||
SceneObject* resolveMenuItem(ScriptContext& ctx, const MenuConfig& config, int index) {
|
||||
if (index < 0 || index >= static_cast<int>(config.menuItemRefs.size())) return nullptr;
|
||||
return ResolveSceneObjectRef(ctx, config.menuItemRefs[static_cast<size_t>(index)]);
|
||||
}
|
||||
|
||||
void updateHeartPosition(ScriptContext& ctx,
|
||||
const MenuConfig& config,
|
||||
MenuRuntimeState& state,
|
||||
float deltaTime) {
|
||||
SceneObject* heart = ResolveSceneObjectRef(ctx, config.heartRef);
|
||||
SceneObject* target = resolveMenuItem(ctx, config, state.currentIndex);
|
||||
if (!heart || !target || !HasUIComponent(*heart) || !HasUIComponent(*target)) return;
|
||||
|
||||
glm::vec2 targetPosition = target->ui.position;
|
||||
const float itemHalfW = std::max(0.0f, target->ui.size.x * 0.5f);
|
||||
const float itemHalfH = std::max(0.0f, target->ui.size.y * 0.5f);
|
||||
const float heartHalfW = std::max(0.0f, heart->ui.size.x * 0.5f);
|
||||
const float heartHalfH = std::max(0.0f, heart->ui.size.y * 0.5f);
|
||||
if (config.orientation == MenuOrientation::Vertical) {
|
||||
targetPosition.x -= itemHalfW + heartHalfW + config.offset;
|
||||
} else {
|
||||
targetPosition.y -= itemHalfH + heartHalfH + config.offset;
|
||||
}
|
||||
|
||||
if (!state.initialized) {
|
||||
state.heartVisualPos = heart->ui.position;
|
||||
}
|
||||
if (config.moveSpeed <= 0.0001f) {
|
||||
state.heartVisualPos = targetPosition;
|
||||
} else {
|
||||
float alpha = std::clamp(deltaTime / config.moveSpeed, 0.0f, 1.0f);
|
||||
state.heartVisualPos = glm::mix(state.heartVisualPos, targetPosition, alpha);
|
||||
}
|
||||
|
||||
if (glm::distance(state.heartVisualPos, heart->ui.position) > 0.001f) {
|
||||
heart->ui.position = state.heartVisualPos;
|
||||
ctx.MarkDirty();
|
||||
}
|
||||
}
|
||||
|
||||
void executeAction(ScriptContext& ctx, const MenuConfig& config, int index) {
|
||||
if (index < 0 || index >= static_cast<int>(config.actions.size())) {
|
||||
ctx.AddConsoleMessage("MainMenuController: no action assigned for current menu item.",
|
||||
ConsoleMessageType::Warning);
|
||||
return;
|
||||
}
|
||||
const MenuAction& action = config.actions[static_cast<size_t>(index)];
|
||||
SetObjectsEnabledState(ctx, action.disableRefs, false);
|
||||
SetObjectsEnabledState(ctx, action.enableRefs, true);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" void Script_OnInspector(ScriptContext& ctx) {
|
||||
MenuConfig config = loadConfig(ctx);
|
||||
bool changed = false;
|
||||
|
||||
const char* orientationLabels[] = { "Vertical", "Horizontal" };
|
||||
int orientation = static_cast<int>(config.orientation);
|
||||
if (ImGui::Combo("Menu Orientation", &orientation, orientationLabels, IM_ARRAYSIZE(orientationLabels))) {
|
||||
config.orientation = static_cast<MenuOrientation>(std::clamp(orientation, 0, 1));
|
||||
changed = true;
|
||||
}
|
||||
|
||||
changed |= DrawObjectRefInput(ctx, "Heart Object", config.heartRef);
|
||||
changed |= DrawObjectRefInput(ctx, "Move Sound Source", config.moveSoundRef);
|
||||
changed |= DrawObjectRefInput(ctx, "Select Sound Source", config.selectSoundRef);
|
||||
changed |= DrawObjectRefListEditor(ctx, "Menu Item Refs", config.menuItemRefs);
|
||||
|
||||
changed |= ImGui::DragFloat("Move Speed (s)", &config.moveSpeed, 0.01f, 0.0f, 5.0f, "%.2f");
|
||||
config.moveSpeed = std::max(0.0f, config.moveSpeed);
|
||||
changed |= ImGui::DragFloat("Offset", &config.offset, 0.5f, -500.0f, 500.0f, "%.1f");
|
||||
changed |= ImGui::DragFloat("Input Delay", &config.inputDelay, 0.01f, 0.0f, 2.0f, "%.2f");
|
||||
config.inputDelay = std::max(0.0f, config.inputDelay);
|
||||
changed |= ImGui::DragFloat("Initial Delay", &config.initialDelay, 0.01f, 0.0f, 5.0f, "%.2f");
|
||||
config.initialDelay = std::max(0.0f, config.initialDelay);
|
||||
|
||||
normalizeActionCount(config);
|
||||
if (!config.menuItemRefs.empty()) {
|
||||
int& selected = g_selectedActionByObject[ctx.object ? ctx.object->id : -1];
|
||||
selected = std::clamp(selected, 0, static_cast<int>(config.menuItemRefs.size()) - 1);
|
||||
if (ImGui::BeginCombo("Edit Action", std::to_string(selected + 1).c_str())) {
|
||||
for (int i = 0; i < static_cast<int>(config.menuItemRefs.size()); ++i) {
|
||||
std::string label = std::to_string(i + 1);
|
||||
if (SceneObject* item = resolveMenuItem(ctx, config, i)) {
|
||||
label += ". " + item->name;
|
||||
}
|
||||
bool isSelected = (selected == i);
|
||||
if (ImGui::Selectable(label.c_str(), isSelected)) {
|
||||
selected = i;
|
||||
}
|
||||
if (isSelected) ImGui::SetItemDefaultFocus();
|
||||
}
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
|
||||
if (selected >= 0 && selected < static_cast<int>(config.actions.size())) {
|
||||
MenuAction& action = config.actions[static_cast<size_t>(selected)];
|
||||
changed |= DrawObjectRefListEditor(ctx, "Enable Objects", action.enableRefs);
|
||||
changed |= DrawObjectRefListEditor(ctx, "Disable Objects", action.disableRefs);
|
||||
}
|
||||
} else {
|
||||
ImGui::TextDisabled("Add menu item references to configure actions.");
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
saveConfig(ctx, config);
|
||||
}
|
||||
}
|
||||
|
||||
void Begin(ScriptContext& ctx, float /*deltaTime*/) {
|
||||
if (!ctx.object) return;
|
||||
MenuConfig config = loadConfig(ctx);
|
||||
MenuRuntimeState& state = g_runtimeStates[ctx.object->id];
|
||||
state.currentIndex = 0;
|
||||
state.nextInputTime = 0.0f;
|
||||
state.startupDelayRemaining = config.initialDelay;
|
||||
state.elapsed = 0.0f;
|
||||
state.initialized = false;
|
||||
updateHeartPosition(ctx, config, state, 1.0f);
|
||||
state.initialized = true;
|
||||
}
|
||||
|
||||
void TickUpdate(ScriptContext& ctx, float deltaTime) {
|
||||
if (!ctx.object || deltaTime <= 0.0f) return;
|
||||
|
||||
MenuConfig config = loadConfig(ctx);
|
||||
normalizeActionCount(config);
|
||||
|
||||
MenuRuntimeState& state = g_runtimeStates[ctx.object->id];
|
||||
if (!state.initialized) {
|
||||
state.currentIndex = 0;
|
||||
state.nextInputTime = 0.0f;
|
||||
state.startupDelayRemaining = config.initialDelay;
|
||||
state.elapsed = 0.0f;
|
||||
state.initialized = true;
|
||||
}
|
||||
|
||||
const int itemCount = static_cast<int>(config.menuItemRefs.size());
|
||||
if (itemCount <= 0) {
|
||||
updateHeartPosition(ctx, config, state, deltaTime);
|
||||
return;
|
||||
}
|
||||
|
||||
state.currentIndex = std::clamp(state.currentIndex, 0, itemCount - 1);
|
||||
state.elapsed += deltaTime;
|
||||
state.startupDelayRemaining = std::max(0.0f, state.startupDelayRemaining - deltaTime);
|
||||
const bool canMove = state.startupDelayRemaining <= 0.0f;
|
||||
|
||||
if (canMove && state.elapsed >= state.nextInputTime) {
|
||||
const int direction = getDirectionInput(config.orientation);
|
||||
if (direction != 0) {
|
||||
state.currentIndex += direction;
|
||||
if (state.currentIndex < 0) state.currentIndex = itemCount - 1;
|
||||
if (state.currentIndex >= itemCount) state.currentIndex = 0;
|
||||
state.nextInputTime = state.elapsed + std::max(0.0f, config.inputDelay);
|
||||
playSoundFromRef(ctx, config.moveSoundRef);
|
||||
}
|
||||
|
||||
if (isSubmitPressed()) {
|
||||
executeAction(ctx, config, state.currentIndex);
|
||||
playSoundFromRef(ctx, config.selectSoundRef);
|
||||
state.nextInputTime = state.elapsed + std::max(0.0f, config.inputDelay);
|
||||
}
|
||||
}
|
||||
|
||||
updateHeartPosition(ctx, config, state, deltaTime);
|
||||
}
|
||||
@@ -76,6 +76,18 @@ namespace ModuCPP {
|
||||
public IntPtr ImGuiBeginCombo;
|
||||
public IntPtr ImGuiEndCombo;
|
||||
public IntPtr ImGuiSelectable;
|
||||
// Version 5+ additions.
|
||||
public IntPtr HasAnimation;
|
||||
public IntPtr PlayAnimation;
|
||||
public IntPtr StopAnimation;
|
||||
public IntPtr PauseAnimation;
|
||||
public IntPtr ReverseAnimation;
|
||||
public IntPtr SetAnimationTime;
|
||||
public IntPtr GetAnimationTime;
|
||||
public IntPtr IsAnimationPlaying;
|
||||
public IntPtr SetAnimationLoop;
|
||||
public IntPtr SetAnimationPlaySpeed;
|
||||
public IntPtr SetAnimationPlayOnAwake;
|
||||
}
|
||||
|
||||
internal unsafe static class Native {
|
||||
@@ -116,6 +128,17 @@ namespace ModuCPP {
|
||||
public static ImGuiEndComboFn ImGuiEndCombo;
|
||||
public static ImGuiSelectableFn ImGuiSelectable;
|
||||
public static ImGuiAcceptSceneObjectDropFn ImGuiAcceptSceneObjectDrop;
|
||||
public static HasAnimationFn HasAnimation;
|
||||
public static PlayAnimationFn PlayAnimation;
|
||||
public static StopAnimationFn StopAnimation;
|
||||
public static PauseAnimationFn PauseAnimation;
|
||||
public static ReverseAnimationFn ReverseAnimation;
|
||||
public static SetAnimationTimeFn SetAnimationTime;
|
||||
public static GetAnimationTimeFn GetAnimationTime;
|
||||
public static IsAnimationPlayingFn IsAnimationPlaying;
|
||||
public static SetAnimationLoopFn SetAnimationLoop;
|
||||
public static SetAnimationPlaySpeedFn SetAnimationPlaySpeed;
|
||||
public static SetAnimationPlayOnAwakeFn SetAnimationPlayOnAwake;
|
||||
|
||||
public static void BindDelegates() {
|
||||
GetObjectId = Marshal.GetDelegateForFunctionPointer<GetObjectIdFn>(Api.GetObjectId);
|
||||
@@ -176,6 +199,61 @@ namespace ModuCPP {
|
||||
} else {
|
||||
ImGuiSelectable = (_, _) => 0;
|
||||
}
|
||||
if (Api.Version >= 5 && Api.HasAnimation != IntPtr.Zero) {
|
||||
HasAnimation = Marshal.GetDelegateForFunctionPointer<HasAnimationFn>(Api.HasAnimation);
|
||||
} else {
|
||||
HasAnimation = _ => 0;
|
||||
}
|
||||
if (Api.Version >= 5 && Api.PlayAnimation != IntPtr.Zero) {
|
||||
PlayAnimation = Marshal.GetDelegateForFunctionPointer<PlayAnimationFn>(Api.PlayAnimation);
|
||||
} else {
|
||||
PlayAnimation = (_, _) => 0;
|
||||
}
|
||||
if (Api.Version >= 5 && Api.StopAnimation != IntPtr.Zero) {
|
||||
StopAnimation = Marshal.GetDelegateForFunctionPointer<StopAnimationFn>(Api.StopAnimation);
|
||||
} else {
|
||||
StopAnimation = (_, _) => 0;
|
||||
}
|
||||
if (Api.Version >= 5 && Api.PauseAnimation != IntPtr.Zero) {
|
||||
PauseAnimation = Marshal.GetDelegateForFunctionPointer<PauseAnimationFn>(Api.PauseAnimation);
|
||||
} else {
|
||||
PauseAnimation = (_, _) => 0;
|
||||
}
|
||||
if (Api.Version >= 5 && Api.ReverseAnimation != IntPtr.Zero) {
|
||||
ReverseAnimation = Marshal.GetDelegateForFunctionPointer<ReverseAnimationFn>(Api.ReverseAnimation);
|
||||
} else {
|
||||
ReverseAnimation = (_, _) => 0;
|
||||
}
|
||||
if (Api.Version >= 5 && Api.SetAnimationTime != IntPtr.Zero) {
|
||||
SetAnimationTime = Marshal.GetDelegateForFunctionPointer<SetAnimationTimeFn>(Api.SetAnimationTime);
|
||||
} else {
|
||||
SetAnimationTime = (_, _) => 0;
|
||||
}
|
||||
if (Api.Version >= 5 && Api.GetAnimationTime != IntPtr.Zero) {
|
||||
GetAnimationTime = Marshal.GetDelegateForFunctionPointer<GetAnimationTimeFn>(Api.GetAnimationTime);
|
||||
} else {
|
||||
GetAnimationTime = _ => 0f;
|
||||
}
|
||||
if (Api.Version >= 5 && Api.IsAnimationPlaying != IntPtr.Zero) {
|
||||
IsAnimationPlaying = Marshal.GetDelegateForFunctionPointer<IsAnimationPlayingFn>(Api.IsAnimationPlaying);
|
||||
} else {
|
||||
IsAnimationPlaying = _ => 0;
|
||||
}
|
||||
if (Api.Version >= 5 && Api.SetAnimationLoop != IntPtr.Zero) {
|
||||
SetAnimationLoop = Marshal.GetDelegateForFunctionPointer<SetAnimationLoopFn>(Api.SetAnimationLoop);
|
||||
} else {
|
||||
SetAnimationLoop = (_, _) => 0;
|
||||
}
|
||||
if (Api.Version >= 5 && Api.SetAnimationPlaySpeed != IntPtr.Zero) {
|
||||
SetAnimationPlaySpeed = Marshal.GetDelegateForFunctionPointer<SetAnimationPlaySpeedFn>(Api.SetAnimationPlaySpeed);
|
||||
} else {
|
||||
SetAnimationPlaySpeed = (_, _) => 0;
|
||||
}
|
||||
if (Api.Version >= 5 && Api.SetAnimationPlayOnAwake != IntPtr.Zero) {
|
||||
SetAnimationPlayOnAwake = Marshal.GetDelegateForFunctionPointer<SetAnimationPlayOnAwakeFn>(Api.SetAnimationPlayOnAwake);
|
||||
} else {
|
||||
SetAnimationPlayOnAwake = (_, _) => 0;
|
||||
}
|
||||
}
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
@@ -250,6 +328,28 @@ namespace ModuCPP {
|
||||
public unsafe delegate int ImGuiSelectableFn(byte* label, int selected);
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
public unsafe delegate int ImGuiAcceptSceneObjectDropFn(int* outId);
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
public unsafe delegate int HasAnimationFn(IntPtr ctx);
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
public unsafe delegate int PlayAnimationFn(IntPtr ctx, int restart);
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
public unsafe delegate int StopAnimationFn(IntPtr ctx, int resetTime);
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
public unsafe delegate int PauseAnimationFn(IntPtr ctx, int pause);
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
public unsafe delegate int ReverseAnimationFn(IntPtr ctx, int restartIfStopped);
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
public unsafe delegate int SetAnimationTimeFn(IntPtr ctx, float timeSeconds);
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
public unsafe delegate float GetAnimationTimeFn(IntPtr ctx);
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
public unsafe delegate int IsAnimationPlayingFn(IntPtr ctx);
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
public unsafe delegate int SetAnimationLoopFn(IntPtr ctx, int loop);
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
public unsafe delegate int SetAnimationPlaySpeedFn(IntPtr ctx, float speed);
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
public unsafe delegate int SetAnimationPlayOnAwakeFn(IntPtr ctx, int playOnAwake);
|
||||
}
|
||||
|
||||
public struct ModuObject {
|
||||
@@ -375,6 +475,48 @@ namespace ModuCPP {
|
||||
Native.AddRigidbodyImpulse(handle, impulse.X, impulse.Y, impulse.Z);
|
||||
}
|
||||
|
||||
public bool HasAnimation => Native.HasAnimation(handle) != 0;
|
||||
|
||||
public bool PlayAnimation(bool restart = true) {
|
||||
return Native.PlayAnimation(handle, restart ? 1 : 0) != 0;
|
||||
}
|
||||
|
||||
public bool StopAnimation(bool resetTime = true) {
|
||||
return Native.StopAnimation(handle, resetTime ? 1 : 0) != 0;
|
||||
}
|
||||
|
||||
public bool PauseAnimation(bool pause = true) {
|
||||
return Native.PauseAnimation(handle, pause ? 1 : 0) != 0;
|
||||
}
|
||||
|
||||
public bool ReverseAnimation(bool restartIfStopped = true) {
|
||||
return Native.ReverseAnimation(handle, restartIfStopped ? 1 : 0) != 0;
|
||||
}
|
||||
|
||||
public bool SetAnimationTime(float timeSeconds) {
|
||||
return Native.SetAnimationTime(handle, timeSeconds) != 0;
|
||||
}
|
||||
|
||||
public float GetAnimationTime() {
|
||||
return Native.GetAnimationTime(handle);
|
||||
}
|
||||
|
||||
public bool IsAnimationPlaying() {
|
||||
return Native.IsAnimationPlaying(handle) != 0;
|
||||
}
|
||||
|
||||
public bool SetAnimationLoop(bool loop) {
|
||||
return Native.SetAnimationLoop(handle, loop ? 1 : 0) != 0;
|
||||
}
|
||||
|
||||
public bool SetAnimationPlaySpeed(float speed) {
|
||||
return Native.SetAnimationPlaySpeed(handle, speed) != 0;
|
||||
}
|
||||
|
||||
public bool SetAnimationPlayOnAwake(bool playOnAwake) {
|
||||
return Native.SetAnimationPlayOnAwake(handle, playOnAwake ? 1 : 0) != 0;
|
||||
}
|
||||
|
||||
public float GetSettingFloat(string key, float fallback = 0f) {
|
||||
byte[] keyBytes = Encoding.UTF8.GetBytes((key ?? string.Empty) + "\0");
|
||||
fixed (byte* keyPtr = keyBytes) {
|
||||
|
||||
160
Scripts/Planned ports/MainMenuController.cs
Normal file
160
Scripts/Planned ports/MainMenuController.cs
Normal file
@@ -0,0 +1,160 @@
|
||||
using UnityEngine;
|
||||
using System.Collections;
|
||||
using TMPro;
|
||||
using UnityEngine.UI;
|
||||
|
||||
public class MainMenuController : MonoBehaviour
|
||||
{
|
||||
public enum MenuOrientation { Vertical, Horizontal }
|
||||
public MenuOrientation menuOrientation = MenuOrientation.Vertical;
|
||||
|
||||
public RectTransform heartImage;
|
||||
public TextMeshProUGUI[] menuItems;
|
||||
public AudioSource MenuMove;
|
||||
public AudioSource MenuSelect;
|
||||
public float moveSpeed = 0.3f;
|
||||
public float offset = 20f;
|
||||
|
||||
public MenuAction[] menuActions;
|
||||
|
||||
private int currentIndex = 0;
|
||||
private float inputDelay = 0.2f;
|
||||
private float nextInputTime = 0f;
|
||||
private float initialDelay = 0.2f;
|
||||
private bool canMove = false;
|
||||
|
||||
[System.Serializable]
|
||||
public class MenuAction
|
||||
{
|
||||
public GameObject[] enableObjects;
|
||||
public GameObject[] disableObjects;
|
||||
}
|
||||
|
||||
void Start()
|
||||
{
|
||||
StartCoroutine(InitialDelayCoroutine());
|
||||
}
|
||||
|
||||
IEnumerator InitialDelayCoroutine()
|
||||
{
|
||||
yield return new WaitForSeconds(initialDelay);
|
||||
canMove = true;
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
if (canMove && Time.time >= nextInputTime)
|
||||
{
|
||||
if (menuOrientation == MenuOrientation.Vertical)
|
||||
{
|
||||
if (Input.GetKeyDown(KeyCode.DownArrow) || Input.GetKeyDown(KeyCode.S) || (Input.GetAxisRaw("Vertical") < -0.5f && Mathf.Approximately(Input.GetAxisRaw("Vertical"), -1)))
|
||||
{
|
||||
Navigate(1);
|
||||
MenuMove.Play();
|
||||
nextInputTime = Time.time + inputDelay;
|
||||
}
|
||||
else if (Input.GetKeyDown(KeyCode.UpArrow) || Input.GetKeyDown(KeyCode.W) || (Input.GetAxisRaw("Vertical") > 0.5f && Mathf.Approximately(Input.GetAxisRaw("Vertical"), 1)))
|
||||
{
|
||||
Navigate(-1);
|
||||
MenuMove.Play();
|
||||
nextInputTime = Time.time + inputDelay;
|
||||
}
|
||||
}
|
||||
else if (menuOrientation == MenuOrientation.Horizontal)
|
||||
{
|
||||
if (Input.GetKeyDown(KeyCode.RightArrow) || Input.GetKeyDown(KeyCode.D) || (Input.GetAxisRaw("Horizontal") > 0.5f && Mathf.Approximately(Input.GetAxisRaw("Horizontal"), 1)))
|
||||
{
|
||||
Navigate(1);
|
||||
MenuMove.Play();
|
||||
nextInputTime = Time.time + inputDelay;
|
||||
}
|
||||
else if (Input.GetKeyDown(KeyCode.LeftArrow) || Input.GetKeyDown(KeyCode.A) || (Input.GetAxisRaw("Horizontal") < -0.5f && Mathf.Approximately(Input.GetAxisRaw("Horizontal"), -1)))
|
||||
{
|
||||
Navigate(-1);
|
||||
MenuMove.Play();
|
||||
nextInputTime = Time.time + inputDelay;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for keyboard and controller input for selection
|
||||
if (Input.GetKeyDown(KeyCode.Return) || Input.GetKeyDown(KeyCode.KeypadEnter) || Input.GetButtonDown("Submit"))
|
||||
{
|
||||
ExecuteAction();
|
||||
MenuSelect.Play();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Navigate(int direction)
|
||||
{
|
||||
currentIndex += direction;
|
||||
|
||||
if (currentIndex < 0)
|
||||
{
|
||||
currentIndex = menuItems.Length - 1;
|
||||
}
|
||||
else if (currentIndex >= menuItems.Length)
|
||||
{
|
||||
currentIndex = 0;
|
||||
}
|
||||
|
||||
MoveHeartToCurrentItem();
|
||||
}
|
||||
|
||||
void MoveHeartToCurrentItem()
|
||||
{
|
||||
if (menuItems.Length > 0)
|
||||
{
|
||||
RectTransform targetRect = menuItems[currentIndex].GetComponent<RectTransform>();
|
||||
Vector2 targetPosition = targetRect.anchoredPosition;
|
||||
|
||||
if (menuOrientation == MenuOrientation.Vertical)
|
||||
{
|
||||
targetPosition.x -= targetRect.rect.width / 2 + heartImage.rect.width / 2 + offset;
|
||||
}
|
||||
else
|
||||
{
|
||||
targetPosition.y -= targetRect.rect.height / 2 + heartImage.rect.height / 2 + offset;
|
||||
}
|
||||
|
||||
StartCoroutine(MoveHeart(targetPosition));
|
||||
}
|
||||
}
|
||||
|
||||
System.Collections.IEnumerator MoveHeart(Vector2 targetPosition)
|
||||
{
|
||||
Vector2 startPos = heartImage.anchoredPosition;
|
||||
float elapsedTime = 0f;
|
||||
|
||||
while (elapsedTime < 1f)
|
||||
{
|
||||
heartImage.anchoredPosition = Vector2.Lerp(startPos, targetPosition, elapsedTime);
|
||||
elapsedTime += Time.deltaTime / moveSpeed;
|
||||
yield return null;
|
||||
}
|
||||
|
||||
heartImage.anchoredPosition = targetPosition;
|
||||
}
|
||||
|
||||
void ExecuteAction()
|
||||
{
|
||||
if (currentIndex < menuActions.Length)
|
||||
{
|
||||
var action = menuActions[currentIndex];
|
||||
|
||||
foreach (GameObject obj in action.disableObjects)
|
||||
{
|
||||
obj.SetActive(false);
|
||||
}
|
||||
|
||||
foreach (GameObject obj in action.enableObjects)
|
||||
{
|
||||
obj.SetActive(true);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning("No action assigned for this menu item.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
public class autoEnableAndDisableListsOfObjectsAfterAmountOfTime : MonoBehaviour
|
||||
{
|
||||
public List<GameObject> objectsToEnable;
|
||||
public List<GameObject> objectsToDisable;
|
||||
public float timeToEnableObjects;
|
||||
private float timer;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
timer = 0;
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
timer += Time.deltaTime;
|
||||
if (timer >= timeToEnableObjects)
|
||||
{
|
||||
EnableObjects();
|
||||
DisableObjects();
|
||||
timer = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private void EnableObjects()
|
||||
{
|
||||
foreach (var obj in objectsToEnable)
|
||||
{
|
||||
if (obj != null)
|
||||
{
|
||||
obj.SetActive(true);
|
||||
|
||||
// Check for Collider (3D)
|
||||
Collider collider = obj.GetComponent<Collider>();
|
||||
if (collider != null && !collider.enabled)
|
||||
{
|
||||
collider.enabled = true; // Enable the Collider if it's disabled
|
||||
}
|
||||
|
||||
// Check for Collider2D (2D)
|
||||
Collider2D collider2D = obj.GetComponent<Collider2D>();
|
||||
if (collider2D != null && !collider2D.enabled)
|
||||
{
|
||||
collider2D.enabled = true; // Enable the Collider2D if it's disabled
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DisableObjects()
|
||||
{
|
||||
foreach (var obj in objectsToDisable)
|
||||
{
|
||||
if (obj != null)
|
||||
{
|
||||
obj.SetActive(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,9 @@
|
||||
#include "SceneObject.h"
|
||||
#include "ThirdParty/imgui/imgui.h"
|
||||
#include <array>
|
||||
#include <cctype>
|
||||
#include <cstdio>
|
||||
#include <random>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
@@ -26,6 +29,7 @@ enum class FacingDirection : int {
|
||||
struct ControllerState {
|
||||
FacingDirection facing = FacingDirection::Down;
|
||||
float animationTime = 0.0f;
|
||||
int lastWalkFrame = -1;
|
||||
bool warnedMissingRb = false;
|
||||
bool warnedMissingSprite = false;
|
||||
};
|
||||
@@ -39,6 +43,28 @@ std::array<std::array<int, 4>, 4> walkClips = {{
|
||||
{{ 0, 0, 0, 0 }}
|
||||
}};
|
||||
constexpr const char* kDirectionLabels[4] = { "Down", "Up", "Right", "Left" };
|
||||
constexpr std::array<const char*, 6> kDefaultStepSounds = {
|
||||
"Resources/Sounds/Footstep_01.wav",
|
||||
"Resources/Sounds/Footstep_02.wav",
|
||||
"Resources/Sounds/Footstep_03.wav",
|
||||
"Resources/Sounds/Footstep_04.wav",
|
||||
"Resources/Sounds/Footstep_05.wav",
|
||||
"Resources/Sounds/Footstep_06.wav"
|
||||
};
|
||||
std::array<std::string, 6> stepSounds = {
|
||||
kDefaultStepSounds[0],
|
||||
kDefaultStepSounds[1],
|
||||
kDefaultStepSounds[2],
|
||||
kDefaultStepSounds[3],
|
||||
kDefaultStepSounds[4],
|
||||
kDefaultStepSounds[5]
|
||||
};
|
||||
std::mt19937 rng(std::random_device{}());
|
||||
|
||||
std::string loadStringSetting(ScriptContext& ctx, const std::string& key, const std::string& fallback) {
|
||||
const std::string raw = ctx.GetSetting(key, "");
|
||||
return raw.empty() ? fallback : raw;
|
||||
}
|
||||
|
||||
int loadIntSetting(ScriptContext& ctx, const std::string& key, int fallback) {
|
||||
const std::string raw = ctx.GetSetting(key, "");
|
||||
@@ -57,6 +83,61 @@ void saveIntSettingIfChanged(ScriptContext& ctx, const std::string& key, int val
|
||||
}
|
||||
}
|
||||
|
||||
void saveStringSettingIfChanged(ScriptContext& ctx, const std::string& key, const std::string& value) {
|
||||
if (ctx.GetSetting(key, "") != value) {
|
||||
ctx.SetSetting(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
bool isAudioPath(const std::string& path) {
|
||||
const size_t dot = path.find_last_of('.');
|
||||
if (dot == std::string::npos || dot + 1 >= path.size()) return false;
|
||||
|
||||
std::string ext = path.substr(dot + 1);
|
||||
for (char& c : ext) {
|
||||
c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
||||
}
|
||||
return ext == "wav" || ext == "mp3" || ext == "ogg" || ext == "flac" || ext == "aac" || ext == "m4a";
|
||||
}
|
||||
|
||||
bool drawStepSoundSlot(ScriptContext& ctx, int slotIndex, std::string& path) {
|
||||
bool changed = false;
|
||||
ImGui::PushID(slotIndex);
|
||||
|
||||
char buffer[512] = {};
|
||||
std::snprintf(buffer, sizeof(buffer), "%s", path.c_str());
|
||||
ImGui::SetNextItemWidth(-84.0f);
|
||||
if (ImGui::InputText("##StepSoundPath", buffer, sizeof(buffer))) {
|
||||
path = buffer;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (ImGui::BeginDragDropTarget()) {
|
||||
if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("FILE_PATH")) {
|
||||
if (payload->Data && payload->DataSize > 0) {
|
||||
const char* droppedPath = static_cast<const char*>(payload->Data);
|
||||
if (droppedPath && isAudioPath(droppedPath)) {
|
||||
path = droppedPath;
|
||||
changed = true;
|
||||
} else {
|
||||
ctx.AddConsoleMessage("TopDownMovement2D: dropped file is not a supported audio format.",
|
||||
ConsoleMessageType::Warning);
|
||||
}
|
||||
}
|
||||
}
|
||||
ImGui::EndDragDropTarget();
|
||||
}
|
||||
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("Clear")) {
|
||||
path.clear();
|
||||
changed = true;
|
||||
}
|
||||
|
||||
ImGui::PopID();
|
||||
return changed;
|
||||
}
|
||||
|
||||
void bindSettings(ScriptContext& ctx) {
|
||||
ctx.AutoSetting("walkSpeed", walkSpeed);
|
||||
ctx.AutoSetting("runSpeed", runSpeed);
|
||||
@@ -76,6 +157,10 @@ void bindSettings(ScriptContext& ctx) {
|
||||
walkClips[dir][frame]);
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < 6; ++i) {
|
||||
stepSounds[i] = loadStringSetting(ctx, "stepSound" + std::to_string(i), kDefaultStepSounds[i]);
|
||||
}
|
||||
}
|
||||
|
||||
bool isValidClip(const ScriptContext& ctx, int clip) {
|
||||
@@ -107,6 +192,23 @@ int selectWalkClip(const ScriptContext& ctx, FacingDirection dir, int frameIndex
|
||||
return isValidClip(ctx, candidate) ? candidate : -1;
|
||||
}
|
||||
|
||||
void playRandomFootstep(ScriptContext& ctx) {
|
||||
if (!ctx.HasAudioSource()) return;
|
||||
|
||||
std::array<int, 6> validIndices{};
|
||||
int count = 0;
|
||||
for (int i = 0; i < 6; ++i) {
|
||||
if (!stepSounds[i].empty()) {
|
||||
validIndices[count++] = i;
|
||||
}
|
||||
}
|
||||
if (count <= 0) return;
|
||||
|
||||
std::uniform_int_distribution<int> dist(0, count - 1);
|
||||
const std::string& clipPath = stepSounds[validIndices[dist(rng)]];
|
||||
ctx.PlayAudioOneShot(clipPath);
|
||||
}
|
||||
|
||||
void applySpriteAnimation(ScriptContext& ctx, ControllerState& state, const glm::vec2& motion, float dt, bool isRunning) {
|
||||
if (!useSpriteAnimation || !ctx.object) return;
|
||||
|
||||
@@ -131,10 +233,28 @@ void applySpriteAnimation(ScriptContext& ctx, ControllerState& state, const glm:
|
||||
if (clip >= 0) {
|
||||
ctx.SetSpriteClipIndex(clip);
|
||||
}
|
||||
|
||||
// Human-readable walk frames 1 and 3 map to zero-based indices 0 and 2.
|
||||
// Treat idle->move as coming from frame 4 (index 3) so the first step (frame 1) is not skipped.
|
||||
int probe = (state.lastWalkFrame >= 0) ? state.lastWalkFrame : 3;
|
||||
int stepHits = 0;
|
||||
while (probe != walkFrame) {
|
||||
probe = (probe + 1) % 4;
|
||||
if (probe == 0 || probe == 2) {
|
||||
++stepHits;
|
||||
}
|
||||
}
|
||||
|
||||
// At low FPS, we may cross multiple step frames in a single tick.
|
||||
for (int i = 0; i < stepHits; ++i) {
|
||||
playRandomFootstep(ctx);
|
||||
}
|
||||
state.lastWalkFrame = walkFrame;
|
||||
return;
|
||||
}
|
||||
|
||||
state.animationTime = 0.0f;
|
||||
state.lastWalkFrame = -1;
|
||||
const int idleClip = idleClips[facingIndex(state.facing)];
|
||||
if (isValidClip(ctx, idleClip)) {
|
||||
ctx.SetSpriteClipIndex(idleClip);
|
||||
@@ -188,6 +308,7 @@ extern "C" void Script_OnInspector(ScriptContext& ctx) {
|
||||
ImGui::Checkbox("Use Sprite Animation", &useSpriteAnimation);
|
||||
|
||||
bool clipSettingsChanged = false;
|
||||
bool stepSoundsChanged = false;
|
||||
if (useSpriteAnimation) {
|
||||
const int clipCount = ctx.GetSpriteClipCount();
|
||||
ImGui::Separator();
|
||||
@@ -212,6 +333,17 @@ extern "C" void Script_OnInspector(ScriptContext& ctx) {
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
ImGui::TextUnformatted("Footstep Sounds");
|
||||
ImGui::TextDisabled("Drop audio files from File Browser or type paths. Randomized on step frames 1 and 3.");
|
||||
if (!ctx.object || !ctx.object->hasAudioSource) {
|
||||
ImGui::TextDisabled("Add an Audio Source component to this object for footsteps.");
|
||||
}
|
||||
for (int i = 0; i < 6; ++i) {
|
||||
ImGui::Text("Sound %d", i + 1);
|
||||
stepSoundsChanged |= drawStepSoundSlot(ctx, i, stepSounds[i]);
|
||||
}
|
||||
|
||||
ctx.SaveAutoSettings();
|
||||
if (clipSettingsChanged) {
|
||||
for (int dir = 0; dir < 4; ++dir) {
|
||||
@@ -223,6 +355,11 @@ extern "C" void Script_OnInspector(ScriptContext& ctx) {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (stepSoundsChanged) {
|
||||
for (int i = 0; i < 6; ++i) {
|
||||
saveStringSettingIfChanged(ctx, "stepSound" + std::to_string(i), stepSounds[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TickUpdate(ScriptContext& ctx, float dt) {
|
||||
@@ -278,5 +415,6 @@ void TickUpdate(ScriptContext& ctx, float dt) {
|
||||
ctx.SetPosition2D(pos);
|
||||
}
|
||||
|
||||
applySpriteAnimation(ctx, state, actualVelocity, dt, isRunning);
|
||||
const glm::vec2 animationMotion = (glm::length(input) > 1e-3f) ? targetVel : actualVelocity;
|
||||
applySpriteAnimation(ctx, state, animationMotion, dt, isRunning);
|
||||
}
|
||||
|
||||
@@ -36,6 +36,17 @@ int Modu_GetRigidbodyVelocity(ModuScriptContext* ctx, ModuVec3* outVelocity);
|
||||
int Modu_SetRigidbodyRotation(ModuScriptContext* ctx, ModuVec3 rotation);
|
||||
int Modu_EnsureCapsuleCollider(ModuScriptContext* ctx, float height, float radius);
|
||||
int Modu_EnsureRigidbody(ModuScriptContext* ctx, int useGravity, int kinematic);
|
||||
int Modu_HasAnimation(ModuScriptContext* ctx);
|
||||
int Modu_PlayAnimation(ModuScriptContext* ctx, int restart);
|
||||
int Modu_StopAnimation(ModuScriptContext* ctx, int resetTime);
|
||||
int Modu_PauseAnimation(ModuScriptContext* ctx, int pause);
|
||||
int Modu_ReverseAnimation(ModuScriptContext* ctx, int restartIfStopped);
|
||||
int Modu_SetAnimationTime(ModuScriptContext* ctx, float timeSeconds);
|
||||
float Modu_GetAnimationTime(ModuScriptContext* ctx);
|
||||
int Modu_IsAnimationPlaying(ModuScriptContext* ctx);
|
||||
int Modu_SetAnimationLoop(ModuScriptContext* ctx, int loop);
|
||||
int Modu_SetAnimationPlaySpeed(ModuScriptContext* ctx, float speed);
|
||||
int Modu_SetAnimationPlayOnAwake(ModuScriptContext* ctx, int playOnAwake);
|
||||
|
||||
int Modu_IsSprintDown(ModuScriptContext* ctx);
|
||||
int Modu_IsJumpDown(ModuScriptContext* ctx);
|
||||
|
||||
@@ -280,6 +280,7 @@ bool AudioSystem::init() {
|
||||
void AudioSystem::shutdown() {
|
||||
stopPreview();
|
||||
destroyActiveSounds();
|
||||
destroyOneShotSounds();
|
||||
shutdownReverbGraph();
|
||||
if (initialized) {
|
||||
ma_engine_uninit(&engine);
|
||||
@@ -297,11 +298,37 @@ void AudioSystem::destroyActiveSounds() {
|
||||
activeSounds.clear();
|
||||
}
|
||||
|
||||
void AudioSystem::destroyOneShotSounds() {
|
||||
for (auto& snd : oneShotSounds) {
|
||||
if (snd) {
|
||||
ma_sound_uninit(&snd->sound);
|
||||
releaseDecodedAudio(snd->decodedData);
|
||||
}
|
||||
}
|
||||
oneShotSounds.clear();
|
||||
}
|
||||
|
||||
void AudioSystem::cleanupFinishedOneShots() {
|
||||
for (auto it = oneShotSounds.begin(); it != oneShotSounds.end(); ) {
|
||||
bool erase = !(*it) || !ma_sound_is_playing(&(*it)->sound);
|
||||
if (erase) {
|
||||
if (*it) {
|
||||
ma_sound_uninit(&(*it)->sound);
|
||||
releaseDecodedAudio((*it)->decodedData);
|
||||
}
|
||||
it = oneShotSounds.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AudioSystem::onPlayStart(const std::vector<SceneObject>& objects) {
|
||||
if (!initialized && !init()) return;
|
||||
destroyActiveSounds();
|
||||
destroyOneShotSounds();
|
||||
for (const auto& obj : objects) {
|
||||
if (!obj.enabled || !obj.hasAudioSource || obj.audioSource.clipPath.empty()) continue;
|
||||
if (!IsObjectEnabledInHierarchy(obj) || !obj.hasAudioSource || obj.audioSource.clipPath.empty()) continue;
|
||||
if (!obj.audioSource.enabled) continue;
|
||||
if (ensureSoundFor(obj) && obj.audioSource.playOnStart) {
|
||||
ma_sound_start(&activeSounds[obj.id]->sound);
|
||||
@@ -311,6 +338,7 @@ void AudioSystem::onPlayStart(const std::vector<SceneObject>& objects) {
|
||||
|
||||
void AudioSystem::onPlayStop() {
|
||||
destroyActiveSounds();
|
||||
destroyOneShotSounds();
|
||||
}
|
||||
|
||||
bool AudioSystem::ensureSoundFor(const SceneObject& obj) {
|
||||
@@ -396,6 +424,7 @@ void AudioSystem::update(const std::vector<SceneObject>& objects, const Camera&
|
||||
|
||||
if (!playing) {
|
||||
destroyActiveSounds();
|
||||
destroyOneShotSounds();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -405,7 +434,7 @@ void AudioSystem::update(const std::vector<SceneObject>& objects, const Camera&
|
||||
stillPresent.insert(obj.id);
|
||||
|
||||
auto eraseIt = activeSounds.find(obj.id);
|
||||
if (!obj.enabled || !obj.audioSource.enabled || obj.audioSource.clipPath.empty()) {
|
||||
if (!IsObjectEnabledInHierarchy(obj) || !obj.audioSource.enabled || obj.audioSource.clipPath.empty()) {
|
||||
if (eraseIt != activeSounds.end()) {
|
||||
if (eraseIt->second) {
|
||||
ma_sound_uninit(&eraseIt->second->sound);
|
||||
@@ -436,6 +465,8 @@ void AudioSystem::update(const std::vector<SceneObject>& objects, const Camera&
|
||||
++it;
|
||||
}
|
||||
}
|
||||
|
||||
cleanupFinishedOneShots();
|
||||
}
|
||||
|
||||
bool AudioSystem::playPreview(const std::string& path, float volume, bool loop) {
|
||||
@@ -554,13 +585,75 @@ bool AudioSystem::setPreviewLoop(bool loop) {
|
||||
}
|
||||
|
||||
bool AudioSystem::playObjectSound(const SceneObject& obj) {
|
||||
if (!obj.hasAudioSource || obj.audioSource.clipPath.empty() || !obj.audioSource.enabled) return false;
|
||||
if (!IsObjectEnabledInHierarchy(obj) || !obj.hasAudioSource || obj.audioSource.clipPath.empty() || !obj.audioSource.enabled) return false;
|
||||
if (!ensureSoundFor(obj)) return false;
|
||||
ActiveSound& snd = *activeSounds[obj.id];
|
||||
snd.started = true;
|
||||
return ma_sound_start(&snd.sound) == MA_SUCCESS;
|
||||
}
|
||||
|
||||
bool AudioSystem::playObjectOneShot(const SceneObject& obj, const std::string& clipPathOverride, float volumeScale) {
|
||||
if (!IsObjectEnabledInHierarchy(obj) || !obj.hasAudioSource || !obj.audioSource.enabled) return false;
|
||||
const std::string& clipPath = clipPathOverride.empty() ? obj.audioSource.clipPath : clipPathOverride;
|
||||
if (clipPath.empty()) return false;
|
||||
if (!initialized && !init()) return false;
|
||||
|
||||
if (!fs::exists(clipPath)) {
|
||||
if (missingClips.insert(clipPath).second) {
|
||||
std::cerr << "AudioSystem: clip not found " << clipPath << "\n";
|
||||
}
|
||||
return false;
|
||||
}
|
||||
missingClips.erase(clipPath);
|
||||
|
||||
auto oneShot = std::make_unique<OneShotSound>();
|
||||
if (!initSoundFromPath(clipPath, 0, reverbReady ? &reverbGroup : nullptr,
|
||||
oneShot->sound, oneShot->decodedData))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const float minDist = std::max(0.1f, obj.audioSource.minDistance);
|
||||
const float maxDist = std::max(obj.audioSource.maxDistance, minDist + 0.5f);
|
||||
ma_sound_set_looping(&oneShot->sound, MA_FALSE);
|
||||
ma_sound_set_volume(&oneShot->sound, std::max(0.0f, obj.audioSource.volume * volumeScale));
|
||||
ma_sound_set_spatialization_enabled(&oneShot->sound, obj.audioSource.spatial ? MA_TRUE : MA_FALSE);
|
||||
ma_sound_set_min_distance(&oneShot->sound, minDist);
|
||||
ma_sound_set_max_distance(&oneShot->sound, maxDist);
|
||||
ma_sound_set_position(&oneShot->sound, obj.position.x, obj.position.y, obj.position.z);
|
||||
|
||||
if (obj.audioSource.spatial) {
|
||||
switch (obj.audioSource.rolloffMode) {
|
||||
case AudioRolloffMode::Linear:
|
||||
ma_sound_set_attenuation_model(&oneShot->sound, ma_attenuation_model_linear);
|
||||
break;
|
||||
case AudioRolloffMode::Exponential:
|
||||
ma_sound_set_attenuation_model(&oneShot->sound, ma_attenuation_model_exponential);
|
||||
break;
|
||||
case AudioRolloffMode::Custom:
|
||||
ma_sound_set_attenuation_model(&oneShot->sound, ma_attenuation_model_none);
|
||||
break;
|
||||
case AudioRolloffMode::Logarithmic:
|
||||
default:
|
||||
ma_sound_set_attenuation_model(&oneShot->sound, ma_attenuation_model_inverse);
|
||||
break;
|
||||
}
|
||||
ma_sound_set_rolloff(&oneShot->sound, std::max(0.01f, obj.audioSource.rolloff));
|
||||
} else {
|
||||
ma_sound_set_attenuation_model(&oneShot->sound, ma_attenuation_model_none);
|
||||
}
|
||||
|
||||
if (ma_sound_start(&oneShot->sound) != MA_SUCCESS) {
|
||||
ma_sound_uninit(&oneShot->sound);
|
||||
releaseDecodedAudio(oneShot->decodedData);
|
||||
return false;
|
||||
}
|
||||
|
||||
oneShotSounds.emplace_back(std::move(oneShot));
|
||||
cleanupFinishedOneShots();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AudioSystem::stopObjectSound(int objectId) {
|
||||
auto it = activeSounds.find(objectId);
|
||||
if (it == activeSounds.end()) return false;
|
||||
@@ -616,7 +709,7 @@ AudioSystem::ReverbSettings AudioSystem::getReverbTarget(const std::vector<Scene
|
||||
float bestBlend = 0.0f;
|
||||
|
||||
for (const auto& obj : objects) {
|
||||
if (!obj.enabled || !obj.hasReverbZone || !obj.reverbZone.enabled) continue;
|
||||
if (!IsObjectEnabledInHierarchy(obj) || !obj.hasReverbZone || !obj.reverbZone.enabled) continue;
|
||||
const auto& zone = obj.reverbZone;
|
||||
float blend = 0.0f;
|
||||
|
||||
|
||||
@@ -48,6 +48,7 @@ public:
|
||||
|
||||
// Scene audio control (runtime)
|
||||
bool playObjectSound(const SceneObject& obj);
|
||||
bool playObjectOneShot(const SceneObject& obj, const std::string& clipPathOverride = "", float volumeScale = 1.0f);
|
||||
bool stopObjectSound(int objectId);
|
||||
bool setObjectLoop(const SceneObject& obj, bool loop);
|
||||
bool setObjectVolume(const SceneObject& obj, float volume);
|
||||
@@ -105,9 +106,15 @@ private:
|
||||
std::shared_ptr<DecodedAudioData> decodedData;
|
||||
};
|
||||
|
||||
struct OneShotSound {
|
||||
ma_sound sound{};
|
||||
std::shared_ptr<DecodedAudioData> decodedData;
|
||||
};
|
||||
|
||||
ma_engine engine{};
|
||||
bool initialized = false;
|
||||
std::unordered_map<int, std::unique_ptr<ActiveSound>> activeSounds;
|
||||
std::vector<std::unique_ptr<OneShotSound>> oneShotSounds;
|
||||
std::unordered_map<std::string, AudioClipPreview> previewCache;
|
||||
std::unordered_set<std::string> missingClips;
|
||||
|
||||
@@ -123,6 +130,8 @@ private:
|
||||
std::shared_ptr<DecodedAudioData> previewDecodedData;
|
||||
|
||||
void destroyActiveSounds();
|
||||
void destroyOneShotSounds();
|
||||
void cleanupFinishedOneShots();
|
||||
bool ensureSoundFor(const SceneObject& obj);
|
||||
void refreshSoundParams(const SceneObject& obj, ActiveSound& snd);
|
||||
float computeCustomAttenuation(const SceneObject& obj, const glm::vec3& listenerPos) const;
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include <csignal>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <cstdint>
|
||||
#include <deque>
|
||||
#include <exception>
|
||||
#include <filesystem>
|
||||
@@ -26,6 +27,7 @@
|
||||
|
||||
#if defined(__linux__) || defined(__APPLE__)
|
||||
#include <execinfo.h>
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
@@ -55,6 +57,14 @@ CrashContext& context() {
|
||||
return ctx;
|
||||
}
|
||||
|
||||
#if defined(__linux__) || defined(__APPLE__)
|
||||
constexpr size_t kSignalPathBufferSize = 1024;
|
||||
char gSessionLogPathForSignal[kSignalPathBufferSize] = {};
|
||||
char gSignalCrashLogPath[kSignalPathBufferSize] = {};
|
||||
char gCrashSummaryPathForSignal[kSignalPathBufferSize] = {};
|
||||
volatile sig_atomic_t gSignalLoggingReady = 0;
|
||||
#endif
|
||||
|
||||
fs::path executableDirectory() {
|
||||
auto& ctx = context();
|
||||
if (!ctx.executablePath.empty()) {
|
||||
@@ -100,6 +110,137 @@ std::string nowForDisplay() {
|
||||
return buffer;
|
||||
}
|
||||
|
||||
#if defined(__linux__) || defined(__APPLE__)
|
||||
void storePathForSignal(const fs::path& path, char* outBuffer, size_t outBufferSize) {
|
||||
if (!outBuffer || outBufferSize == 0) return;
|
||||
std::memset(outBuffer, 0, outBufferSize);
|
||||
const std::string value = path.string();
|
||||
const size_t copyLen = std::min(value.size(), outBufferSize - 1);
|
||||
std::memcpy(outBuffer, value.data(), copyLen);
|
||||
outBuffer[copyLen] = '\0';
|
||||
}
|
||||
|
||||
size_t signalSafeStrLen(const char* text) {
|
||||
if (!text) return 0;
|
||||
size_t n = 0;
|
||||
while (text[n] != '\0') ++n;
|
||||
return n;
|
||||
}
|
||||
|
||||
void signalSafeWrite(int fd, const char* text) {
|
||||
if (fd < 0 || !text) return;
|
||||
const size_t len = signalSafeStrLen(text);
|
||||
if (len == 0) return;
|
||||
(void)!::write(fd, text, len);
|
||||
}
|
||||
|
||||
char* appendUnsignedDec(char* out, unsigned long long value) {
|
||||
char tmp[32];
|
||||
int idx = 0;
|
||||
do {
|
||||
tmp[idx++] = static_cast<char>('0' + (value % 10ULL));
|
||||
value /= 10ULL;
|
||||
} while (value && idx < static_cast<int>(sizeof(tmp)));
|
||||
while (idx > 0) {
|
||||
*out++ = tmp[--idx];
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
char* appendSignedDec(char* out, long long value) {
|
||||
if (value < 0) {
|
||||
*out++ = '-';
|
||||
const unsigned long long magnitude = static_cast<unsigned long long>(-(value + 1)) + 1ULL;
|
||||
return appendUnsignedDec(out, magnitude);
|
||||
}
|
||||
return appendUnsignedDec(out, static_cast<unsigned long long>(value));
|
||||
}
|
||||
|
||||
char* appendHex(char* out, uintptr_t value) {
|
||||
static constexpr char kHex[] = "0123456789abcdef";
|
||||
*out++ = '0';
|
||||
*out++ = 'x';
|
||||
bool started = false;
|
||||
for (int shift = static_cast<int>(sizeof(uintptr_t) * 8) - 4; shift >= 0; shift -= 4) {
|
||||
const unsigned nibble = static_cast<unsigned>((value >> shift) & 0xF);
|
||||
if (!started && nibble == 0 && shift > 0) {
|
||||
continue;
|
||||
}
|
||||
started = true;
|
||||
*out++ = kHex[nibble];
|
||||
}
|
||||
if (!started) {
|
||||
*out++ = '0';
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
void signalWriteKeyInt(int fd, const char* key, long long value) {
|
||||
char line[128];
|
||||
char* ptr = line;
|
||||
for (const char* p = key; *p; ++p) *ptr++ = *p;
|
||||
*ptr++ = '=';
|
||||
ptr = appendSignedDec(ptr, value);
|
||||
*ptr++ = '\n';
|
||||
(void)!::write(fd, line, static_cast<size_t>(ptr - line));
|
||||
}
|
||||
|
||||
void signalWriteKeyHex(int fd, const char* key, uintptr_t value) {
|
||||
char line[128];
|
||||
char* ptr = line;
|
||||
for (const char* p = key; *p; ++p) *ptr++ = *p;
|
||||
*ptr++ = '=';
|
||||
ptr = appendHex(ptr, value);
|
||||
*ptr++ = '\n';
|
||||
(void)!::write(fd, line, static_cast<size_t>(ptr - line));
|
||||
}
|
||||
|
||||
void writeSignalSummaryFile(int signalValue) {
|
||||
if (!gCrashSummaryPathForSignal[0]) return;
|
||||
const int fd = ::open(gCrashSummaryPathForSignal, O_WRONLY | O_CREAT | O_TRUNC, 0644);
|
||||
if (fd < 0) return;
|
||||
signalSafeWrite(fd, "reason=Fatal signal\n");
|
||||
signalWriteKeyInt(fd, "signal", signalValue);
|
||||
if (gSignalCrashLogPath[0]) {
|
||||
signalSafeWrite(fd, "log=");
|
||||
signalSafeWrite(fd, gSignalCrashLogPath);
|
||||
signalSafeWrite(fd, "\n");
|
||||
} else if (gSessionLogPathForSignal[0]) {
|
||||
signalSafeWrite(fd, "log=");
|
||||
signalSafeWrite(fd, gSessionLogPathForSignal);
|
||||
signalSafeWrite(fd, "\n");
|
||||
}
|
||||
(void)!::close(fd);
|
||||
}
|
||||
|
||||
void writeSignalCrashLog(int signalValue, siginfo_t* info) {
|
||||
int fd = -1;
|
||||
if (gSignalCrashLogPath[0]) {
|
||||
fd = ::open(gSignalCrashLogPath, O_WRONLY | O_CREAT | O_APPEND, 0644);
|
||||
}
|
||||
if (fd < 0 && gSessionLogPathForSignal[0]) {
|
||||
fd = ::open(gSessionLogPathForSignal, O_WRONLY | O_CREAT | O_APPEND, 0644);
|
||||
}
|
||||
if (fd < 0) return;
|
||||
|
||||
signalSafeWrite(fd, "\n[CrashReporter] Fatal signal captured on POSIX.\n");
|
||||
signalWriteKeyInt(fd, "signal", signalValue);
|
||||
signalWriteKeyInt(fd, "pid", static_cast<long long>(::getpid()));
|
||||
if (info) {
|
||||
signalWriteKeyInt(fd, "si_code", static_cast<long long>(info->si_code));
|
||||
signalWriteKeyHex(fd, "si_addr", reinterpret_cast<uintptr_t>(info->si_addr));
|
||||
}
|
||||
signalSafeWrite(fd, "stacktrace_begin\n");
|
||||
void* frames[64];
|
||||
const int count = ::backtrace(frames, 64);
|
||||
if (count > 0) {
|
||||
::backtrace_symbols_fd(frames, count, fd);
|
||||
}
|
||||
signalSafeWrite(fd, "stacktrace_end\n");
|
||||
(void)!::close(fd);
|
||||
}
|
||||
#endif
|
||||
|
||||
class TeeStreamBuf final : public std::streambuf {
|
||||
public:
|
||||
TeeStreamBuf(std::streambuf* primary, std::streambuf* secondary)
|
||||
@@ -257,20 +398,28 @@ void launchReporterProcess(const std::string& reason, const std::string& details
|
||||
std::_Exit(exitCode);
|
||||
}
|
||||
|
||||
void signalHandler(int signalValue) {
|
||||
#if defined(_WIN32)
|
||||
void signalHandler(int signalValue) {
|
||||
std::ostringstream details;
|
||||
details << signalValue;
|
||||
handleCrash("Fatal signal", details.str(), 128 + signalValue);
|
||||
#else
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(__linux__) || defined(__APPLE__)
|
||||
void signalHandlerWithInfo(int signalValue, siginfo_t* info, void* /*ucontext*/) {
|
||||
static constexpr char kMessage[] =
|
||||
"[CrashReporter] Fatal signal received. Reporter fallback is disabled for POSIX signal crashes.\n";
|
||||
"[CrashReporter] Fatal signal captured. Wrote POSIX crash log.\n";
|
||||
if (gSignalLoggingReady) {
|
||||
writeSignalCrashLog(signalValue, info);
|
||||
writeSignalSummaryFile(signalValue);
|
||||
}
|
||||
(void)!::write(STDERR_FILENO, kMessage, sizeof(kMessage) - 1);
|
||||
std::signal(signalValue, SIG_DFL);
|
||||
std::raise(signalValue);
|
||||
std::_Exit(128 + signalValue);
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
void terminateHandler() {
|
||||
std::string details = "No active exception.";
|
||||
@@ -666,6 +815,16 @@ void Initialize(const std::string& productName, const std::string& executablePat
|
||||
ctx.productName = productName;
|
||||
ctx.executablePath = executablePath;
|
||||
ctx.sessionLogPath = crashDirectory() / (productName + "-session-" + nowForFileName() + ".log");
|
||||
#if defined(__linux__) || defined(__APPLE__)
|
||||
storePathForSignal(ctx.sessionLogPath, gSessionLogPathForSignal, sizeof(gSessionLogPathForSignal));
|
||||
storePathForSignal(crashDirectory() / (productName + "-signal-last.log"),
|
||||
gSignalCrashLogPath,
|
||||
sizeof(gSignalCrashLogPath));
|
||||
storePathForSignal(crashDirectory() / "last_crash.txt",
|
||||
gCrashSummaryPathForSignal,
|
||||
sizeof(gCrashSummaryPathForSignal));
|
||||
gSignalLoggingReady = 1;
|
||||
#endif
|
||||
ctx.logFile.open(ctx.sessionLogPath, std::ios::out | std::ios::trunc);
|
||||
if (ctx.logFile.is_open()) {
|
||||
ctx.oldCout = std::cout.rdbuf();
|
||||
@@ -686,9 +845,9 @@ void Initialize(const std::string& productName, const std::string& executablePat
|
||||
SetUnhandledExceptionFilter(unhandledExceptionFilter);
|
||||
#else
|
||||
struct sigaction action {};
|
||||
action.sa_handler = signalHandler;
|
||||
action.sa_sigaction = signalHandlerWithInfo;
|
||||
sigemptyset(&action.sa_mask);
|
||||
action.sa_flags = 0;
|
||||
action.sa_flags = SA_SIGINFO | SA_RESETHAND;
|
||||
sigaction(SIGABRT, &action, nullptr);
|
||||
sigaction(SIGILL, &action, nullptr);
|
||||
sigaction(SIGFPE, &action, nullptr);
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
#include <unordered_map>
|
||||
|
||||
namespace {
|
||||
constexpr float kEditorBottomStatusReserveHeight = 24.0f;
|
||||
|
||||
struct TouchSwipeWindowState {
|
||||
ImVec2 virtualScroll = ImVec2(0.0f, 0.0f);
|
||||
ImVec2 velocity = ImVec2(0.0f, 0.0f);
|
||||
@@ -212,6 +214,7 @@ FileCategory FileBrowser::getFileCategory(const fs::directory_entry& entry) cons
|
||||
|
||||
// Scene files
|
||||
if (ext == ".modu" || ext == ".scene") return FileCategory::Scene;
|
||||
if (ext == ".modupak") return FileCategory::Text;
|
||||
|
||||
// Model files
|
||||
if (ext == ".fbx" || ext == ".obj" || ext == ".gltf" || ext == ".glb" ||
|
||||
@@ -566,11 +569,18 @@ ImGuiID setupDockspace(const std::function<void()>& menuBarContent) {
|
||||
}
|
||||
|
||||
ImGuiID dockspaceId = ImGui::GetID("MainDockspace");
|
||||
ImGui::DockSpace(dockspaceId, ImVec2(0.0f, 0.0f), dockspaceFlags);
|
||||
const float reserveHeight = getEditorBottomStatusReserveHeight();
|
||||
ImVec2 dockspaceSize = ImGui::GetContentRegionAvail();
|
||||
dockspaceSize.y = ImMax(0.0f, dockspaceSize.y - reserveHeight);
|
||||
ImGui::DockSpace(dockspaceId, dockspaceSize, dockspaceFlags);
|
||||
|
||||
ImGui::End();
|
||||
return dockspaceId;
|
||||
}
|
||||
|
||||
float getEditorBottomStatusReserveHeight() {
|
||||
return kEditorBottomStatusReserveHeight;
|
||||
}
|
||||
#pragma endregion
|
||||
|
||||
#pragma region Touch Swipe Scroll
|
||||
@@ -585,6 +595,26 @@ void updateTouchSwipeScrolling() {
|
||||
static TouchSwipeRuntimeState runtime;
|
||||
|
||||
const bool touchScreenMode = (io.ConfigFlags & ImGuiConfigFlags_IsTouchScreen) != 0;
|
||||
const bool hasWheelInput = std::abs(io.MouseWheelH) > 0.0001f || std::abs(io.MouseWheel) > 0.0001f;
|
||||
|
||||
// On non-touch platforms, skip the full window traversal when there is no
|
||||
// scroll input and no active inertial movement to process.
|
||||
if (!touchScreenMode && !hasWheelInput && runtime.activeWindowId == 0) {
|
||||
bool hasResidualMotion = false;
|
||||
for (const auto& [id, state] : runtime.windowStates) {
|
||||
(void)id;
|
||||
if (state.isDragging ||
|
||||
std::abs(state.velocity.x) > 0.35f ||
|
||||
std::abs(state.velocity.y) > 0.35f) {
|
||||
hasResidualMotion = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!hasResidualMotion) {
|
||||
runtime.windowStates.clear();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const float dt = std::max(io.DeltaTime, 1.0f / 240.0f);
|
||||
const float dragThresholdSqr = 16.0f;
|
||||
|
||||
@@ -86,6 +86,7 @@ void applySuperRoundStyle(ImGuiStyle& style);
|
||||
|
||||
// Setup ImGui dockspace for the editor and return its stable dockspace ID.
|
||||
ImGuiID setupDockspace(const std::function<void()>& menuBarContent = nullptr);
|
||||
float getEditorBottomStatusReserveHeight();
|
||||
|
||||
// Apply touch-style swipe scrolling with inertial motion and elastic edge return.
|
||||
void updateTouchSwipeScrolling();
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,9 +4,11 @@
|
||||
#include <array>
|
||||
#include <cstring>
|
||||
#include <cerrno>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cfloat>
|
||||
#include <cmath>
|
||||
#include <cctype>
|
||||
#include <iomanip>
|
||||
#include <functional>
|
||||
#include <sstream>
|
||||
@@ -17,6 +19,7 @@
|
||||
#include <future>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <commdlg.h>
|
||||
#include <shlobj.h>
|
||||
#include <shellapi.h>
|
||||
#endif
|
||||
@@ -965,6 +968,154 @@ namespace {
|
||||
openPathInShell(path);
|
||||
#endif
|
||||
}
|
||||
|
||||
std::string trimWhitespace(std::string value) {
|
||||
auto isSpace = [](unsigned char c) { return std::isspace(c) != 0; };
|
||||
while (!value.empty() && isSpace(static_cast<unsigned char>(value.front()))) {
|
||||
value.erase(value.begin());
|
||||
}
|
||||
while (!value.empty() && isSpace(static_cast<unsigned char>(value.back()))) {
|
||||
value.pop_back();
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
#if defined(__linux__)
|
||||
std::string shellQuote(const std::string& value) {
|
||||
std::string out;
|
||||
out.reserve(value.size() + 2);
|
||||
out.push_back('\'');
|
||||
for (char c : value) {
|
||||
if (c == '\'') {
|
||||
out += "'\\''";
|
||||
} else {
|
||||
out.push_back(c);
|
||||
}
|
||||
}
|
||||
out.push_back('\'');
|
||||
return out;
|
||||
}
|
||||
|
||||
bool commandExists(const char* command) {
|
||||
if (!command || !*command) {
|
||||
return false;
|
||||
}
|
||||
std::string check = "command -v ";
|
||||
check += command;
|
||||
check += " >/dev/null 2>&1";
|
||||
return std::system(check.c_str()) == 0;
|
||||
}
|
||||
|
||||
std::optional<std::string> runSelectionDialogCommand(const std::string& command) {
|
||||
FILE* pipe = popen(command.c_str(), "r");
|
||||
if (!pipe) {
|
||||
return std::nullopt;
|
||||
}
|
||||
std::string output;
|
||||
char buffer[512];
|
||||
while (std::fgets(buffer, static_cast<int>(sizeof(buffer)), pipe)) {
|
||||
output += buffer;
|
||||
}
|
||||
pclose(pipe);
|
||||
output = trimWhitespace(output);
|
||||
if (output.empty()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return output;
|
||||
}
|
||||
#endif
|
||||
|
||||
std::optional<fs::path> chooseImportFilePath(const fs::path& initialDir) {
|
||||
#ifdef _WIN32
|
||||
std::wstring initialDirWide = initialDir.wstring();
|
||||
std::array<wchar_t, MAX_PATH> fileBuffer{};
|
||||
OPENFILENAMEW ofn{};
|
||||
ofn.lStructSize = sizeof(ofn);
|
||||
ofn.hwndOwner = nullptr;
|
||||
ofn.lpstrFilter = L"All Files\0*.*\0";
|
||||
ofn.lpstrFile = fileBuffer.data();
|
||||
ofn.nMaxFile = static_cast<DWORD>(fileBuffer.size());
|
||||
ofn.lpstrInitialDir = initialDirWide.empty() ? nullptr : initialDirWide.c_str();
|
||||
ofn.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST | OFN_HIDEREADONLY;
|
||||
if (GetOpenFileNameW(&ofn) == TRUE) {
|
||||
return fs::path(fileBuffer.data());
|
||||
}
|
||||
return std::nullopt;
|
||||
#elif __linux__
|
||||
const fs::path initial = initialDir.empty() ? fs::current_path() : initialDir;
|
||||
const std::string initialString = initial.string();
|
||||
const std::string initialForDialog =
|
||||
(!initialString.empty() && initialString.back() == '/') ? initialString : (initialString + "/");
|
||||
|
||||
if (commandExists("zenity")) {
|
||||
std::string cmd = "zenity --file-selection --filename=" + shellQuote(initialForDialog) + " 2>/dev/null";
|
||||
if (auto selected = runSelectionDialogCommand(cmd)) {
|
||||
return fs::path(*selected);
|
||||
}
|
||||
}
|
||||
if (commandExists("kdialog")) {
|
||||
std::string cmd = "kdialog --getopenfilename " + shellQuote(initialString) + " 2>/dev/null";
|
||||
if (auto selected = runSelectionDialogCommand(cmd)) {
|
||||
return fs::path(*selected);
|
||||
}
|
||||
}
|
||||
return std::nullopt;
|
||||
#else
|
||||
(void)initialDir;
|
||||
return std::nullopt;
|
||||
#endif
|
||||
}
|
||||
|
||||
std::optional<fs::path> chooseImportFolderPath(const fs::path& initialDir) {
|
||||
#ifdef _WIN32
|
||||
BROWSEINFOW info{};
|
||||
info.ulFlags = BIF_RETURNONLYFSDIRS | BIF_NEWDIALOGSTYLE;
|
||||
info.lpszTitle = L"Select Folder";
|
||||
LPITEMIDLIST selected = SHBrowseForFolderW(&info);
|
||||
if (!selected) {
|
||||
return std::nullopt;
|
||||
}
|
||||
std::array<wchar_t, MAX_PATH> folderPath{};
|
||||
std::optional<fs::path> result;
|
||||
if (SHGetPathFromIDListW(selected, folderPath.data())) {
|
||||
result = fs::path(folderPath.data());
|
||||
}
|
||||
CoTaskMemFree(selected);
|
||||
return result;
|
||||
#elif __linux__
|
||||
const fs::path initial = initialDir.empty() ? fs::current_path() : initialDir;
|
||||
const std::string initialString = initial.string();
|
||||
const std::string initialForDialog =
|
||||
(!initialString.empty() && initialString.back() == '/') ? initialString : (initialString + "/");
|
||||
|
||||
if (commandExists("zenity")) {
|
||||
std::string cmd = "zenity --file-selection --directory --filename=" + shellQuote(initialForDialog) + " 2>/dev/null";
|
||||
if (auto selected = runSelectionDialogCommand(cmd)) {
|
||||
return fs::path(*selected);
|
||||
}
|
||||
}
|
||||
if (commandExists("kdialog")) {
|
||||
std::string cmd = "kdialog --getexistingdirectory " + shellQuote(initialString) + " 2>/dev/null";
|
||||
if (auto selected = runSelectionDialogCommand(cmd)) {
|
||||
return fs::path(*selected);
|
||||
}
|
||||
}
|
||||
return std::nullopt;
|
||||
#else
|
||||
(void)initialDir;
|
||||
return std::nullopt;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool supportsNativeImportPathPicker() {
|
||||
#ifdef _WIN32
|
||||
return true;
|
||||
#elif __linux__
|
||||
return commandExists("zenity") || commandExists("kdialog");
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
#pragma endregion
|
||||
|
||||
@@ -989,6 +1140,10 @@ void Engine::renderFileBrowserPanel() {
|
||||
static fs::path pendingDeletePath;
|
||||
static fs::path pendingRenamePath;
|
||||
static char renameName[256] = "";
|
||||
static bool showImportAssetsPopup = false;
|
||||
static bool triggerImportAssetsPopup = false;
|
||||
static fs::path pendingImportTargetPath;
|
||||
static char importAssetPaths[4096] = "";
|
||||
bool settingsDirty = false;
|
||||
|
||||
auto openEntry = [&](const fs::directory_entry& entry) {
|
||||
@@ -1147,6 +1302,234 @@ void Engine::renderFileBrowserPanel() {
|
||||
}
|
||||
return path.lexically_normal();
|
||||
};
|
||||
|
||||
auto isPathWithin = [&](const fs::path& root, const fs::path& path) {
|
||||
if (root.empty()) return true;
|
||||
const fs::path normalizedRoot = normalizePath(root);
|
||||
const fs::path normalizedPath = normalizePath(path);
|
||||
if (normalizedPath == normalizedRoot) return true;
|
||||
fs::path current = normalizedPath;
|
||||
while (current.has_parent_path()) {
|
||||
fs::path parent = current.parent_path();
|
||||
if (parent.empty() || parent == current) {
|
||||
break;
|
||||
}
|
||||
current = parent;
|
||||
if (current == normalizedRoot) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
auto remapSelectedPathAfterMove = [&](const fs::path& sourcePath, const fs::path& movedPath) {
|
||||
fs::path selected = normalizePath(fileBrowser.selectedFile);
|
||||
fs::path source = normalizePath(sourcePath);
|
||||
fs::path moved = normalizePath(movedPath);
|
||||
if (selected.empty()) return;
|
||||
if (selected == source) {
|
||||
fileBrowser.selectedFile = moved;
|
||||
return;
|
||||
}
|
||||
if (!isPathWithin(source, selected)) {
|
||||
return;
|
||||
}
|
||||
std::error_code relEc;
|
||||
fs::path rel = fs::relative(selected, source, relEc);
|
||||
if (relEc || rel.empty()) {
|
||||
return;
|
||||
}
|
||||
fileBrowser.selectedFile = moved / rel;
|
||||
};
|
||||
|
||||
auto importPathIntoDirectory = [&](const fs::path& sourcePath, const fs::path& destinationDir) {
|
||||
std::error_code ec;
|
||||
if (sourcePath.empty() || destinationDir.empty()) {
|
||||
return false;
|
||||
}
|
||||
if (!fs::exists(sourcePath, ec) || ec) {
|
||||
addConsoleMessage("Import failed: source missing " + sourcePath.string(), ConsoleMessageType::Error);
|
||||
return false;
|
||||
}
|
||||
if (!fs::exists(destinationDir, ec) || ec || !fs::is_directory(destinationDir, ec)) {
|
||||
addConsoleMessage("Import failed: destination folder missing " + destinationDir.string(), ConsoleMessageType::Error);
|
||||
return false;
|
||||
}
|
||||
|
||||
const fs::path normalizedSource = normalizePath(sourcePath);
|
||||
const fs::path normalizedDestination = normalizePath(destinationDir);
|
||||
if (fs::is_directory(normalizedSource, ec) && !ec && isPathWithin(normalizedSource, normalizedDestination)) {
|
||||
addConsoleMessage("Import failed: cannot import a folder into itself " + normalizedSource.string(),
|
||||
ConsoleMessageType::Error);
|
||||
return false;
|
||||
}
|
||||
|
||||
fs::path targetPath = makeUniquePath(normalizedDestination / normalizedSource.filename());
|
||||
ec.clear();
|
||||
if (fs::is_directory(normalizedSource, ec) && !ec) {
|
||||
fs::copy(normalizedSource, targetPath,
|
||||
fs::copy_options::recursive |
|
||||
fs::copy_options::copy_symlinks |
|
||||
fs::copy_options::skip_existing,
|
||||
ec);
|
||||
} else {
|
||||
fs::copy_file(normalizedSource, targetPath, fs::copy_options::overwrite_existing, ec);
|
||||
}
|
||||
|
||||
if (ec) {
|
||||
addConsoleMessage("Import failed: " + normalizedSource.string() + " (" + ec.message() + ")",
|
||||
ConsoleMessageType::Error);
|
||||
return false;
|
||||
}
|
||||
addConsoleMessage("Imported: " + targetPath.string(), ConsoleMessageType::Success);
|
||||
fileBrowser.selectedFile = targetPath;
|
||||
fileBrowser.needsRefresh = true;
|
||||
projectManager.currentProject.hasUnsavedChanges = true;
|
||||
return true;
|
||||
};
|
||||
|
||||
auto importDirectoryContentsIntoDirectory = [&](const fs::path& sourceDir, const fs::path& destinationDir) {
|
||||
std::error_code ec;
|
||||
if (sourceDir.empty() || destinationDir.empty()) {
|
||||
return false;
|
||||
}
|
||||
if (!fs::exists(sourceDir, ec) || ec || !fs::is_directory(sourceDir, ec)) {
|
||||
addConsoleMessage("Import failed: source folder missing " + sourceDir.string(), ConsoleMessageType::Error);
|
||||
return false;
|
||||
}
|
||||
if (!fs::exists(destinationDir, ec) || ec || !fs::is_directory(destinationDir, ec)) {
|
||||
addConsoleMessage("Import failed: destination folder missing " + destinationDir.string(), ConsoleMessageType::Error);
|
||||
return false;
|
||||
}
|
||||
|
||||
const fs::path normalizedSource = normalizePath(sourceDir);
|
||||
const fs::path normalizedDestination = normalizePath(destinationDir);
|
||||
if (normalizedSource == normalizedDestination) {
|
||||
addConsoleMessage("Import failed: source and destination folder are the same.", ConsoleMessageType::Error);
|
||||
return false;
|
||||
}
|
||||
|
||||
int importedCount = 0;
|
||||
int failedCount = 0;
|
||||
for (const auto& child : fs::directory_iterator(normalizedSource, ec)) {
|
||||
if (ec) {
|
||||
addConsoleMessage("Import failed while reading folder: " + normalizedSource.string() +
|
||||
" (" + ec.message() + ")", ConsoleMessageType::Error);
|
||||
return false;
|
||||
}
|
||||
if (importPathIntoDirectory(child.path(), normalizedDestination)) {
|
||||
++importedCount;
|
||||
} else {
|
||||
++failedCount;
|
||||
}
|
||||
}
|
||||
|
||||
if (importedCount == 0 && failedCount == 0) {
|
||||
addConsoleMessage("Import skipped: folder is empty " + normalizedSource.string(), ConsoleMessageType::Info);
|
||||
return false;
|
||||
}
|
||||
if (importedCount > 0) {
|
||||
addConsoleMessage("Imported " + std::to_string(importedCount) + " item(s) from folder contents.",
|
||||
ConsoleMessageType::Success);
|
||||
}
|
||||
if (failedCount > 0) {
|
||||
addConsoleMessage("Failed to import " + std::to_string(failedCount) + " item(s) from folder contents.",
|
||||
ConsoleMessageType::Warning);
|
||||
}
|
||||
return importedCount > 0;
|
||||
};
|
||||
|
||||
auto movePathIntoDirectory = [&](const fs::path& sourcePath, const fs::path& destinationDir) {
|
||||
std::error_code ec;
|
||||
if (sourcePath.empty() || destinationDir.empty()) {
|
||||
return false;
|
||||
}
|
||||
if (!fs::exists(sourcePath, ec) || ec) {
|
||||
return false;
|
||||
}
|
||||
if (!fs::exists(destinationDir, ec) || ec || !fs::is_directory(destinationDir, ec)) {
|
||||
return false;
|
||||
}
|
||||
if (!fileBrowser.projectRoot.empty() && !isPathWithin(fileBrowser.projectRoot, sourcePath)) {
|
||||
addConsoleMessage("Move blocked: source is outside the project root.", ConsoleMessageType::Warning);
|
||||
return false;
|
||||
}
|
||||
if (!fileBrowser.projectRoot.empty() && !isPathWithin(fileBrowser.projectRoot, destinationDir)) {
|
||||
addConsoleMessage("Move blocked: destination is outside the project root.", ConsoleMessageType::Warning);
|
||||
return false;
|
||||
}
|
||||
|
||||
fs::path normalizedSource = normalizePath(sourcePath);
|
||||
fs::path normalizedDestination = normalizePath(destinationDir);
|
||||
fs::path sourceParent = normalizePath(normalizedSource.parent_path());
|
||||
if (normalizedDestination == sourceParent) {
|
||||
return false;
|
||||
}
|
||||
if (fs::is_directory(normalizedSource, ec) && !ec && isPathWithin(normalizedSource, normalizedDestination)) {
|
||||
addConsoleMessage("Move failed: cannot move a folder into itself.", ConsoleMessageType::Error);
|
||||
return false;
|
||||
}
|
||||
|
||||
fs::path targetPath = normalizedDestination / normalizedSource.filename();
|
||||
if (targetPath == normalizedSource) {
|
||||
return false;
|
||||
}
|
||||
if (fs::exists(targetPath, ec) && !ec) {
|
||||
targetPath = makeUniquePath(targetPath);
|
||||
}
|
||||
|
||||
ec.clear();
|
||||
fs::rename(normalizedSource, targetPath, ec);
|
||||
if (ec) {
|
||||
ec.clear();
|
||||
if (fs::is_directory(normalizedSource, ec) && !ec) {
|
||||
fs::copy(normalizedSource, targetPath,
|
||||
fs::copy_options::recursive |
|
||||
fs::copy_options::copy_symlinks |
|
||||
fs::copy_options::skip_existing,
|
||||
ec);
|
||||
if (!ec) {
|
||||
fs::remove_all(normalizedSource, ec);
|
||||
}
|
||||
} else {
|
||||
fs::copy_file(normalizedSource, targetPath, fs::copy_options::overwrite_existing, ec);
|
||||
if (!ec) {
|
||||
fs::remove(normalizedSource, ec);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (ec) {
|
||||
addConsoleMessage("Move failed: " + normalizedSource.string() + " (" + ec.message() + ")",
|
||||
ConsoleMessageType::Error);
|
||||
return false;
|
||||
}
|
||||
|
||||
remapSelectedPathAfterMove(normalizedSource, targetPath);
|
||||
fileBrowser.needsRefresh = true;
|
||||
projectManager.currentProject.hasUnsavedChanges = true;
|
||||
addConsoleMessage("Moved: " + normalizedSource.filename().string() + " -> " + targetPath.string(),
|
||||
ConsoleMessageType::Success);
|
||||
return true;
|
||||
};
|
||||
|
||||
auto handleMovePayloadToDirectory = [&](const ImGuiPayload* payload, const fs::path& destinationDir) {
|
||||
if (!payload || payload->DataSize <= 0 || !payload->Data) {
|
||||
return false;
|
||||
}
|
||||
const char* sourcePathChars = static_cast<const char*>(payload->Data);
|
||||
if (!sourcePathChars || !*sourcePathChars) {
|
||||
return false;
|
||||
}
|
||||
return movePathIntoDirectory(fs::path(sourcePathChars), destinationDir);
|
||||
};
|
||||
|
||||
auto queueImportAssetsDialog = [&](const fs::path& destinationDir) {
|
||||
pendingImportTargetPath = destinationDir.empty() ? fileBrowser.currentPath : destinationDir;
|
||||
importAssetPaths[0] = '\0';
|
||||
showImportAssetsPopup = true;
|
||||
triggerImportAssetsPopup = true;
|
||||
};
|
||||
|
||||
// Get colors for categories
|
||||
auto getCategoryColor = [](FileCategory cat) -> ImU32 {
|
||||
@@ -1194,6 +1577,16 @@ void Engine::renderFileBrowserPanel() {
|
||||
ImGui::Button("^##Up", ImVec2(24, 0));
|
||||
if (ImGui::IsItemActivated()) fileBrowser.navigateUp();
|
||||
ImGui::EndDisabled();
|
||||
if (canGoUp && ImGui::BeginDragDropTarget()) {
|
||||
fs::path parentTarget = fileBrowser.currentPath.parent_path();
|
||||
if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("FILE_BROWSER_ENTRY")) {
|
||||
handleMovePayloadToDirectory(payload, parentTarget);
|
||||
}
|
||||
if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("FILE_PATH")) {
|
||||
handleMovePayloadToDirectory(payload, parentTarget);
|
||||
}
|
||||
ImGui::EndDragDropTarget();
|
||||
}
|
||||
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) ImGui::SetTooltip("Up one folder");
|
||||
ImGui::PopStyleVar();
|
||||
|
||||
@@ -1245,6 +1638,15 @@ void Engine::renderFileBrowserPanel() {
|
||||
if (ImGui::SmallButton(crumbs[i].label.c_str())) {
|
||||
fileBrowser.navigateTo(crumbs[i].target);
|
||||
}
|
||||
if (ImGui::BeginDragDropTarget()) {
|
||||
if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("FILE_BROWSER_ENTRY")) {
|
||||
handleMovePayloadToDirectory(payload, crumbs[i].target);
|
||||
}
|
||||
if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("FILE_PATH")) {
|
||||
handleMovePayloadToDirectory(payload, crumbs[i].target);
|
||||
}
|
||||
ImGui::EndDragDropTarget();
|
||||
}
|
||||
ImGui::PopID();
|
||||
if (i < crumbs.size() - 1) {
|
||||
ImGui::SameLine(0, 2);
|
||||
@@ -1343,6 +1745,22 @@ void Engine::renderFileBrowserPanel() {
|
||||
contentBg.z = std::min(contentBg.z + 0.01f, 1.0f);
|
||||
ImGui::PushStyleColor(ImGuiCol_ChildBg, contentBg);
|
||||
ImGui::BeginChild("FileContent", ImVec2(0, 0), true);
|
||||
|
||||
if (!pendingExternalFileDrops.empty()) {
|
||||
const ImVec2 contentMin = ImGui::GetWindowPos();
|
||||
const ImVec2 contentMax(contentMin.x + ImGui::GetWindowWidth(),
|
||||
contentMin.y + ImGui::GetWindowHeight());
|
||||
for (const ExternalFileDropEvent& drop : pendingExternalFileDrops) {
|
||||
const bool droppedOverPanel =
|
||||
(drop.mouseX >= contentMin.x && drop.mouseX <= contentMax.x &&
|
||||
drop.mouseY >= contentMin.y && drop.mouseY <= contentMax.y);
|
||||
if (droppedOverPanel) {
|
||||
importPathIntoDirectory(drop.path, fileBrowser.currentPath);
|
||||
}
|
||||
}
|
||||
pendingExternalFileDrops.clear();
|
||||
}
|
||||
|
||||
if (showFileBrowserSidebar) {
|
||||
float minSidebarWidth = 150.0f;
|
||||
float maxSidebarWidth = std::max(minSidebarWidth, ImGui::GetContentRegionAvail().x * 0.5f);
|
||||
@@ -1444,6 +1862,15 @@ void Engine::renderFileBrowserPanel() {
|
||||
if (ImGui::IsItemClicked() && !ImGui::IsItemToggledOpen()) {
|
||||
fileBrowser.navigateTo(path);
|
||||
}
|
||||
if (ImGui::BeginDragDropTarget()) {
|
||||
if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("FILE_BROWSER_ENTRY")) {
|
||||
handleMovePayloadToDirectory(payload, path);
|
||||
}
|
||||
if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("FILE_PATH")) {
|
||||
handleMovePayloadToDirectory(payload, path);
|
||||
}
|
||||
ImGui::EndDragDropTarget();
|
||||
}
|
||||
if (open) {
|
||||
std::vector<fs::path> dirs;
|
||||
std::error_code ec;
|
||||
@@ -1656,6 +2083,9 @@ void Engine::renderFileBrowserPanel() {
|
||||
}
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
if (entry.is_directory() && ImGui::MenuItem("Import Assets Here...")) {
|
||||
queueImportAssetsDialog(entry.path());
|
||||
}
|
||||
if (fileBrowser.isModelFile(entry)) {
|
||||
bool isObj = fileBrowser.isOBJFile(entry);
|
||||
if (ImGui::MenuItem("Import to Scene")) {
|
||||
@@ -1744,6 +2174,14 @@ void Engine::renderFileBrowserPanel() {
|
||||
if (ImGui::MenuItem("Open in File Explorer")) {
|
||||
openPathInFileManager(entry.path());
|
||||
}
|
||||
if (ImGui::MenuItem("Move to Parent Folder")) {
|
||||
movePathIntoDirectory(entry.path(), entry.path().parent_path());
|
||||
}
|
||||
if (!fileBrowser.projectRoot.empty() &&
|
||||
normalizePath(entry.path().parent_path()) != normalizePath(fileBrowser.projectRoot) &&
|
||||
ImGui::MenuItem("Move to Project Root")) {
|
||||
movePathIntoDirectory(entry.path(), fileBrowser.projectRoot);
|
||||
}
|
||||
if (ImGui::MenuItem("Rename")) {
|
||||
pendingRenamePath = entry.path();
|
||||
std::string baseName = pendingRenamePath.filename().string();
|
||||
@@ -1762,9 +2200,20 @@ void Engine::renderFileBrowserPanel() {
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
|
||||
if (!entry.is_directory() && ImGui::BeginDragDropSource(ImGuiDragDropFlags_SourceAllowNullID)) {
|
||||
if (entry.is_directory() && ImGui::BeginDragDropTarget()) {
|
||||
if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("FILE_BROWSER_ENTRY")) {
|
||||
handleMovePayloadToDirectory(payload, entry.path());
|
||||
}
|
||||
if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("FILE_PATH")) {
|
||||
handleMovePayloadToDirectory(payload, entry.path());
|
||||
}
|
||||
ImGui::EndDragDropTarget();
|
||||
}
|
||||
|
||||
if (ImGui::BeginDragDropSource(ImGuiDragDropFlags_SourceAllowNullID)) {
|
||||
std::string payloadPath = entry.path().string();
|
||||
ImGui::SetDragDropPayload("FILE_PATH", payloadPath.c_str(), payloadPath.size() + 1);
|
||||
const char* payloadType = entry.is_directory() ? "FILE_BROWSER_ENTRY" : "FILE_PATH";
|
||||
ImGui::SetDragDropPayload(payloadType, payloadPath.c_str(), payloadPath.size() + 1);
|
||||
ImGui::Text("%s", filename.c_str());
|
||||
ImGui::EndDragDropSource();
|
||||
}
|
||||
@@ -1940,6 +2389,9 @@ void Engine::renderFileBrowserPanel() {
|
||||
}
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
if (entry.is_directory() && ImGui::MenuItem("Import Assets Here...")) {
|
||||
queueImportAssetsDialog(entry.path());
|
||||
}
|
||||
if (fileBrowser.isModelFile(entry)) {
|
||||
bool isObj = fileBrowser.isOBJFile(entry);
|
||||
std::string ext = entry.path().extension().string();
|
||||
@@ -2031,6 +2483,14 @@ void Engine::renderFileBrowserPanel() {
|
||||
if (ImGui::MenuItem("Open in File Explorer")) {
|
||||
openPathInFileManager(entry.path());
|
||||
}
|
||||
if (ImGui::MenuItem("Move to Parent Folder")) {
|
||||
movePathIntoDirectory(entry.path(), entry.path().parent_path());
|
||||
}
|
||||
if (!fileBrowser.projectRoot.empty() &&
|
||||
normalizePath(entry.path().parent_path()) != normalizePath(fileBrowser.projectRoot) &&
|
||||
ImGui::MenuItem("Move to Project Root")) {
|
||||
movePathIntoDirectory(entry.path(), fileBrowser.projectRoot);
|
||||
}
|
||||
if (ImGui::MenuItem("Rename")) {
|
||||
pendingRenamePath = entry.path();
|
||||
std::string baseName = pendingRenamePath.filename().string();
|
||||
@@ -2049,9 +2509,20 @@ void Engine::renderFileBrowserPanel() {
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
|
||||
if (!entry.is_directory() && ImGui::BeginDragDropSource(ImGuiDragDropFlags_SourceAllowNullID)) {
|
||||
if (entry.is_directory() && ImGui::BeginDragDropTarget()) {
|
||||
if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("FILE_BROWSER_ENTRY")) {
|
||||
handleMovePayloadToDirectory(payload, entry.path());
|
||||
}
|
||||
if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("FILE_PATH")) {
|
||||
handleMovePayloadToDirectory(payload, entry.path());
|
||||
}
|
||||
ImGui::EndDragDropTarget();
|
||||
}
|
||||
|
||||
if (ImGui::BeginDragDropSource(ImGuiDragDropFlags_SourceAllowNullID)) {
|
||||
std::string payloadPath = entry.path().string();
|
||||
ImGui::SetDragDropPayload("FILE_PATH", payloadPath.c_str(), payloadPath.size() + 1);
|
||||
const char* payloadType = entry.is_directory() ? "FILE_BROWSER_ENTRY" : "FILE_PATH";
|
||||
ImGui::SetDragDropPayload(payloadType, payloadPath.c_str(), payloadPath.size() + 1);
|
||||
ImGui::Text("%s", filename.c_str());
|
||||
ImGui::EndDragDropSource();
|
||||
}
|
||||
@@ -2062,10 +2533,31 @@ void Engine::renderFileBrowserPanel() {
|
||||
ImGui::PopStyleVar();
|
||||
}
|
||||
|
||||
const bool fileMainBackgroundLeftClicked =
|
||||
ImGui::IsWindowHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem) &&
|
||||
ImGui::IsMouseClicked(ImGuiMouseButton_Left) &&
|
||||
!ImGui::IsAnyItemHovered();
|
||||
if (fileMainBackgroundLeftClicked) {
|
||||
fileBrowser.selectedFile.clear();
|
||||
}
|
||||
|
||||
if (ImGui::BeginDragDropTarget()) {
|
||||
if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("FILE_BROWSER_ENTRY")) {
|
||||
handleMovePayloadToDirectory(payload, fileBrowser.currentPath);
|
||||
}
|
||||
if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("FILE_PATH")) {
|
||||
handleMovePayloadToDirectory(payload, fileBrowser.currentPath);
|
||||
}
|
||||
ImGui::EndDragDropTarget();
|
||||
}
|
||||
|
||||
if (ImGui::BeginPopupContextWindow("FileBrowserEmptyContext", ImGuiPopupFlags_NoOpenOverItems | ImGuiPopupFlags_MouseButtonRight)) {
|
||||
if (ImGui::MenuItem("Open in File Explorer")) {
|
||||
openPathInFileManager(fileBrowser.currentPath);
|
||||
}
|
||||
if (ImGui::MenuItem("Import Assets...")) {
|
||||
queueImportAssetsDialog(fileBrowser.currentPath);
|
||||
}
|
||||
if (ImGui::MenuItem("Refresh")) {
|
||||
fileBrowser.needsRefresh = true;
|
||||
}
|
||||
@@ -2205,6 +2697,104 @@ void Engine::renderFileBrowserPanel() {
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
|
||||
if (triggerImportAssetsPopup) {
|
||||
ImGui::OpenPopup("Import Assets");
|
||||
triggerImportAssetsPopup = false;
|
||||
}
|
||||
if (ImGui::BeginPopupModal("Import Assets", &showImportAssetsPopup, ImGuiWindowFlags_AlwaysAutoResize)) {
|
||||
fs::path targetDir = pendingImportTargetPath.empty() ? fileBrowser.currentPath : pendingImportTargetPath;
|
||||
ImGui::Text("Import assets into:");
|
||||
ImGui::TextDisabled("%s", targetDir.string().c_str());
|
||||
ImGui::Spacing();
|
||||
ImGui::TextWrapped("Use your file explorer to import a single file or an entire folder's contents.");
|
||||
const bool nativePickerAvailable = supportsNativeImportPathPicker();
|
||||
ImGui::BeginDisabled(!nativePickerAvailable);
|
||||
if (ImGui::Button("Select File...", ImVec2(180, 0))) {
|
||||
if (auto selectedPath = chooseImportFilePath(targetDir)) {
|
||||
if (importPathIntoDirectory(*selectedPath, targetDir)) {
|
||||
showImportAssetsPopup = false;
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
}
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Select Folder Contents...", ImVec2(220, 0))) {
|
||||
if (auto selectedFolder = chooseImportFolderPath(targetDir)) {
|
||||
if (importDirectoryContentsIntoDirectory(*selectedFolder, targetDir)) {
|
||||
showImportAssetsPopup = false;
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
}
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
if (!nativePickerAvailable) {
|
||||
ImGui::TextDisabled("Native picker unavailable. Install zenity/kdialog or paste paths below.");
|
||||
}
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::Spacing();
|
||||
ImGui::TextWrapped("Paste one or more source paths. Separate paths with new lines, semicolons, or commas.");
|
||||
ImGui::InputTextMultiline("##ImportAssetPaths", importAssetPaths, sizeof(importAssetPaths),
|
||||
ImVec2(520.0f, 120.0f));
|
||||
bool canImport = importAssetPaths[0] != '\0';
|
||||
ImGui::BeginDisabled(!canImport);
|
||||
if (ImGui::Button("Import", ImVec2(120, 0))) {
|
||||
auto trim = [](std::string value) {
|
||||
auto isSpace = [](unsigned char c) { return std::isspace(c) != 0; };
|
||||
while (!value.empty() && isSpace(static_cast<unsigned char>(value.front()))) {
|
||||
value.erase(value.begin());
|
||||
}
|
||||
while (!value.empty() && isSpace(static_cast<unsigned char>(value.back()))) {
|
||||
value.pop_back();
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
int importedCount = 0;
|
||||
int failedCount = 0;
|
||||
std::string raw(importAssetPaths);
|
||||
size_t start = 0;
|
||||
for (size_t i = 0; i <= raw.size(); ++i) {
|
||||
const bool split = (i == raw.size()) || raw[i] == '\n' || raw[i] == '\r' || raw[i] == ';' || raw[i] == ',';
|
||||
if (!split) continue;
|
||||
std::string token = trim(raw.substr(start, i - start));
|
||||
start = i + 1;
|
||||
if (token.empty()) continue;
|
||||
|
||||
std::error_code absEc;
|
||||
fs::path source = fs::absolute(fs::path(token), absEc);
|
||||
if (absEc) {
|
||||
source = fs::path(token);
|
||||
}
|
||||
if (importPathIntoDirectory(source, targetDir)) {
|
||||
++importedCount;
|
||||
} else {
|
||||
++failedCount;
|
||||
}
|
||||
}
|
||||
|
||||
if (importedCount > 0) {
|
||||
addConsoleMessage("Imported " + std::to_string(importedCount) + " asset(s).",
|
||||
ConsoleMessageType::Success);
|
||||
}
|
||||
if (failedCount > 0) {
|
||||
addConsoleMessage("Failed to import " + std::to_string(failedCount) + " path(s).",
|
||||
ConsoleMessageType::Warning);
|
||||
}
|
||||
if (importedCount > 0 || failedCount > 0) {
|
||||
showImportAssetsPopup = false;
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Cancel", ImVec2(120, 0))) {
|
||||
showImportAssetsPopup = false;
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
|
||||
ImGui::End();
|
||||
}
|
||||
#pragma endregion
|
||||
|
||||
@@ -282,6 +282,9 @@ void Engine::renderPixelSpriteEditorWindow() {
|
||||
EnsureSpriteClipScales(pixelSpriteDocument.spriteFrameScales, pixelSpriteDocument.spriteFrames.size());
|
||||
pixelSpriteDocument.activeLayer = std::clamp(pixelSpriteDocument.activeLayer, 0, std::max(0, static_cast<int>(pixelSpriteDocument.layers.size()) - 1));
|
||||
|
||||
if (mainDockspaceId != 0) {
|
||||
ImGui::SetNextWindowDockID(mainDockspaceId, ImGuiCond_FirstUseEver);
|
||||
}
|
||||
if (!ImGui::Begin("Pixel Sprite Editor", &showPixelSpriteEditorWindow, ImGuiWindowFlags_NoCollapse)) {
|
||||
ImGui::End();
|
||||
return;
|
||||
|
||||
@@ -278,6 +278,80 @@ static float EaseOutCubic(float t) {
|
||||
return 1.0f - inv * inv * inv;
|
||||
}
|
||||
|
||||
static float EaseInOutCubic(float t) {
|
||||
t = ImClamp(t, 0.0f, 1.0f);
|
||||
if (t < 0.5f) {
|
||||
return 4.0f * t * t * t;
|
||||
}
|
||||
const float f = -2.0f * t + 2.0f;
|
||||
return 1.0f - (f * f * f) * 0.5f;
|
||||
}
|
||||
|
||||
static float EaseOutBack(float t) {
|
||||
t = ImClamp(t, 0.0f, 1.0f);
|
||||
constexpr float c1 = 1.70158f;
|
||||
constexpr float c3 = c1 + 1.0f;
|
||||
const float p = t - 1.0f;
|
||||
return 1.0f + c3 * p * p * p + c1 * p * p;
|
||||
}
|
||||
|
||||
struct LauncherIntroTimings {
|
||||
float fadeIn = 0.16f;
|
||||
float popIn = 0.44f;
|
||||
float hold = 0.10f;
|
||||
float drift = 0.64f;
|
||||
};
|
||||
|
||||
struct LauncherIntroState {
|
||||
float elapsed = 0.0f;
|
||||
float introTotal = 0.0f;
|
||||
float textAlpha = 0.0f;
|
||||
float popT = 1.0f;
|
||||
float driftT = 1.0f;
|
||||
float driftEase = 1.0f;
|
||||
float contentRevealT = 1.0f;
|
||||
bool finished = true;
|
||||
};
|
||||
|
||||
static LauncherIntroState EvaluateLauncherIntro(double now,
|
||||
double introStartTime,
|
||||
bool forceFinished,
|
||||
const LauncherIntroTimings& timings) {
|
||||
LauncherIntroState state;
|
||||
const float popStart = timings.fadeIn;
|
||||
const float driftStart = popStart + timings.popIn + timings.hold;
|
||||
state.introTotal = driftStart + timings.drift;
|
||||
state.elapsed = forceFinished
|
||||
? state.introTotal
|
||||
: static_cast<float>(now - introStartTime);
|
||||
|
||||
if (timings.fadeIn <= 0.0001f) {
|
||||
state.textAlpha = 1.0f;
|
||||
} else {
|
||||
state.textAlpha = ImClamp(state.elapsed / timings.fadeIn, 0.0f, 1.0f);
|
||||
}
|
||||
|
||||
state.popT = timings.popIn <= 0.0001f
|
||||
? 1.0f
|
||||
: ImClamp((state.elapsed - popStart) / timings.popIn, 0.0f, 1.0f);
|
||||
state.driftT = timings.drift <= 0.0001f
|
||||
? 1.0f
|
||||
: ImClamp((state.elapsed - driftStart) / timings.drift, 0.0f, 1.0f);
|
||||
state.driftEase = EaseInOutCubic(state.driftT);
|
||||
const float contentWindow = std::max(0.001f, timings.drift + timings.hold);
|
||||
state.contentRevealT = ImClamp((state.elapsed - (driftStart - timings.hold * 0.55f)) / contentWindow, 0.0f, 1.0f);
|
||||
state.finished = forceFinished || state.driftT >= 1.0f;
|
||||
return state;
|
||||
}
|
||||
|
||||
static const PackageInfo* FindRegistryPackageById(const std::vector<PackageInfo>& registry,
|
||||
const std::string& id) {
|
||||
auto it = std::find_if(registry.begin(), registry.end(), [&](const PackageInfo& pkg) {
|
||||
return pkg.id == id;
|
||||
});
|
||||
return it != registry.end() ? &(*it) : nullptr;
|
||||
}
|
||||
|
||||
struct LauncherPackageSnapshot {
|
||||
std::string fingerprint;
|
||||
std::string sourceProjectName;
|
||||
@@ -344,26 +418,39 @@ static bool ParsePackageManifestLabels(const fs::path& manifestPath,
|
||||
continue;
|
||||
}
|
||||
|
||||
std::string sourceTag;
|
||||
std::string sourceSuffix;
|
||||
size_t sourcePrefixLen = 0;
|
||||
if (cleaned.rfind("git=", 0) == 0) {
|
||||
const auto parts = SplitString(cleaned.substr(4), '|');
|
||||
if (parts.empty()) continue;
|
||||
sourceTag = "git";
|
||||
sourceSuffix = " (Git)";
|
||||
sourcePrefixLen = 4;
|
||||
} else if (cleaned.rfind("modupak=", 0) == 0) {
|
||||
sourceTag = "modupak";
|
||||
sourceSuffix = " (.modupak)";
|
||||
sourcePrefixLen = 8;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
const std::string id = TrimCopy(parts[0]);
|
||||
std::string label = id;
|
||||
if (parts.size() > 1) {
|
||||
const std::string parsedName = TrimCopy(parts[1]);
|
||||
if (!parsedName.empty()) {
|
||||
label = parsedName;
|
||||
}
|
||||
}
|
||||
if (!label.empty()) {
|
||||
const std::string key = "git:" + id;
|
||||
if (seen.insert(key).second) {
|
||||
outLabels.push_back(label + " (Git)");
|
||||
outExternalCount++;
|
||||
}
|
||||
const auto parts = SplitString(cleaned.substr(sourcePrefixLen), '|');
|
||||
if (parts.empty()) continue;
|
||||
|
||||
const std::string id = TrimCopy(parts[0]);
|
||||
std::string label = id;
|
||||
if (parts.size() > 1) {
|
||||
const std::string parsedName = TrimCopy(parts[1]);
|
||||
if (!parsedName.empty()) {
|
||||
label = parsedName;
|
||||
}
|
||||
}
|
||||
|
||||
if (label.empty()) continue;
|
||||
const std::string key = sourceTag + ":" + id;
|
||||
if (seen.insert(key).second) {
|
||||
outLabels.push_back(label + sourceSuffix);
|
||||
outExternalCount++;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -769,35 +856,13 @@ void Engine::renderLauncher() {
|
||||
launcherIntroSoundPlayed = true;
|
||||
}
|
||||
|
||||
const float introFadeIn = 0.6f;
|
||||
const float introHold = 0.5f;
|
||||
const float introFadeOut = 0.7f;
|
||||
const float introSlide = 0.8f;
|
||||
const float introSlideStart = introFadeIn + introHold + introFadeOut;
|
||||
const float introTotal = introSlideStart + introSlide;
|
||||
const double introElapsedRaw = launcherIntroFinished ? introTotal : (now - launcherIntroStartTime);
|
||||
const float introElapsed = static_cast<float>(introElapsedRaw);
|
||||
|
||||
float textAlpha = 0.0f;
|
||||
if (!launcherIntroFinished) {
|
||||
if (introElapsed < introFadeIn) {
|
||||
textAlpha = introElapsed / introFadeIn;
|
||||
} else if (introElapsed < introFadeIn + introHold) {
|
||||
textAlpha = 1.0f;
|
||||
} else if (introElapsed < introSlideStart) {
|
||||
textAlpha = 1.0f - (introElapsed - (introFadeIn + introHold)) / introFadeOut;
|
||||
}
|
||||
const LauncherIntroTimings introTimings{};
|
||||
LauncherIntroState introState = EvaluateLauncherIntro(now, launcherIntroStartTime, launcherIntroFinished, introTimings);
|
||||
if (!launcherIntroFinished && introState.finished) {
|
||||
launcherIntroFinished = true;
|
||||
introState = EvaluateLauncherIntro(now, launcherIntroStartTime, true, introTimings);
|
||||
}
|
||||
|
||||
float slideT = 1.0f;
|
||||
if (!launcherIntroFinished) {
|
||||
slideT = ImClamp((introElapsed - introSlideStart) / introSlide, 0.0f, 1.0f);
|
||||
if (slideT >= 1.0f) {
|
||||
launcherIntroFinished = true;
|
||||
}
|
||||
}
|
||||
const float slideEase = 1.0f - std::pow(1.0f - slideT, 3.0f);
|
||||
|
||||
const float transitionDuration = 0.45f;
|
||||
float transitionT = 0.0f;
|
||||
if (launcherTransitionActive) {
|
||||
@@ -805,9 +870,15 @@ void Engine::renderLauncher() {
|
||||
}
|
||||
const float transitionEase = 1.0f - std::pow(1.0f - transitionT, 3.0f);
|
||||
const float transitionAlpha = 1.0f - transitionEase;
|
||||
const float uiScale = 1.0f + 0.06f * transitionEase;
|
||||
const float introOffsetT = launcherIntroFinished ? 0.0f : (1.0f - slideEase);
|
||||
const float contentAlpha = launcherIntroFinished ? 1.0f : slideEase;
|
||||
const float menuBuildT = launcherIntroFinished ? 1.0f : EaseOutCubic(introState.contentRevealT);
|
||||
const float sidebarBuildSeed = launcherIntroFinished ? 1.0f : ImClamp((menuBuildT - 0.02f) / 0.92f, 0.0f, 1.0f);
|
||||
const float contentBuildSeed = launcherIntroFinished ? 1.0f : ImClamp((menuBuildT - 0.16f) / 0.94f, 0.0f, 1.0f);
|
||||
const float sidebarBuildT = launcherIntroFinished ? 1.0f : std::pow(sidebarBuildSeed, 1.32f);
|
||||
const float contentBuildT = launcherIntroFinished ? 1.0f : std::pow(contentBuildSeed, 1.40f);
|
||||
const float introMenuScale = launcherIntroFinished ? 1.0f : ImLerp(1.07f, 1.0f, menuBuildT);
|
||||
const float uiScale = (1.0f + 0.06f * transitionEase) * introMenuScale;
|
||||
const float introOffsetT = launcherIntroFinished ? 0.0f : (1.0f - introState.driftEase);
|
||||
const float contentAlpha = launcherIntroFinished ? 1.0f : ImClamp(0.12f + introState.contentRevealT * 0.88f, 0.0f, 1.0f);
|
||||
const float introHeroOffsetY = -90.0f * uiScale * introOffsetT;
|
||||
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
|
||||
@@ -982,7 +1053,8 @@ void Engine::renderLauncher() {
|
||||
const float paneGap = 12.0f * uiScale;
|
||||
const float shellHeight = ImMax(0.0f, ImGui::GetContentRegionAvail().y - shellInsetTop - shellInsetBottom);
|
||||
|
||||
ImGui::SetCursorPos(ImVec2(contentStart.x + shellInsetX, contentStart.y + introHeroOffsetY + shellInsetTop));
|
||||
ImGui::SetCursorPos(ImVec2(contentStart.x + shellInsetX,
|
||||
contentStart.y + introHeroOffsetY + shellInsetTop));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ChildBorderSize, 0.0f);
|
||||
ImGui::PushStyleColor(ImGuiCol_ChildBg, shellBg);
|
||||
ImGui::BeginChild("LauncherShell", ImVec2(-shellInsetX, shellHeight), false);
|
||||
@@ -991,21 +1063,55 @@ void Engine::renderLauncher() {
|
||||
const ImVec2 sidebarPadding(14.0f * uiScale, 14.0f * uiScale);
|
||||
const ImVec2 contentPadding(14.0f * uiScale, 12.0f * uiScale);
|
||||
const ImVec2 sectionInset(0.0f, 0.0f);
|
||||
const ImVec2 shellLayoutOrigin = ImGui::GetCursorPos();
|
||||
const ImVec2 shellLayoutAvail = ImGui::GetContentRegionAvail();
|
||||
const float paneHeight = ImMax(0.0f, shellLayoutAvail.y);
|
||||
const float contentPaneWidth = ImMax(0.0f, shellLayoutAvail.x - sidebarWidth - paneGap);
|
||||
|
||||
const float sidebarPaneT = launcherIntroFinished ? 1.0f : EaseOutCubic(sidebarBuildT);
|
||||
const float sidebarPaneOffsetX = -(1.0f - sidebarPaneT) * 88.0f * uiScale;
|
||||
const float sidebarPaneOffsetY = (1.0f - sidebarPaneT) * 14.0f * uiScale;
|
||||
const float sidebarPaneAlpha = ImLerp(0.0f, 1.0f, sidebarPaneT);
|
||||
|
||||
const float contentPaneT = launcherIntroFinished ? 1.0f : EaseOutCubic(contentBuildT);
|
||||
const float contentPaneOffsetX = (1.0f - contentPaneT) * 96.0f * uiScale;
|
||||
const float contentPaneOffsetY = (1.0f - contentPaneT) * 12.0f * uiScale;
|
||||
const float contentPaneAlpha = ImLerp(0.0f, 1.0f, contentPaneT);
|
||||
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_Alpha, sidebarPaneAlpha);
|
||||
ImGui::SetCursorPos(ImVec2(shellLayoutOrigin.x + sidebarPaneOffsetX,
|
||||
shellLayoutOrigin.y + sidebarPaneOffsetY));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, sidebarPadding);
|
||||
ImGui::PushStyleColor(ImGuiCol_ChildBg, sidebarBg);
|
||||
ImGui::BeginChild("LauncherSidebar", ImVec2(sidebarWidth, 0), false);
|
||||
ImGui::BeginChild("LauncherSidebar", ImVec2(sidebarWidth, paneHeight), false);
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::PopStyleVar();
|
||||
|
||||
ImGui::TextColored(ImVec4(0.86f, 0.89f, 0.96f, 1.0f), "Modularity Project Manager");
|
||||
ImGui::Spacing();
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::Spacing();
|
||||
|
||||
int sidebarAnimIndex = 0;
|
||||
auto beginSidebarBuildStep = [&](float delayStep = 0.132f) {
|
||||
const float delay = std::min(0.86f, delayStep * static_cast<float>(sidebarAnimIndex));
|
||||
const float localT = ImClamp(
|
||||
(sidebarBuildT - delay) / std::max(0.0001f, 1.0f - delay),
|
||||
0.0f,
|
||||
1.0f);
|
||||
const float eased = EaseOutCubic(localT);
|
||||
const float alpha = ImLerp(0.0f, 1.0f, eased);
|
||||
const float yOffset = (1.0f - eased) * (24.0f + static_cast<float>(sidebarAnimIndex) * 2.0f) * uiScale;
|
||||
const float xOffset = -(1.0f - eased) * 34.0f * uiScale;
|
||||
ImGui::SetCursorPos(ImVec2(ImGui::GetCursorPosX() + xOffset, ImGui::GetCursorPosY() + yOffset));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_Alpha, alpha);
|
||||
++sidebarAnimIndex;
|
||||
};
|
||||
auto endSidebarBuildStep = [&]() {
|
||||
ImGui::PopStyleVar();
|
||||
};
|
||||
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(14.0f * uiScale, 10.0f * uiScale));
|
||||
beginSidebarBuildStep();
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, launcherSection == 0 ? ImVec4(0.21f, 0.32f, 0.47f, 1.0f) : ImVec4(0.16f, 0.19f, 0.28f, 0.95f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.24f, 0.36f, 0.54f, 1.0f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.21f, 0.32f, 0.47f, 1.0f));
|
||||
@@ -1013,6 +1119,9 @@ void Engine::renderLauncher() {
|
||||
setLauncherSection(0);
|
||||
}
|
||||
ImGui::PopStyleColor(3);
|
||||
endSidebarBuildStep();
|
||||
|
||||
beginSidebarBuildStep();
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, launcherSection == 1 ? ImVec4(0.21f, 0.32f, 0.47f, 1.0f) : ImVec4(0.16f, 0.19f, 0.28f, 0.95f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.24f, 0.36f, 0.54f, 1.0f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.21f, 0.32f, 0.47f, 1.0f));
|
||||
@@ -1020,6 +1129,9 @@ void Engine::renderLauncher() {
|
||||
setLauncherSection(1);
|
||||
}
|
||||
ImGui::PopStyleColor(3);
|
||||
|
||||
endSidebarBuildStep();
|
||||
beginSidebarBuildStep();
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, launcherSection == 2 ? ImVec4(0.21f, 0.32f, 0.47f, 1.0f) : ImVec4(0.16f, 0.19f, 0.28f, 0.95f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.24f, 0.36f, 0.54f, 1.0f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.21f, 0.32f, 0.47f, 1.0f));
|
||||
@@ -1027,12 +1139,14 @@ void Engine::renderLauncher() {
|
||||
setLauncherSection(2);
|
||||
}
|
||||
ImGui::PopStyleColor(3);
|
||||
endSidebarBuildStep();
|
||||
ImGui::PopStyleVar();
|
||||
const float footerY = ImGui::GetWindowHeight() - 60.0f * uiScale;
|
||||
if (ImGui::GetCursorPosY() < footerY) {
|
||||
ImGui::SetCursorPosY(footerY);
|
||||
}
|
||||
ImGui::Separator();
|
||||
beginSidebarBuildStep();
|
||||
if (ImGui::Button("Modularity Website", ImVec2(-1, 28.0f * uiScale))) {
|
||||
#ifdef _WIN32
|
||||
system("start https://moduengine.xyz");
|
||||
@@ -1040,6 +1154,8 @@ void Engine::renderLauncher() {
|
||||
system("xdg-open https://moduengine.xyz &");
|
||||
#endif
|
||||
}
|
||||
endSidebarBuildStep();
|
||||
beginSidebarBuildStep();
|
||||
if (ImGui::Button("Documentation", ImVec2(-1, 28.0f * uiScale))) {
|
||||
#ifdef _WIN32
|
||||
system("start https://moduengine.xyz/docs");
|
||||
@@ -1047,16 +1163,21 @@ void Engine::renderLauncher() {
|
||||
system("xdg-open https://moduengine.xyz/docs &");
|
||||
#endif
|
||||
}
|
||||
endSidebarBuildStep();
|
||||
beginSidebarBuildStep();
|
||||
if (ImGui::Button("Exit", ImVec2(-1, 28.0f * uiScale))) {
|
||||
glfwSetWindowShouldClose(editorWindow, GLFW_TRUE);
|
||||
}
|
||||
endSidebarBuildStep();
|
||||
ImGui::EndChild();
|
||||
ImGui::PopStyleVar();
|
||||
|
||||
ImGui::SameLine(0.0f, paneGap);
|
||||
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_Alpha, contentPaneAlpha);
|
||||
ImGui::SetCursorPos(ImVec2(shellLayoutOrigin.x + sidebarWidth + paneGap + contentPaneOffsetX,
|
||||
shellLayoutOrigin.y + contentPaneOffsetY));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, contentPadding);
|
||||
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.0f, 0.0f, 0.0f, 0.0f));
|
||||
ImGui::BeginChild("LauncherContent", ImVec2(0, 0), false);
|
||||
ImGui::BeginChild("LauncherContent", ImVec2(contentPaneWidth, paneHeight), false);
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::PopStyleVar();
|
||||
|
||||
@@ -1070,8 +1191,14 @@ void Engine::renderLauncher() {
|
||||
const float projectsInsetX = 10.0f * uiScale;
|
||||
const float projectsInsetY = 6.0f * uiScale;
|
||||
const float projectsHeaderGap = 12.0f * uiScale;
|
||||
const float panelBuildT = launcherIntroFinished ? 1.0f : EaseOutCubic(contentBuildT);
|
||||
const float headerAlpha = ImLerp(0.0f, 1.0f, panelBuildT);
|
||||
const float headerOffsetY = (1.0f - panelBuildT) * 24.0f * uiScale;
|
||||
const float headerOffsetX = (1.0f - panelBuildT) * 30.0f * uiScale;
|
||||
ImGui::SetCursorPos(ImVec2(ImGui::GetCursorPosX() + projectsInsetX,
|
||||
ImGui::GetCursorPosY() + projectsInsetY));
|
||||
ImGui::GetCursorPosY() + projectsInsetY + headerOffsetY));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_Alpha, headerAlpha);
|
||||
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + headerOffsetX);
|
||||
ImGui::TextColored(ImVec4(0.92f, 0.94f, 0.98f, 1.0f), "Projects");
|
||||
const float buttonWidth = 112.0f * uiScale;
|
||||
const float spacing = ImGui::GetStyle().ItemSpacing.x;
|
||||
@@ -1113,12 +1240,15 @@ void Engine::renderLauncher() {
|
||||
ImGui::Dummy(ImVec2(0.0f, projectsHeaderGap));
|
||||
ImGui::Separator();
|
||||
ImGui::Dummy(ImVec2(0.0f, projectsHeaderGap));
|
||||
ImGui::PopStyleVar();
|
||||
|
||||
const std::string filter = toLower(TrimCopy(launcherSearch));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
|
||||
ImGui::BeginChild("RecentProjectsList", ImVec2(0, 0), false);
|
||||
bool hadVisible = false;
|
||||
bool removedProject = false;
|
||||
int visibleRowIndex = 0;
|
||||
const float cardsBuildT = launcherIntroFinished ? 1.0f : EaseOutCubic(contentBuildT);
|
||||
|
||||
for (size_t i = 0; i < projectManager.recentProjects.size(); ++i) {
|
||||
const auto& rp = projectManager.recentProjects[i];
|
||||
@@ -1128,6 +1258,16 @@ void Engine::renderLauncher() {
|
||||
}
|
||||
|
||||
hadVisible = true;
|
||||
const int rowIndex = visibleRowIndex++;
|
||||
const float rowDelay = std::min(0.74f, static_cast<float>(rowIndex) * 0.072f);
|
||||
const float rowInput = ImClamp(
|
||||
(cardsBuildT - rowDelay) / std::max(0.0001f, 1.0f - rowDelay),
|
||||
0.0f,
|
||||
1.0f);
|
||||
const float rowReveal = EaseOutCubic(rowInput);
|
||||
const float rowSlideX = (1.0f - rowReveal) * 56.0f * uiScale;
|
||||
const float rowSlideY = (1.0f - rowReveal) * (20.0f + static_cast<float>(rowIndex) * 2.8f) * uiScale;
|
||||
const float rowAlpha = ImLerp(0.0f, 1.0f, rowReveal);
|
||||
ImGui::PushID(static_cast<int>(i));
|
||||
|
||||
ImTextureID previewTexId = static_cast<ImTextureID>(0);
|
||||
@@ -1150,6 +1290,8 @@ void Engine::renderLauncher() {
|
||||
|
||||
const float cardHeight = 92.0f * uiScale;
|
||||
const ImVec2 cardSize(ImGui::GetContentRegionAvail().x, cardHeight);
|
||||
const ImVec2 rowBasePos = ImGui::GetCursorScreenPos();
|
||||
ImGui::SetCursorScreenPos(ImVec2(rowBasePos.x + rowSlideX, rowBasePos.y + rowSlideY));
|
||||
const ImVec2 cardPos = ImGui::GetCursorScreenPos();
|
||||
ImGui::InvisibleButton("RecentProjectCard", cardSize);
|
||||
const bool hovered = ImGui::IsItemHovered();
|
||||
@@ -1172,20 +1314,23 @@ void Engine::renderLauncher() {
|
||||
}
|
||||
|
||||
ImDrawList* list = ImGui::GetWindowDrawList();
|
||||
auto withRowAlpha = [rowAlpha](const ImVec4& col) {
|
||||
return ImVec4(col.x, col.y, col.z, col.w * rowAlpha);
|
||||
};
|
||||
const float hoverT = EaseOutCubic(hovered ? 1.0f : 0.0f);
|
||||
const ImU32 cardBg = ImGui::GetColorU32(hovered
|
||||
? ImVec4(0.18f, 0.22f, 0.31f, 1.0f)
|
||||
: ImVec4(0.14f, 0.16f, 0.22f, 0.98f));
|
||||
? withRowAlpha(ImVec4(0.18f, 0.22f, 0.31f, 1.0f))
|
||||
: withRowAlpha(ImVec4(0.14f, 0.16f, 0.22f, 0.98f)));
|
||||
const ImU32 cardBorder = ImGui::GetColorU32(hovered
|
||||
? ImVec4(0.30f, 0.52f, 0.80f, 1.0f)
|
||||
: ImVec4(0.22f, 0.27f, 0.36f, 0.95f));
|
||||
? withRowAlpha(ImVec4(0.30f, 0.52f, 0.80f, 1.0f))
|
||||
: withRowAlpha(ImVec4(0.22f, 0.27f, 0.36f, 0.95f)));
|
||||
const float cardRounding = 12.0f * uiScale;
|
||||
list->AddRectFilled(cardPos, ImVec2(cardPos.x + cardSize.x, cardPos.y + cardSize.y), cardBg, cardRounding);
|
||||
list->AddRect(cardPos, ImVec2(cardPos.x + cardSize.x, cardPos.y + cardSize.y), cardBorder, cardRounding, 0, hovered ? 2.0f : 1.0f);
|
||||
if (hovered) {
|
||||
list->AddRectFilled(cardPos,
|
||||
ImVec2(cardPos.x + cardSize.x, cardPos.y + cardSize.y),
|
||||
ImGui::GetColorU32(ImVec4(0.22f, 0.36f, 0.58f, 0.06f + 0.08f * hoverT)),
|
||||
ImGui::GetColorU32(withRowAlpha(ImVec4(0.22f, 0.36f, 0.58f, 0.06f + 0.08f * hoverT))),
|
||||
cardRounding);
|
||||
}
|
||||
|
||||
@@ -1195,21 +1340,22 @@ void Engine::renderLauncher() {
|
||||
const ImVec2 thumbMax(thumbMin.x + thumbSize.x, thumbMin.y + thumbSize.y);
|
||||
if (previewTexId != static_cast<ImTextureID>(0)) {
|
||||
DrawImageCover(list, previewTexId, thumbMin, thumbMax, previewTexWidth, previewTexHeight,
|
||||
ImGui::GetColorU32(ImVec4(1, 1, 1, 1)), 8.0f * uiScale);
|
||||
ImGui::GetColorU32(withRowAlpha(ImVec4(1, 1, 1, 1))), 8.0f * uiScale);
|
||||
} else {
|
||||
list->AddRectFilled(thumbMin, thumbMax, ImGui::GetColorU32(ImVec4(0.11f, 0.13f, 0.18f, 1.0f)), 8.0f * uiScale);
|
||||
list->AddRectFilled(thumbMin, thumbMax, ImGui::GetColorU32(withRowAlpha(ImVec4(0.11f, 0.13f, 0.18f, 1.0f))), 8.0f * uiScale);
|
||||
const char* noPreview = "No Preview";
|
||||
ImVec2 txt = ImGui::CalcTextSize(noPreview);
|
||||
list->AddText(ImVec2(thumbMin.x + (thumbSize.x - txt.x) * 0.5f,
|
||||
thumbMin.y + (thumbSize.y - txt.y) * 0.5f),
|
||||
ImGui::GetColorU32(ImVec4(0.66f, 0.70f, 0.78f, 1.0f)),
|
||||
ImGui::GetColorU32(withRowAlpha(ImVec4(0.66f, 0.70f, 0.78f, 1.0f))),
|
||||
noPreview);
|
||||
}
|
||||
list->AddRect(thumbMin, thumbMax, ImGui::GetColorU32(ImVec4(0.24f, 0.28f, 0.38f, 0.95f)), 8.0f * uiScale);
|
||||
list->AddRect(thumbMin, thumbMax, ImGui::GetColorU32(withRowAlpha(ImVec4(0.24f, 0.28f, 0.38f, 0.95f))), 8.0f * uiScale);
|
||||
|
||||
const float actionWidth = 112.0f * uiScale;
|
||||
const float textStartX = thumbMax.x + 16.0f * uiScale;
|
||||
const float textWidth = std::max(140.0f * uiScale, cardMax.x - textStartX - actionWidth - 28.0f * uiScale);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_Alpha, rowAlpha);
|
||||
ImGui::SetCursorScreenPos(ImVec2(textStartX, cardPos.y + 15.0f * uiScale));
|
||||
ImGui::PushTextWrapPos(textStartX + textWidth);
|
||||
ImGui::TextColored(ImVec4(0.92f, 0.94f, 0.98f, 1.0f), "%s",
|
||||
@@ -1235,6 +1381,7 @@ void Engine::renderLauncher() {
|
||||
}
|
||||
ImGui::PopStyleColor(3);
|
||||
ImGui::EndDisabled();
|
||||
ImGui::PopStyleVar();
|
||||
|
||||
if (shouldRemove) {
|
||||
projectManager.recentProjects.erase(
|
||||
@@ -1250,7 +1397,7 @@ void Engine::renderLauncher() {
|
||||
launchRecentProject(rp, rowCenter);
|
||||
}
|
||||
|
||||
ImGui::SetCursorScreenPos(ImVec2(cardPos.x, cardPos.y + cardHeight));
|
||||
ImGui::SetCursorScreenPos(ImVec2(rowBasePos.x, rowBasePos.y + cardHeight));
|
||||
ImGui::Dummy(ImVec2(cardSize.x, 5.0f * uiScale));
|
||||
ImGui::PopID();
|
||||
}
|
||||
@@ -1661,6 +1808,7 @@ void Engine::renderLauncher() {
|
||||
ImGui::PopStyleVar(2);
|
||||
|
||||
ImGui::EndChild();
|
||||
ImGui::PopStyleVar();
|
||||
ImGui::EndChild();
|
||||
ImGui::PopStyleVar();
|
||||
|
||||
@@ -1670,54 +1818,87 @@ void Engine::renderLauncher() {
|
||||
ImGui::SetWindowFontScale(1.0f);
|
||||
ImGui::PopStyleVar();
|
||||
|
||||
if (textAlpha > 0.001f) {
|
||||
ImDrawList* overlay = ImGui::GetWindowDrawList();
|
||||
if (introState.textAlpha > 0.001f) {
|
||||
ImDrawList* overlay = ImGui::GetForegroundDrawList(ImGui::GetMainViewport());
|
||||
const char* title = "Modularity";
|
||||
ImFont* font = ImGui::GetFont();
|
||||
const float baseFontSize = ImGui::GetFontSize();
|
||||
const float fadeOutT = ImClamp((introElapsed - (introFadeIn + introHold)) / introFadeOut, 0.0f, 1.0f);
|
||||
const float globalScale = 2.0f - 0.18f * fadeOutT;
|
||||
const float fontSizeNormal = baseFontSize * 2.6f * globalScale;
|
||||
const float centerFontSize = baseFontSize * 2.62f;
|
||||
const float headerFontSize = baseFontSize * 1.24f;
|
||||
const ImVec4 baseCol = ImVec4(0.95f, 0.96f, 0.98f, 1.0f);
|
||||
const float letterDelay = 0.07f;
|
||||
const float letterIn = 0.18f;
|
||||
|
||||
auto ease = [](float t) {
|
||||
t = ImClamp(t, 0.0f, 1.0f);
|
||||
return t * t * (3.0f - 2.0f * t);
|
||||
};
|
||||
|
||||
ImVec2 center = ImVec2(displaySize.x * 0.5f, displaySize.y * 0.38f);
|
||||
float totalWidth = 0.0f;
|
||||
ImVec2 center = ImVec2(displaySize.x * 0.5f, displaySize.y * 0.36f);
|
||||
float centerTotalWidth = 0.0f;
|
||||
float headerTotalWidth = 0.0f;
|
||||
for (const char* c = title; *c; ++c) {
|
||||
char letter[2] = { *c, 0 };
|
||||
totalWidth += font->CalcTextSizeA(fontSizeNormal, FLT_MAX, 0.0f, letter).x;
|
||||
centerTotalWidth += font->CalcTextSizeA(centerFontSize, FLT_MAX, 0.0f, letter).x;
|
||||
headerTotalWidth += font->CalcTextSizeA(headerFontSize, FLT_MAX, 0.0f, letter).x;
|
||||
}
|
||||
|
||||
ImVec2 textPos = ImVec2(center.x - totalWidth * 0.5f,
|
||||
center.y - fontSizeNormal * 0.5f);
|
||||
const ImVec2 centerTextPos(center.x - centerTotalWidth * 0.5f,
|
||||
center.y - centerFontSize * 0.5f);
|
||||
const float headerMarginLeft = 26.0f * uiScale;
|
||||
const float headerTop = windowPos.y + 14.0f * uiScale;
|
||||
const ImVec2 headerTextPos(
|
||||
windowPos.x + headerMarginLeft,
|
||||
headerTop);
|
||||
|
||||
float advanceX = 0.0f;
|
||||
float centerAdvanceX = 0.0f;
|
||||
float headerAdvanceX = 0.0f;
|
||||
int index = 0;
|
||||
const int totalLetters = static_cast<int>(std::strlen(title));
|
||||
const float popDelayStep = 0.055f;
|
||||
const float driftDelayStep = 0.024f;
|
||||
for (const char* c = title; *c; ++c, ++index) {
|
||||
char letter[2] = { *c, 0 };
|
||||
float letterT = (introElapsed - (letterDelay * index)) / letterIn;
|
||||
float letterEase = ease(letterT);
|
||||
float letterAlpha = textAlpha * letterEase;
|
||||
if (letterAlpha <= 0.001f) {
|
||||
float letterWidth = font->CalcTextSizeA(fontSizeNormal, FLT_MAX, 0.0f, letter).x;
|
||||
advanceX += letterWidth;
|
||||
continue;
|
||||
}
|
||||
const float popDelay = (totalLetters > 1) ? (popDelayStep * static_cast<float>(index)) : 0.0f;
|
||||
const float popInput = ImClamp(
|
||||
(introState.elapsed - introTimings.fadeIn - popDelay) / std::max(0.0001f, introTimings.popIn),
|
||||
0.0f,
|
||||
1.0f);
|
||||
const float popEase = EaseOutBack(popInput);
|
||||
const float popAlpha = EaseOutCubic(popInput);
|
||||
|
||||
float letterScale = (1.15f - 0.15f * letterEase) * globalScale;
|
||||
float letterFontSize = baseFontSize * 2.6f * letterScale;
|
||||
float letterWidth = font->CalcTextSizeA(fontSizeNormal, FLT_MAX, 0.0f, letter).x;
|
||||
float offsetX = (letterWidth * (letterScale / globalScale - 1.0f)) * 0.5f;
|
||||
ImVec2 letterPos = ImVec2(textPos.x + advanceX - offsetX, textPos.y);
|
||||
ImU32 textCol = ImGui::GetColorU32(ImVec4(baseCol.x, baseCol.y, baseCol.z, letterAlpha));
|
||||
overlay->AddText(font, letterFontSize, letterPos, textCol, letter);
|
||||
advanceX += letterWidth;
|
||||
const float driftDelay = (totalLetters > 1) ? (driftDelayStep * static_cast<float>(index)) : 0.0f;
|
||||
const float driftInput = ImClamp(
|
||||
(introState.driftT - driftDelay) / std::max(0.0001f, 1.0f - driftDelay),
|
||||
0.0f,
|
||||
1.0f);
|
||||
const float driftEase = EaseInOutCubic(driftInput);
|
||||
|
||||
const float centerWidth = font->CalcTextSizeA(centerFontSize, FLT_MAX, 0.0f, letter).x;
|
||||
const float headerWidth = font->CalcTextSizeA(headerFontSize, FLT_MAX, 0.0f, letter).x;
|
||||
const ImVec2 centerLetterPos(centerTextPos.x + centerAdvanceX, centerTextPos.y);
|
||||
const ImVec2 headerLetterPos(headerTextPos.x + headerAdvanceX, headerTextPos.y);
|
||||
|
||||
const float popScale = std::max(0.05f, 0.34f + 0.66f * popEase);
|
||||
const float popSize = centerFontSize * popScale;
|
||||
const ImVec2 spawnPos(centerLetterPos.x, centerLetterPos.y + (1.0f - popAlpha) * 22.0f * uiScale);
|
||||
const ImVec2 popPos(
|
||||
ImLerp(spawnPos.x, centerLetterPos.x, popAlpha),
|
||||
ImLerp(spawnPos.y, centerLetterPos.y, popAlpha));
|
||||
|
||||
const float letterSize = ImLerp(popSize, headerFontSize, driftEase);
|
||||
const ImVec2 letterPos(
|
||||
ImLerp(popPos.x, headerLetterPos.x, driftEase),
|
||||
ImLerp(popPos.y, headerLetterPos.y, driftEase));
|
||||
const float letterAlpha = introState.textAlpha * popAlpha * ImLerp(0.92f, 1.0f, driftEase);
|
||||
const ImU32 textCol = ImGui::GetColorU32(ImVec4(baseCol.x, baseCol.y, baseCol.z, letterAlpha));
|
||||
overlay->AddText(font, letterSize, letterPos, textCol, letter);
|
||||
|
||||
centerAdvanceX += centerWidth;
|
||||
headerAdvanceX += headerWidth;
|
||||
}
|
||||
|
||||
const float packageTagAlpha = introState.textAlpha * ImClamp((introState.driftT - 0.23f) / 0.62f, 0.0f, 1.0f);
|
||||
if (packageTagAlpha > 0.001f) {
|
||||
const char* packageLabel = "Project manager";
|
||||
const float packageFontSize = baseFontSize * 0.95f;
|
||||
const ImU32 tagCol = ImGui::GetColorU32(ImVec4(0.79f, 0.84f, 0.92f, packageTagAlpha));
|
||||
const ImVec2 tagPos(headerTextPos.x + headerTotalWidth + 14.0f * uiScale,
|
||||
headerTextPos.y + headerFontSize * 0.18f);
|
||||
overlay->AddText(font, packageFontSize, tagPos, tagCol, packageLabel);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2075,6 +2256,7 @@ void Engine::renderProjectBrowserPanel() {
|
||||
static char gitUrlBuf[256] = "";
|
||||
static char gitNameBuf[128] = "";
|
||||
static char gitIncludeBuf[128] = "include";
|
||||
static char moduPakPathBuf[512] = "";
|
||||
|
||||
auto pollPackageTask = [&]() {
|
||||
if (!packageTask.active) return;
|
||||
@@ -2139,6 +2321,25 @@ void Engine::renderProjectBrowserPanel() {
|
||||
});
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::TextDisabled("Install from .modupak");
|
||||
ImGui::InputTextWithHint("Bundle path", "/path/to/package.modupak", moduPakPathBuf, sizeof(moduPakPathBuf));
|
||||
if (ImGui::Button("Install .modupak")) {
|
||||
const std::string bundlePath = TrimCopy(moduPakPathBuf);
|
||||
startPackageTask("Importing .modupak...", [this, bundlePath]() {
|
||||
PackageTaskResult result;
|
||||
std::string newId;
|
||||
if (packageManager.installModuPak(fs::path(bundlePath), newId)) {
|
||||
result.success = true;
|
||||
result.message = "Installed .modupak package: " + newId;
|
||||
} else {
|
||||
result.message = packageManager.getLastError();
|
||||
}
|
||||
return result;
|
||||
});
|
||||
}
|
||||
ImGui::TextDisabled("Expected bundle layout: manifest.modu + payload/ (tar archive or .modupak folder).");
|
||||
|
||||
ImGui::EndDisabled();
|
||||
ImGui::Separator();
|
||||
ImGui::TextDisabled("Installed packages");
|
||||
@@ -2151,57 +2352,74 @@ void Engine::renderProjectBrowserPanel() {
|
||||
ImGui::TextDisabled("None installed");
|
||||
} else {
|
||||
for (const auto& id : installedIds) {
|
||||
const PackageInfo* pkg = nullptr;
|
||||
for (const auto& p : registry) {
|
||||
if (p.id == id) { pkg = &p; break; }
|
||||
}
|
||||
const PackageInfo* pkg = FindRegistryPackageById(registry, id);
|
||||
if (!pkg) continue;
|
||||
|
||||
ImGui::PushID(pkg->id.c_str());
|
||||
ImGui::Separator();
|
||||
ImGui::Text("%s", pkg->name.c_str());
|
||||
ImGui::TextDisabled("%s", pkg->description.c_str());
|
||||
if (!pkg->external) {
|
||||
ImGui::SameLine();
|
||||
ImGui::TextColored(ImVec4(0.4f, 0.7f, 0.95f, 1.0f), "[bundled]");
|
||||
|
||||
const char* sourceBadge = "[bundled]";
|
||||
ImVec4 badgeColor = ImVec4(0.4f, 0.7f, 0.95f, 1.0f);
|
||||
if (pkg->modupak) {
|
||||
sourceBadge = "[.modupak]";
|
||||
badgeColor = ImVec4(0.62f, 0.86f, 0.68f, 1.0f);
|
||||
} else if (pkg->external) {
|
||||
sourceBadge = "[git]";
|
||||
badgeColor = ImVec4(0.96f, 0.79f, 0.49f, 1.0f);
|
||||
}
|
||||
ImGui::SameLine();
|
||||
ImGui::TextColored(badgeColor, "%s", sourceBadge);
|
||||
|
||||
if (pkg->external) {
|
||||
ImGui::TextDisabled("Path: %s", pkg->localPath.string().c_str());
|
||||
}
|
||||
if (pkg->modupak && !pkg->modupakSourcePath.empty()) {
|
||||
ImGui::TextDisabled("Bundle: %s", pkg->modupakSourcePath.string().c_str());
|
||||
} else if (pkg->external && !pkg->gitUrl.empty()) {
|
||||
ImGui::TextDisabled("Git: %s", pkg->gitUrl.c_str());
|
||||
}
|
||||
if (pkg->builtIn) {
|
||||
ImGui::TextDisabled("Required package.");
|
||||
ImGui::PopID();
|
||||
continue;
|
||||
}
|
||||
|
||||
ImGui::TextDisabled("Path: %s", pkg->localPath.string().c_str());
|
||||
ImGui::TextDisabled("Git: %s", pkg->gitUrl.c_str());
|
||||
if (pkg->external && !pkg->gitUrl.empty()) {
|
||||
if (ImGui::Button("Check updates")) {
|
||||
std::string id = pkg->id;
|
||||
startPackageTask("Checking package status...", [this, id]() {
|
||||
PackageTaskResult result;
|
||||
std::string status;
|
||||
if (packageManager.checkGitStatus(id, status)) {
|
||||
result.success = true;
|
||||
result.message = status;
|
||||
} else {
|
||||
result.message = packageManager.getLastError();
|
||||
}
|
||||
return result;
|
||||
});
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Update")) {
|
||||
std::string id = pkg->id;
|
||||
std::string name = pkg->name;
|
||||
startPackageTask("Updating package...", [this, id, name]() {
|
||||
PackageTaskResult result;
|
||||
std::string log;
|
||||
if (packageManager.updateGitPackage(id, log)) {
|
||||
result.success = true;
|
||||
result.message = "Updated " + name + "\n" + log;
|
||||
} else {
|
||||
result.message = packageManager.getLastError();
|
||||
}
|
||||
return result;
|
||||
});
|
||||
}
|
||||
ImGui::SameLine();
|
||||
}
|
||||
|
||||
if (ImGui::Button("Check updates")) {
|
||||
std::string id = pkg->id;
|
||||
startPackageTask("Checking package status...", [this, id]() {
|
||||
PackageTaskResult result;
|
||||
std::string status;
|
||||
if (packageManager.checkGitStatus(id, status)) {
|
||||
result.success = true;
|
||||
result.message = status;
|
||||
} else {
|
||||
result.message = packageManager.getLastError();
|
||||
}
|
||||
return result;
|
||||
});
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Update")) {
|
||||
std::string id = pkg->id;
|
||||
std::string name = pkg->name;
|
||||
startPackageTask("Updating package...", [this, id, name]() {
|
||||
PackageTaskResult result;
|
||||
std::string log;
|
||||
if (packageManager.updateGitPackage(id, log)) {
|
||||
result.success = true;
|
||||
result.message = "Updated " + name + "\n" + log;
|
||||
} else {
|
||||
result.message = packageManager.getLastError();
|
||||
}
|
||||
return result;
|
||||
});
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Uninstall")) {
|
||||
std::string id = pkg->id;
|
||||
std::string name = pkg->name;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -457,9 +457,11 @@ void Engine::openScriptInEditor(const fs::path& path) {
|
||||
void Engine::renderScriptingWindow() {
|
||||
if (!showScriptingWindow) return;
|
||||
|
||||
bool listRefreshedThisFrame = false;
|
||||
if (scriptingFilesDirty) {
|
||||
refreshScriptingFileList();
|
||||
scriptingFilesDirty = false;
|
||||
listRefreshedThisFrame = true;
|
||||
}
|
||||
|
||||
ImGui::Begin("Scripting", &showScriptingWindow);
|
||||
@@ -480,7 +482,17 @@ void Engine::renderScriptingWindow() {
|
||||
static std::vector<std::string> completionPool;
|
||||
static std::vector<std::string> activeSuggestions;
|
||||
static std::string activePrefix;
|
||||
static bool completionPoolDirty = true;
|
||||
static bool completionPanelOpen = true;
|
||||
static fs::path reloadCheckPath;
|
||||
static bool cachedCanReload = false;
|
||||
static double lastReloadCheckTime = 0.0;
|
||||
static std::string cachedFilterLower;
|
||||
static std::vector<int> filteredScriptIndices;
|
||||
|
||||
if (listRefreshedThisFrame) {
|
||||
completionPoolDirty = true;
|
||||
}
|
||||
|
||||
ImGui::TextDisabled("Script & Shader Editor");
|
||||
ImGui::SameLine();
|
||||
@@ -493,21 +505,35 @@ void Engine::renderScriptingWindow() {
|
||||
float leftWidth = 240.0f;
|
||||
ImGui::BeginChild("ScriptingFiles", ImVec2(leftWidth, 0.0f), true);
|
||||
ImGui::TextDisabled("Scripts / Shaders");
|
||||
ImGui::InputTextWithHint("##ScriptFilter", "Filter", scriptingFilter, sizeof(scriptingFilter));
|
||||
bool filterChanged = ImGui::InputTextWithHint("##ScriptFilter", "Filter", scriptingFilter, sizeof(scriptingFilter));
|
||||
ImGui::Separator();
|
||||
|
||||
for (const auto& scriptPath : scriptingFileList) {
|
||||
std::string label = scriptPath.filename().string();
|
||||
std::string filter = scriptingFilter;
|
||||
std::transform(filter.begin(), filter.end(), filter.begin(), ::tolower);
|
||||
std::string lowerLabel = label;
|
||||
std::transform(lowerLabel.begin(), lowerLabel.end(), lowerLabel.begin(), ::tolower);
|
||||
if (!filter.empty() && lowerLabel.find(filter) == std::string::npos) {
|
||||
continue;
|
||||
std::string filterLower = toLowerCopy(scriptingFilter);
|
||||
if (listRefreshedThisFrame || filterChanged || filterLower != cachedFilterLower) {
|
||||
cachedFilterLower = filterLower;
|
||||
filteredScriptIndices.clear();
|
||||
filteredScriptIndices.reserve(scriptingFileList.size());
|
||||
for (size_t i = 0; i < scriptingFileList.size(); ++i) {
|
||||
if (!cachedFilterLower.empty()) {
|
||||
std::string labelLower = toLowerCopy(scriptingFileList[i].filename().string());
|
||||
if (labelLower.find(cachedFilterLower) == std::string::npos) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
filteredScriptIndices.push_back(static_cast<int>(i));
|
||||
}
|
||||
bool selected = (scriptEditorState.filePath == scriptPath);
|
||||
if (ImGui::Selectable(label.c_str(), selected)) {
|
||||
openScriptInEditor(scriptPath);
|
||||
}
|
||||
|
||||
ImGuiListClipper clipper;
|
||||
clipper.Begin(static_cast<int>(filteredScriptIndices.size()));
|
||||
while (clipper.Step()) {
|
||||
for (int row = clipper.DisplayStart; row < clipper.DisplayEnd; ++row) {
|
||||
const fs::path& scriptPath = scriptingFileList[filteredScriptIndices[row]];
|
||||
std::string label = scriptPath.filename().string();
|
||||
bool selected = (scriptEditorState.filePath == scriptPath);
|
||||
if (ImGui::Selectable(label.c_str(), selected)) {
|
||||
openScriptInEditor(scriptPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
ImGui::EndChild();
|
||||
@@ -537,6 +563,8 @@ void Engine::renderScriptingWindow() {
|
||||
std::error_code ec;
|
||||
scriptEditorState.lastWriteTime = fs::last_write_time(scriptEditorState.filePath, ec);
|
||||
scriptEditorState.hasWriteTime = !ec;
|
||||
cachedCanReload = false;
|
||||
lastReloadCheckTime = glfwGetTime();
|
||||
if (scriptEditorState.autoCompileOnSave && canCompileFile) {
|
||||
compileScriptFile(scriptEditorState.filePath);
|
||||
}
|
||||
@@ -565,14 +593,25 @@ void Engine::renderScriptingWindow() {
|
||||
}
|
||||
|
||||
bool canReload = false;
|
||||
if (reloadCheckPath != scriptEditorState.filePath) {
|
||||
reloadCheckPath = scriptEditorState.filePath;
|
||||
cachedCanReload = false;
|
||||
lastReloadCheckTime = 0.0;
|
||||
}
|
||||
if (hasFile && scriptEditorState.hasWriteTime) {
|
||||
std::error_code ec;
|
||||
if (fs::exists(scriptEditorState.filePath, ec)) {
|
||||
auto diskTime = fs::last_write_time(scriptEditorState.filePath, ec);
|
||||
if (!ec && diskTime > scriptEditorState.lastWriteTime) {
|
||||
canReload = true;
|
||||
const double now = glfwGetTime();
|
||||
if (now - lastReloadCheckTime >= 0.25) {
|
||||
lastReloadCheckTime = now;
|
||||
cachedCanReload = false;
|
||||
std::error_code ec;
|
||||
if (fs::exists(scriptEditorState.filePath, ec)) {
|
||||
auto diskTime = fs::last_write_time(scriptEditorState.filePath, ec);
|
||||
if (!ec && diskTime > scriptEditorState.lastWriteTime) {
|
||||
cachedCanReload = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
canReload = cachedCanReload;
|
||||
}
|
||||
if (canReload) {
|
||||
ImGui::SameLine();
|
||||
@@ -580,6 +619,8 @@ void Engine::renderScriptingWindow() {
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Reload")) {
|
||||
openScriptInEditor(scriptEditorState.filePath);
|
||||
cachedCanReload = false;
|
||||
lastReloadCheckTime = glfwGetTime();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -593,62 +634,81 @@ void Engine::renderScriptingWindow() {
|
||||
scriptTextEditorReady = true;
|
||||
}
|
||||
|
||||
ScriptEditorLanguage language = detectScriptEditorLanguage(scriptEditorState.filePath);
|
||||
uint64_t bufferHash = hashBuffer(scriptEditorState.buffer);
|
||||
bool fileChanged = (identifiersFilePath != scriptEditorState.filePath);
|
||||
bool languageChanged = (activeLanguage != language);
|
||||
if (bufferHash != identifiersHash || fileChanged || languageChanged) {
|
||||
identifiersHash = bufferHash;
|
||||
identifiersFilePath = scriptEditorState.filePath;
|
||||
activeLanguage = language;
|
||||
ScriptEditorLanguage language = detectScriptEditorLanguage(scriptEditorState.filePath);
|
||||
uint64_t bufferHash = hashBuffer(scriptEditorState.buffer);
|
||||
bool fileChanged = (identifiersFilePath != scriptEditorState.filePath);
|
||||
bool languageChanged = (activeLanguage != language);
|
||||
if (bufferHash != identifiersHash || fileChanged || languageChanged) {
|
||||
identifiersHash = bufferHash;
|
||||
identifiersFilePath = scriptEditorState.filePath;
|
||||
activeLanguage = language;
|
||||
|
||||
const auto& keywords = keywordsForLanguage(language);
|
||||
bufferIdentifiers = extractIdentifiers(scriptEditorState.buffer, keywords);
|
||||
bufferFunctions = extractFunctionIdentifiers(scriptEditorState.buffer, keywords);
|
||||
bufferDefines = extractDefineIdentifiers(scriptEditorState.buffer);
|
||||
scriptTextEditor.SetLanguageDefinition(buildLanguageDefinition(language, bufferFunctions, bufferDefines));
|
||||
}
|
||||
|
||||
completionPool.clear();
|
||||
std::unordered_set<std::string> poolSet;
|
||||
const auto& langDef = scriptTextEditor.GetLanguageDefinition();
|
||||
for (const auto& kw : langDef.mKeywords) {
|
||||
poolSet.insert(kw);
|
||||
}
|
||||
for (const auto& identifier : langDef.mIdentifiers) {
|
||||
poolSet.insert(identifier.first);
|
||||
}
|
||||
for (const auto& identifier : langDef.mPreprocIdentifiers) {
|
||||
poolSet.insert(identifier.first);
|
||||
}
|
||||
for (const auto& entry : scriptingCompletions) {
|
||||
poolSet.insert(entry);
|
||||
}
|
||||
for (const auto& entry : symbols) {
|
||||
poolSet.insert(entry);
|
||||
}
|
||||
for (const auto& entry : bufferIdentifiers) {
|
||||
poolSet.insert(entry);
|
||||
}
|
||||
for (const auto& entry : bufferFunctions) {
|
||||
poolSet.insert(entry);
|
||||
}
|
||||
for (const auto& entry : bufferDefines) {
|
||||
poolSet.insert(entry);
|
||||
}
|
||||
completionPool.assign(poolSet.begin(), poolSet.end());
|
||||
std::sort(completionPool.begin(), completionPool.end());
|
||||
|
||||
TextEditor::Coordinates cursorBefore = scriptTextEditor.GetCursorPosition();
|
||||
activePrefix = scriptTextEditor.GetWordAtPublic(cursorBefore);
|
||||
if (activePrefix.empty() && cursorBefore.mColumn > 0) {
|
||||
TextEditor::Coordinates prev(cursorBefore.mLine, cursorBefore.mColumn - 1);
|
||||
activePrefix = scriptTextEditor.GetWordAtPublic(prev);
|
||||
const auto& keywords = keywordsForLanguage(language);
|
||||
bufferIdentifiers = extractIdentifiers(scriptEditorState.buffer, keywords);
|
||||
bufferFunctions = extractFunctionIdentifiers(scriptEditorState.buffer, keywords);
|
||||
bufferDefines = extractDefineIdentifiers(scriptEditorState.buffer);
|
||||
scriptTextEditor.SetLanguageDefinition(buildLanguageDefinition(language, bufferFunctions, bufferDefines));
|
||||
completionPoolDirty = true;
|
||||
}
|
||||
if (!activePrefix.empty() && activePrefix.size() >= 2) {
|
||||
activeSuggestions = buildCompletionList(completionPool, activePrefix);
|
||||
} else {
|
||||
activeSuggestions.clear();
|
||||
|
||||
auto rebuildCompletionPool = [&]() -> bool {
|
||||
if (!completionPoolDirty) return false;
|
||||
completionPool.clear();
|
||||
std::unordered_set<std::string> poolSet;
|
||||
const auto& langDef = scriptTextEditor.GetLanguageDefinition();
|
||||
for (const auto& kw : langDef.mKeywords) {
|
||||
poolSet.insert(kw);
|
||||
}
|
||||
for (const auto& identifier : langDef.mIdentifiers) {
|
||||
poolSet.insert(identifier.first);
|
||||
}
|
||||
for (const auto& identifier : langDef.mPreprocIdentifiers) {
|
||||
poolSet.insert(identifier.first);
|
||||
}
|
||||
for (const auto& entry : scriptingCompletions) {
|
||||
poolSet.insert(entry);
|
||||
}
|
||||
for (const auto& entry : symbols) {
|
||||
poolSet.insert(entry);
|
||||
}
|
||||
for (const auto& entry : bufferIdentifiers) {
|
||||
poolSet.insert(entry);
|
||||
}
|
||||
for (const auto& entry : bufferFunctions) {
|
||||
poolSet.insert(entry);
|
||||
}
|
||||
for (const auto& entry : bufferDefines) {
|
||||
poolSet.insert(entry);
|
||||
}
|
||||
completionPool.assign(poolSet.begin(), poolSet.end());
|
||||
std::sort(completionPool.begin(), completionPool.end());
|
||||
completionPoolDirty = false;
|
||||
return true;
|
||||
};
|
||||
|
||||
auto extractPrefixAtCursor = [&]() {
|
||||
TextEditor::Coordinates cursor = scriptTextEditor.GetCursorPosition();
|
||||
std::string prefix = scriptTextEditor.GetWordAtPublic(cursor);
|
||||
if (prefix.empty() && cursor.mColumn > 0) {
|
||||
TextEditor::Coordinates prev(cursor.mLine, cursor.mColumn - 1);
|
||||
prefix = scriptTextEditor.GetWordAtPublic(prev);
|
||||
}
|
||||
return prefix;
|
||||
};
|
||||
|
||||
auto updateSuggestions = [&](const std::string& prefix) {
|
||||
activePrefix = prefix;
|
||||
if (!activePrefix.empty() && activePrefix.size() >= 2) {
|
||||
activeSuggestions = buildCompletionList(completionPool, activePrefix);
|
||||
} else {
|
||||
activeSuggestions.clear();
|
||||
}
|
||||
};
|
||||
|
||||
bool poolRebuiltBeforeRender = rebuildCompletionPool();
|
||||
std::string prefixBeforeRender = extractPrefixAtCursor();
|
||||
if (poolRebuiltBeforeRender || prefixBeforeRender != activePrefix) {
|
||||
updateSuggestions(prefixBeforeRender);
|
||||
}
|
||||
|
||||
bool tabPressed = ImGui::IsKeyPressed(ImGuiKey_Tab);
|
||||
@@ -668,18 +728,13 @@ void Engine::renderScriptingWindow() {
|
||||
if (newHash != symbolsHash) {
|
||||
symbolsHash = newHash;
|
||||
symbols = buildSymbolList(scriptEditorState.buffer);
|
||||
completionPoolDirty = true;
|
||||
}
|
||||
|
||||
TextEditor::Coordinates cursorAfter = scriptTextEditor.GetCursorPosition();
|
||||
activePrefix = scriptTextEditor.GetWordAtPublic(cursorAfter);
|
||||
if (activePrefix.empty() && cursorAfter.mColumn > 0) {
|
||||
TextEditor::Coordinates prev(cursorAfter.mLine, cursorAfter.mColumn - 1);
|
||||
activePrefix = scriptTextEditor.GetWordAtPublic(prev);
|
||||
}
|
||||
if (!activePrefix.empty() && activePrefix.size() >= 2) {
|
||||
activeSuggestions = buildCompletionList(completionPool, activePrefix);
|
||||
} else {
|
||||
activeSuggestions.clear();
|
||||
bool poolRebuiltAfterRender = rebuildCompletionPool();
|
||||
std::string prefixAfterRender = extractPrefixAtCursor();
|
||||
if (poolRebuiltAfterRender || prefixAfterRender != activePrefix) {
|
||||
updateSuggestions(prefixAfterRender);
|
||||
}
|
||||
bool canCompleteNow = !activeSuggestions.empty() && !ImGui::GetIO().KeyShift;
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
1696
src/Engine.cpp
1696
src/Engine.cpp
File diff suppressed because it is too large
Load Diff
80
src/Engine.h
80
src/Engine.h
@@ -45,6 +45,8 @@ private:
|
||||
int viewportWidth = 800;
|
||||
int viewportHeight = 600;
|
||||
bool gizmoHistoryCaptured = false;
|
||||
bool worldUiGizmoHistoryCaptured = false;
|
||||
bool gameUiGizmoHistoryCaptured = false;
|
||||
// Standalone material inspection cache
|
||||
std::string inspectedMaterialPath;
|
||||
MaterialProperties inspectedMaterial;
|
||||
@@ -82,6 +84,9 @@ private:
|
||||
uint64_t runtimeScriptBindingsCachedVersion = 0;
|
||||
int selectedObjectId = -1; // primary selection (last)
|
||||
std::vector<int> selectedObjectIds; // multi-select
|
||||
std::vector<int> hierarchyVisibleOrder;
|
||||
int hierarchyRangeAnchorId = -1;
|
||||
std::vector<SceneObject> objectClipboard;
|
||||
int nextObjectId = 0;
|
||||
|
||||
// Gizmo state
|
||||
@@ -165,6 +170,12 @@ private:
|
||||
float fileBrowserSidebarWidth = 220.0f;
|
||||
bool showFileBrowserSidebar = true;
|
||||
std::vector<fs::path> fileBrowserFavorites;
|
||||
struct ExternalFileDropEvent {
|
||||
fs::path path;
|
||||
double mouseX = 0.0;
|
||||
double mouseY = 0.0;
|
||||
};
|
||||
std::vector<ExternalFileDropEvent> pendingExternalFileDrops;
|
||||
std::string uiStylePresetName = "Current";
|
||||
enum class UIAnimationMode {
|
||||
Off = 0,
|
||||
@@ -341,11 +352,21 @@ private:
|
||||
bool meshEditLoaded = false;
|
||||
bool meshEditDirty = false;
|
||||
bool meshEditExtrudeMode = false;
|
||||
bool meshEditAutoUV = true;
|
||||
bool meshEditTriangleSelection = false;
|
||||
std::string meshEditPath;
|
||||
RawMeshAsset meshEditAsset;
|
||||
std::vector<int> meshEditSelectedVertices;
|
||||
std::vector<int> meshEditSelectedEdges; // indices into generated edge list
|
||||
std::vector<int> meshEditSelectedFaces; // indices into mesh faces
|
||||
int meshEditActiveMaterialSlot = 0;
|
||||
float meshEditInsetAmount = 0.2f;
|
||||
float meshEditExtrudeAmount = 0.3f;
|
||||
float meshEditBevelAmount = 0.1f;
|
||||
float meshEditGridSnap = 0.1f;
|
||||
float meshEditUvMoveStep = 0.1f;
|
||||
float meshEditUvScaleStep = 1.1f;
|
||||
float meshEditUvRotateStep = 15.0f;
|
||||
struct UIAnimationState {
|
||||
float hover = 0.0f;
|
||||
float active = 0.0f;
|
||||
@@ -390,8 +411,8 @@ private:
|
||||
std::unordered_map<int, UiCanvas3DContext> uiCanvas3DContexts;
|
||||
std::unordered_map<int, UiCanvas3DInput> uiCanvas3DInputs;
|
||||
bool consoleWrapText = true;
|
||||
enum class MeshEditSelectionMode { Vertex = 0, Edge = 1, Face = 2 };
|
||||
MeshEditSelectionMode meshEditSelectionMode = MeshEditSelectionMode::Vertex;
|
||||
enum class MeshEditSelectionMode { Object = 0, Vertex = 1, Edge = 2, Face = 3, UV = 4 };
|
||||
MeshEditSelectionMode meshEditSelectionMode = MeshEditSelectionMode::Object;
|
||||
ScriptCompiler scriptCompiler;
|
||||
ScriptRuntime scriptRuntime;
|
||||
ManagedScriptRuntime managedRuntime;
|
||||
@@ -551,6 +572,34 @@ private:
|
||||
bool scriptTextEditorReady = false;
|
||||
char scriptingFilter[128] = "";
|
||||
bool scriptingFilesDirty = true;
|
||||
struct RuntimeAnimKey {
|
||||
float time = 0.0f;
|
||||
float value = 0.0f;
|
||||
float inTangent = 0.0f;
|
||||
float outTangent = 0.0f;
|
||||
int interpolation = 1; // 0=constant, 1=linear, 2=cubic
|
||||
};
|
||||
struct RuntimeAnimTrack {
|
||||
std::string propertyId;
|
||||
std::vector<RuntimeAnimKey> keys;
|
||||
};
|
||||
struct RuntimeAnimBinding {
|
||||
std::string path;
|
||||
std::vector<RuntimeAnimTrack> tracks;
|
||||
};
|
||||
struct RuntimeAnimationClip {
|
||||
std::string name;
|
||||
float duration = 2.0f;
|
||||
float sampleRate = 30.0f;
|
||||
std::vector<RuntimeAnimBinding> bindings;
|
||||
};
|
||||
struct RuntimeClipCacheEntry {
|
||||
RuntimeAnimationClip clip;
|
||||
fs::file_time_type lastWriteTime{};
|
||||
bool hasWriteTime = false;
|
||||
bool valid = false;
|
||||
};
|
||||
std::unordered_map<std::string, RuntimeClipCacheEntry> runtimeAnimationClipCache;
|
||||
// Private methods
|
||||
SceneObject* getSelectedObject();
|
||||
glm::vec3 getSelectionCenterWorld(bool worldSpace) const;
|
||||
@@ -625,9 +674,16 @@ private:
|
||||
void updatePlayerController(float delta);
|
||||
void updateRigidbody2D(float delta);
|
||||
void updateCameraFollow2D(float delta);
|
||||
void updateRuntimeAnimations(float delta);
|
||||
void updateAIAgents(float delta);
|
||||
void updateSkeletalAnimations(float delta);
|
||||
void updateSkinningMatrices();
|
||||
fs::path resolveAnimationClipPath(const std::string& storedPath) const;
|
||||
bool loadRuntimeAnimationClipFile(const fs::path& path, RuntimeAnimationClip& outClip) const;
|
||||
const RuntimeAnimationClip* getRuntimeAnimationClip(const std::string& storedPath);
|
||||
float getAnimationDurationForObject(const SceneObject& obj) const;
|
||||
bool applyRuntimeAnimatedProperty(SceneObject& obj, const std::string& propertyId, float value);
|
||||
void evaluateRuntimeAnimationClip(const RuntimeAnimationClip& clip, float time, int rootObjectId);
|
||||
void rebuildSkeletalBindings();
|
||||
void initUIStylePresets();
|
||||
int findUIStylePreset(const std::string& name) const;
|
||||
@@ -687,8 +743,11 @@ private:
|
||||
// Scene object management
|
||||
void addObject(ObjectType type, const std::string& baseName);
|
||||
void duplicateSelected();
|
||||
void copySelected();
|
||||
void pasteClipboard();
|
||||
void selectAllObjects();
|
||||
void deleteSelected();
|
||||
void setParent(int childId, int parentId);
|
||||
void setParent(int childId, int parentId, int beforeSiblingId = -1);
|
||||
void loadMaterialFromFile(SceneObject& obj);
|
||||
void saveMaterialToFile(const SceneObject& obj);
|
||||
void recordState(const char* reason = "");
|
||||
@@ -729,6 +788,9 @@ public:
|
||||
void shutdown();
|
||||
SceneObject* findObjectByName(const std::string& name);
|
||||
SceneObject* findObjectById(int id);
|
||||
bool propagateObjectRenameReferences(const std::string& oldName,
|
||||
const std::string& newName,
|
||||
int renamedObjectId = -1);
|
||||
bool loadPixelSpriteDocument(const fs::path& imagePath);
|
||||
bool savePixelSpriteDocument();
|
||||
fs::path resolveScriptBinary(const fs::path& sourcePath);
|
||||
@@ -767,6 +829,18 @@ public:
|
||||
bool setAudioLoopFromScript(int id, bool loop);
|
||||
bool setAudioVolumeFromScript(int id, float volume);
|
||||
bool setAudioClipFromScript(int id, const std::string& path);
|
||||
bool playAudioOneShotFromScript(int id, const std::string& clipPath, float volumeScale = 1.0f);
|
||||
bool hasAnimationFromScript(int id) const;
|
||||
bool playAnimationFromScript(int id, bool restart = true);
|
||||
bool stopAnimationFromScript(int id, bool resetTime = true);
|
||||
bool pauseAnimationFromScript(int id, bool pause);
|
||||
bool reverseAnimationFromScript(int id, bool restartIfStopped = true);
|
||||
bool setAnimationTimeFromScript(int id, float timeSeconds);
|
||||
float getAnimationTimeFromScript(int id) const;
|
||||
bool isAnimationPlayingFromScript(int id) const;
|
||||
bool setAnimationLoopFromScript(int id, bool loop);
|
||||
bool setAnimationPlaySpeedFromScript(int id, float speed);
|
||||
bool setAnimationPlayOnAwakeFromScript(int id, bool playOnAwake);
|
||||
void syncLocalTransform(SceneObject& obj);
|
||||
const std::vector<SceneObject>& getSceneObjects() const { return sceneObjects; }
|
||||
const std::vector<UIStylePreset>& getUIStylePresets() const { return uiStylePresets; }
|
||||
|
||||
@@ -78,6 +78,59 @@ int modu_ctx_add_rigidbody_impulse(ScriptContext* ctx, float x, float y, float z
|
||||
return ctx->AddRigidbodyImpulse(glm::vec3(x, y, z)) ? 1 : 0;
|
||||
}
|
||||
|
||||
int modu_ctx_has_animation(ScriptContext* ctx) {
|
||||
return (ctx && ctx->HasAnimation()) ? 1 : 0;
|
||||
}
|
||||
|
||||
int modu_ctx_play_animation(ScriptContext* ctx, int restart) {
|
||||
if (!ctx) return 0;
|
||||
return ctx->PlayAnimation(restart != 0) ? 1 : 0;
|
||||
}
|
||||
|
||||
int modu_ctx_stop_animation(ScriptContext* ctx, int resetTime) {
|
||||
if (!ctx) return 0;
|
||||
return ctx->StopAnimation(resetTime != 0) ? 1 : 0;
|
||||
}
|
||||
|
||||
int modu_ctx_pause_animation(ScriptContext* ctx, int pause) {
|
||||
if (!ctx) return 0;
|
||||
return ctx->PauseAnimation(pause != 0) ? 1 : 0;
|
||||
}
|
||||
|
||||
int modu_ctx_reverse_animation(ScriptContext* ctx, int restartIfStopped) {
|
||||
if (!ctx) return 0;
|
||||
return ctx->ReverseAnimation(restartIfStopped != 0) ? 1 : 0;
|
||||
}
|
||||
|
||||
int modu_ctx_set_animation_time(ScriptContext* ctx, float timeSeconds) {
|
||||
if (!ctx) return 0;
|
||||
return ctx->SetAnimationTime(timeSeconds) ? 1 : 0;
|
||||
}
|
||||
|
||||
float modu_ctx_get_animation_time(ScriptContext* ctx) {
|
||||
if (!ctx) return 0.0f;
|
||||
return ctx->GetAnimationTime();
|
||||
}
|
||||
|
||||
int modu_ctx_is_animation_playing(ScriptContext* ctx) {
|
||||
return (ctx && ctx->IsAnimationPlaying()) ? 1 : 0;
|
||||
}
|
||||
|
||||
int modu_ctx_set_animation_loop(ScriptContext* ctx, int loop) {
|
||||
if (!ctx) return 0;
|
||||
return ctx->SetAnimationLoop(loop != 0) ? 1 : 0;
|
||||
}
|
||||
|
||||
int modu_ctx_set_animation_play_speed(ScriptContext* ctx, float speed) {
|
||||
if (!ctx) return 0;
|
||||
return ctx->SetAnimationPlaySpeed(speed) ? 1 : 0;
|
||||
}
|
||||
|
||||
int modu_ctx_set_animation_play_on_awake(ScriptContext* ctx, int playOnAwake) {
|
||||
if (!ctx) return 0;
|
||||
return ctx->SetAnimationPlayOnAwake(playOnAwake != 0) ? 1 : 0;
|
||||
}
|
||||
|
||||
float modu_ctx_get_setting_float(ScriptContext* ctx, const char* key, float fallback) {
|
||||
if (!ctx || !key) return fallback;
|
||||
return ctx->GetSettingFloat(key, fallback);
|
||||
@@ -221,7 +274,7 @@ int modu_imgui_accept_scene_object_drop(int* outId) {
|
||||
|
||||
ManagedNativeApi BuildManagedNativeApi() {
|
||||
ManagedNativeApi api;
|
||||
api.version = 4;
|
||||
api.version = 5;
|
||||
api.getObjectId = modu_ctx_get_object_id;
|
||||
api.getPosition = modu_ctx_get_position;
|
||||
api.setPosition = modu_ctx_set_position;
|
||||
@@ -258,5 +311,16 @@ ManagedNativeApi BuildManagedNativeApi() {
|
||||
api.imguiBeginCombo = modu_imgui_begin_combo;
|
||||
api.imguiEndCombo = modu_imgui_end_combo;
|
||||
api.imguiSelectable = modu_imgui_selectable;
|
||||
api.hasAnimation = modu_ctx_has_animation;
|
||||
api.playAnimation = modu_ctx_play_animation;
|
||||
api.stopAnimation = modu_ctx_stop_animation;
|
||||
api.pauseAnimation = modu_ctx_pause_animation;
|
||||
api.reverseAnimation = modu_ctx_reverse_animation;
|
||||
api.setAnimationTime = modu_ctx_set_animation_time;
|
||||
api.getAnimationTime = modu_ctx_get_animation_time;
|
||||
api.isAnimationPlaying = modu_ctx_is_animation_playing;
|
||||
api.setAnimationLoop = modu_ctx_set_animation_loop;
|
||||
api.setAnimationPlaySpeed = modu_ctx_set_animation_play_speed;
|
||||
api.setAnimationPlayOnAwake = modu_ctx_set_animation_play_on_awake;
|
||||
return api;
|
||||
}
|
||||
|
||||
@@ -17,6 +17,17 @@ int modu_ctx_set_rigidbody_velocity(ScriptContext* ctx, float x, float y, float
|
||||
int modu_ctx_get_rigidbody_velocity(ScriptContext* ctx, float* x, float* y, float* z);
|
||||
int modu_ctx_add_rigidbody_force(ScriptContext* ctx, float x, float y, float z);
|
||||
int modu_ctx_add_rigidbody_impulse(ScriptContext* ctx, float x, float y, float z);
|
||||
int modu_ctx_has_animation(ScriptContext* ctx);
|
||||
int modu_ctx_play_animation(ScriptContext* ctx, int restart);
|
||||
int modu_ctx_stop_animation(ScriptContext* ctx, int resetTime);
|
||||
int modu_ctx_pause_animation(ScriptContext* ctx, int pause);
|
||||
int modu_ctx_reverse_animation(ScriptContext* ctx, int restartIfStopped);
|
||||
int modu_ctx_set_animation_time(ScriptContext* ctx, float timeSeconds);
|
||||
float modu_ctx_get_animation_time(ScriptContext* ctx);
|
||||
int modu_ctx_is_animation_playing(ScriptContext* ctx);
|
||||
int modu_ctx_set_animation_loop(ScriptContext* ctx, int loop);
|
||||
int modu_ctx_set_animation_play_speed(ScriptContext* ctx, float speed);
|
||||
int modu_ctx_set_animation_play_on_awake(ScriptContext* ctx, int playOnAwake);
|
||||
float modu_ctx_get_setting_float(ScriptContext* ctx, const char* key, float fallback);
|
||||
int modu_ctx_get_setting_bool(ScriptContext* ctx, const char* key, int fallback);
|
||||
void modu_ctx_get_setting_string(ScriptContext* ctx, const char* key, const char* fallback,
|
||||
@@ -83,6 +94,18 @@ struct ManagedNativeApi {
|
||||
int (*imguiBeginCombo)(const char* label, const char* previewValue) = nullptr;
|
||||
void (*imguiEndCombo)() = nullptr;
|
||||
int (*imguiSelectable)(const char* label, int selected) = nullptr;
|
||||
// Version 5+ additions.
|
||||
int (*hasAnimation)(ScriptContext* ctx) = nullptr;
|
||||
int (*playAnimation)(ScriptContext* ctx, int restart) = nullptr;
|
||||
int (*stopAnimation)(ScriptContext* ctx, int resetTime) = nullptr;
|
||||
int (*pauseAnimation)(ScriptContext* ctx, int pause) = nullptr;
|
||||
int (*reverseAnimation)(ScriptContext* ctx, int restartIfStopped) = nullptr;
|
||||
int (*setAnimationTime)(ScriptContext* ctx, float timeSeconds) = nullptr;
|
||||
float (*getAnimationTime)(ScriptContext* ctx) = nullptr;
|
||||
int (*isAnimationPlaying)(ScriptContext* ctx) = nullptr;
|
||||
int (*setAnimationLoop)(ScriptContext* ctx, int loop) = nullptr;
|
||||
int (*setAnimationPlaySpeed)(ScriptContext* ctx, float speed) = nullptr;
|
||||
int (*setAnimationPlayOnAwake)(ScriptContext* ctx, int playOnAwake) = nullptr;
|
||||
};
|
||||
|
||||
ManagedNativeApi BuildManagedNativeApi();
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -36,7 +36,11 @@ struct RawMeshAsset {
|
||||
std::vector<glm::vec3> positions;
|
||||
std::vector<glm::vec3> normals;
|
||||
std::vector<glm::vec2> uvs;
|
||||
std::vector<glm::u32vec2> edges;
|
||||
std::vector<glm::u32vec3> faces;
|
||||
std::vector<uint32_t> faceMaterialIndices;
|
||||
std::vector<uint32_t> faceIslandIds;
|
||||
std::vector<std::string> materialSlots;
|
||||
glm::vec3 boundsMin = glm::vec3(FLT_MAX);
|
||||
glm::vec3 boundsMax = glm::vec3(-FLT_MAX);
|
||||
bool hasNormals = false;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <unordered_set>
|
||||
#include <array>
|
||||
#include <cstdio>
|
||||
#include <chrono>
|
||||
#include <sstream>
|
||||
|
||||
#pragma region Local Path Helpers
|
||||
@@ -16,6 +17,40 @@ fs::path normalizePath(const fs::path& p) {
|
||||
return canonical.lexically_normal();
|
||||
}
|
||||
|
||||
std::string trimCopy(const std::string& value) {
|
||||
size_t start = 0;
|
||||
while (start < value.size() && std::isspace(static_cast<unsigned char>(value[start]))) start++;
|
||||
size_t end = value.size();
|
||||
while (end > start && std::isspace(static_cast<unsigned char>(value[end - 1]))) end--;
|
||||
return value.substr(start, end - start);
|
||||
}
|
||||
|
||||
std::vector<std::string> splitTokens(const std::string& input, char delim) {
|
||||
std::vector<std::string> out;
|
||||
std::stringstream ss(input);
|
||||
std::string part;
|
||||
while (std::getline(ss, part, delim)) {
|
||||
out.push_back(part);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
fs::path resolveManifestPath(const fs::path& projectRoot, const std::string& token) {
|
||||
if (token.empty()) return {};
|
||||
const fs::path parsed(token);
|
||||
if (parsed.is_absolute()) {
|
||||
return normalizePath(parsed);
|
||||
}
|
||||
return normalizePath(projectRoot / parsed);
|
||||
}
|
||||
|
||||
std::string toManifestPathToken(const fs::path& pathValue, const fs::path& projectRoot) {
|
||||
if (pathValue.empty()) return "";
|
||||
std::error_code ec;
|
||||
fs::path rel = fs::relative(pathValue, projectRoot, ec);
|
||||
return (!ec ? rel : pathValue).generic_string();
|
||||
}
|
||||
|
||||
bool containsPath(const std::vector<fs::path>& haystack, const fs::path& needle) {
|
||||
std::string target = normalizePath(needle).string();
|
||||
for (const auto& entry : haystack) {
|
||||
@@ -59,6 +94,117 @@ fs::path guessIncludeDir(const fs::path& repoRoot, const std::string& includeRel
|
||||
|
||||
return normalizePath(repoRoot);
|
||||
}
|
||||
|
||||
bool copyDirectoryRecursive(const fs::path& sourceRoot,
|
||||
const fs::path& destinationRoot,
|
||||
std::string& outError) {
|
||||
if (!fs::exists(sourceRoot)) {
|
||||
outError = "Source folder does not exist: " + sourceRoot.string();
|
||||
return false;
|
||||
}
|
||||
|
||||
std::error_code ec;
|
||||
fs::create_directories(destinationRoot, ec);
|
||||
if (ec) {
|
||||
outError = "Failed to create destination folder: " + destinationRoot.string();
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const auto& entry : fs::recursive_directory_iterator(sourceRoot, ec)) {
|
||||
if (ec) {
|
||||
outError = "Failed to read source folder: " + sourceRoot.string();
|
||||
return false;
|
||||
}
|
||||
|
||||
const fs::path rel = fs::relative(entry.path(), sourceRoot, ec);
|
||||
if (ec) {
|
||||
outError = "Failed to compute relative path for " + entry.path().string();
|
||||
return false;
|
||||
}
|
||||
|
||||
const fs::path dst = destinationRoot / rel;
|
||||
if (entry.is_directory()) {
|
||||
fs::create_directories(dst, ec);
|
||||
if (ec) {
|
||||
outError = "Failed to create directory: " + dst.string();
|
||||
return false;
|
||||
}
|
||||
} else if (entry.is_regular_file()) {
|
||||
fs::create_directories(dst.parent_path(), ec);
|
||||
if (ec) {
|
||||
outError = "Failed to create directory: " + dst.parent_path().string();
|
||||
return false;
|
||||
}
|
||||
fs::copy_file(entry.path(), dst, fs::copy_options::overwrite_existing, ec);
|
||||
if (ec) {
|
||||
outError = "Failed to copy file: " + entry.path().string();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool parseExternalManifestPackage(const fs::path& projectRoot,
|
||||
const std::string& payload,
|
||||
bool modupak,
|
||||
PackageInfo& outPackage) {
|
||||
const auto parts = splitTokens(payload, '|');
|
||||
if (parts.size() < 4) return false;
|
||||
|
||||
outPackage = PackageInfo{};
|
||||
outPackage.id = trimCopy(parts[0]);
|
||||
outPackage.name = trimCopy(parts[1]);
|
||||
outPackage.external = true;
|
||||
outPackage.modupak = modupak;
|
||||
outPackage.localPath = resolveManifestPath(projectRoot, trimCopy(parts[3]));
|
||||
outPackage.description = modupak ? "External package from .modupak" : "External package from git";
|
||||
|
||||
const std::string sourceToken = trimCopy(parts[2]);
|
||||
if (modupak) {
|
||||
outPackage.modupakSourcePath = resolveManifestPath(projectRoot, sourceToken);
|
||||
} else {
|
||||
outPackage.gitUrl = sourceToken;
|
||||
}
|
||||
|
||||
if (parts.size() > 4) {
|
||||
for (const auto& inc : splitTokens(parts[4], ';')) {
|
||||
const std::string cleaned = trimCopy(inc);
|
||||
if (cleaned.empty()) continue;
|
||||
outPackage.includeDirs.push_back(resolveManifestPath(projectRoot, cleaned));
|
||||
}
|
||||
}
|
||||
auto readCleanList = [](const std::string& raw) {
|
||||
std::vector<std::string> vals;
|
||||
for (const std::string& token : splitTokens(raw, ';')) {
|
||||
const std::string cleaned = trimCopy(token);
|
||||
if (!cleaned.empty()) vals.push_back(cleaned);
|
||||
}
|
||||
return vals;
|
||||
};
|
||||
|
||||
if (parts.size() > 5) {
|
||||
outPackage.defines = readCleanList(parts[5]);
|
||||
}
|
||||
if (parts.size() > 6) {
|
||||
outPackage.linuxLibs = readCleanList(parts[6]);
|
||||
}
|
||||
if (parts.size() > 7) {
|
||||
outPackage.windowsLibs = readCleanList(parts[7]);
|
||||
}
|
||||
if (parts.size() > 8) {
|
||||
const std::string desc = trimCopy(parts[8]);
|
||||
if (!desc.empty()) outPackage.description = desc;
|
||||
}
|
||||
|
||||
if (outPackage.id.empty()) return false;
|
||||
if (outPackage.name.empty()) outPackage.name = outPackage.id;
|
||||
if (outPackage.includeDirs.empty()) {
|
||||
outPackage.includeDirs.push_back(guessIncludeDir(outPackage.localPath, "include"));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
} // namespace
|
||||
#pragma endregion
|
||||
|
||||
@@ -108,7 +254,8 @@ bool PackageManager::remove(const std::string& id) {
|
||||
}
|
||||
if (pkg->external) {
|
||||
std::string log;
|
||||
if (isGitRepo(projectRoot)) {
|
||||
const bool isGitExternal = !pkg->gitUrl.empty();
|
||||
if (isGitExternal && isGitRepo(projectRoot)) {
|
||||
std::error_code ec;
|
||||
fs::path relPath = fs::relative(pkg->localPath, projectRoot, ec);
|
||||
std::string rel = (!ec ? relPath : pkg->localPath).generic_string();
|
||||
@@ -267,44 +414,40 @@ void PackageManager::loadManifest() {
|
||||
std::string cleaned = trim(line);
|
||||
if (cleaned.empty() || cleaned[0] == '#') continue;
|
||||
|
||||
std::string id;
|
||||
if (cleaned.rfind("package=", 0) == 0) {
|
||||
id = cleaned.substr(8);
|
||||
const std::string id = trim(cleaned.substr(8));
|
||||
if (!id.empty() && !isInstalled(id) && findPackage(id)) {
|
||||
installedIds.push_back(id);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
bool isGitLine = false;
|
||||
bool isModuPakLine = false;
|
||||
std::string payload;
|
||||
if (cleaned.rfind("git=", 0) == 0) {
|
||||
auto payload = cleaned.substr(4);
|
||||
auto parts = split(payload, '|');
|
||||
if (parts.size() < 4) continue;
|
||||
PackageInfo pkg;
|
||||
pkg.id = parts[0];
|
||||
pkg.name = parts[1];
|
||||
pkg.description = "External package from git";
|
||||
pkg.external = true;
|
||||
pkg.gitUrl = parts[2];
|
||||
fs::path relPath = parts[3];
|
||||
pkg.localPath = normalizePath(projectRoot / relPath);
|
||||
payload = cleaned.substr(4);
|
||||
isGitLine = true;
|
||||
} else if (cleaned.rfind("modupak=", 0) == 0) {
|
||||
payload = cleaned.substr(8);
|
||||
isModuPakLine = true;
|
||||
}
|
||||
|
||||
std::vector<std::string> includeTokens;
|
||||
if (parts.size() > 4) includeTokens = split(parts[4], ';');
|
||||
for (const auto& inc : includeTokens) {
|
||||
if (inc.empty()) continue;
|
||||
pkg.includeDirs.push_back(normalizePath(projectRoot / inc));
|
||||
}
|
||||
std::vector<std::string> defTokens;
|
||||
if (parts.size() > 5) defTokens = split(parts[5], ';');
|
||||
pkg.defines = defTokens;
|
||||
if (parts.size() > 6) pkg.linuxLibs = split(parts[6], ';');
|
||||
if (parts.size() > 7) pkg.windowsLibs = split(parts[7], ';');
|
||||
if (!isGitLine && !isModuPakLine) {
|
||||
continue;
|
||||
}
|
||||
|
||||
registry.push_back(pkg);
|
||||
if (!isInstalled(pkg.id)) {
|
||||
installedIds.push_back(pkg.id);
|
||||
}
|
||||
PackageInfo pkg;
|
||||
if (!parseExternalManifestPackage(projectRoot, payload, isModuPakLine, pkg)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
registry.erase(std::remove_if(registry.begin(), registry.end(),
|
||||
[&](const PackageInfo& entry) { return entry.id == pkg.id && entry.external; }),
|
||||
registry.end());
|
||||
registry.push_back(pkg);
|
||||
if (!isInstalled(pkg.id)) {
|
||||
installedIds.push_back(pkg.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -316,6 +459,9 @@ void PackageManager::saveManifest() const {
|
||||
|
||||
file << "# Modularity package manifest\n";
|
||||
file << "# Add optional script-time dependencies here\n";
|
||||
file << "# package=<id>\n";
|
||||
file << "# git=<id>|<name>|<url>|<path>|<includeDirs>|<defines>|<linuxLibs>|<windowsLibs>|<description>\n";
|
||||
file << "# modupak=<id>|<name>|<bundlePath>|<path>|<includeDirs>|<defines>|<linuxLibs>|<windowsLibs>|<description>\n";
|
||||
for (const auto& id : installedIds) {
|
||||
const PackageInfo* pkg = findPackage(id);
|
||||
if (!pkg) continue;
|
||||
@@ -334,17 +480,21 @@ void PackageManager::saveManifest() const {
|
||||
relIncludes.push_back((!ec ? rel : inc).generic_string());
|
||||
}
|
||||
|
||||
file << "git=" << pkg->id << "|"
|
||||
<< pkg->name << "|"
|
||||
<< pkg->gitUrl << "|";
|
||||
const char* sourceTag = pkg->modupak ? "modupak=" : "git=";
|
||||
file << sourceTag << pkg->id << "|"
|
||||
<< pkg->name << "|";
|
||||
if (pkg->modupak) {
|
||||
file << toManifestPathToken(pkg->modupakSourcePath, projectRoot) << "|";
|
||||
} else {
|
||||
file << pkg->gitUrl << "|";
|
||||
}
|
||||
|
||||
std::error_code ec;
|
||||
fs::path relPath = fs::relative(pkg->localPath, projectRoot, ec);
|
||||
file << ((!ec ? relPath : pkg->localPath).generic_string()) << "|";
|
||||
file << toManifestPathToken(pkg->localPath, projectRoot) << "|";
|
||||
file << join(relIncludes, ';') << "|";
|
||||
file << join(pkg->defines, ';') << "|";
|
||||
file << join(pkg->linuxLibs, ';') << "|";
|
||||
file << join(pkg->windowsLibs, ';') << "\n";
|
||||
file << join(pkg->windowsLibs, ';') << "|";
|
||||
file << pkg->description << "\n";
|
||||
}
|
||||
}
|
||||
#pragma endregion
|
||||
@@ -508,12 +658,177 @@ bool PackageManager::installGitPackage(const std::string& url,
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PackageManager::installModuPak(const fs::path& modupakPath, std::string& outId) {
|
||||
lastError.clear();
|
||||
outId.clear();
|
||||
|
||||
if (!ensureProjectRoot()) {
|
||||
lastError = "Project root not set.";
|
||||
return false;
|
||||
}
|
||||
if (modupakPath.empty()) {
|
||||
lastError = ".modupak path is required.";
|
||||
return false;
|
||||
}
|
||||
|
||||
const fs::path source = normalizePath(modupakPath);
|
||||
if (!fs::exists(source)) {
|
||||
lastError = ".modupak was not found: " + source.string();
|
||||
return false;
|
||||
}
|
||||
if (source.extension() != ".modupak") {
|
||||
lastError = "Expected a .modupak bundle.";
|
||||
return false;
|
||||
}
|
||||
|
||||
const fs::path tempRoot = projectRoot / "Library" / "Temp" / "ModuPakInstall";
|
||||
std::error_code ec;
|
||||
fs::create_directories(tempRoot, ec);
|
||||
if (ec) {
|
||||
lastError = "Failed to create temp folder: " + ec.message();
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto now = std::chrono::steady_clock::now().time_since_epoch().count();
|
||||
const fs::path unpackRoot = tempRoot / ("extract_" + std::to_string(now));
|
||||
fs::create_directories(unpackRoot, ec);
|
||||
if (ec) {
|
||||
lastError = "Failed to create extraction folder: " + ec.message();
|
||||
return false;
|
||||
}
|
||||
|
||||
auto cleanup = [&]() {
|
||||
std::error_code removeEc;
|
||||
fs::remove_all(unpackRoot, removeEc);
|
||||
};
|
||||
|
||||
if (fs::is_directory(source)) {
|
||||
std::string copyError;
|
||||
if (!copyDirectoryRecursive(source, unpackRoot, copyError)) {
|
||||
cleanup();
|
||||
lastError = copyError;
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
std::string tarLog;
|
||||
const std::string extractCmd =
|
||||
"tar -xf \"" + source.string() + "\" -C \"" + unpackRoot.string() + "\" 2>&1";
|
||||
if (!runCommand(extractCmd, tarLog)) {
|
||||
cleanup();
|
||||
lastError = "Failed to extract .modupak with tar.\n" + tarLog;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const fs::path manifestFile = unpackRoot / "manifest.modu";
|
||||
if (!fs::exists(manifestFile)) {
|
||||
cleanup();
|
||||
lastError = ".modupak is missing manifest.modu.";
|
||||
return false;
|
||||
}
|
||||
|
||||
PackageInfo pkg;
|
||||
pkg.external = true;
|
||||
pkg.modupak = true;
|
||||
pkg.modupakSourcePath = source;
|
||||
pkg.description = "External package from .modupak";
|
||||
|
||||
std::vector<std::string> includeHints;
|
||||
std::ifstream manifest(manifestFile);
|
||||
if (!manifest.is_open()) {
|
||||
cleanup();
|
||||
lastError = "Failed to open .modupak manifest.";
|
||||
return false;
|
||||
}
|
||||
std::string line;
|
||||
while (std::getline(manifest, line)) {
|
||||
const std::string cleaned = trim(line);
|
||||
if (cleaned.empty() || cleaned[0] == '#') continue;
|
||||
|
||||
auto readValue = [&](size_t prefixSize) {
|
||||
return trim(cleaned.substr(prefixSize));
|
||||
};
|
||||
|
||||
if (cleaned.rfind("id=", 0) == 0) {
|
||||
pkg.id = slugify(readValue(3));
|
||||
} else if (cleaned.rfind("name=", 0) == 0) {
|
||||
pkg.name = readValue(5);
|
||||
} else if (cleaned.rfind("description=", 0) == 0) {
|
||||
pkg.description = readValue(12);
|
||||
} else if (cleaned.rfind("includeDir=", 0) == 0) {
|
||||
const std::string value = readValue(11);
|
||||
if (!value.empty()) includeHints.push_back(value);
|
||||
} else if (cleaned.rfind("define=", 0) == 0) {
|
||||
const std::string value = readValue(7);
|
||||
if (!value.empty()) pkg.defines.push_back(value);
|
||||
} else if (cleaned.rfind("linux.linkLib=", 0) == 0) {
|
||||
const std::string value = readValue(14);
|
||||
if (!value.empty()) pkg.linuxLibs.push_back(value);
|
||||
} else if (cleaned.rfind("win.linkLib=", 0) == 0) {
|
||||
const std::string value = readValue(12);
|
||||
if (!value.empty()) pkg.windowsLibs.push_back(value);
|
||||
} else if (cleaned.rfind("windows.linkLib=", 0) == 0) {
|
||||
const std::string value = readValue(16);
|
||||
if (!value.empty()) pkg.windowsLibs.push_back(value);
|
||||
}
|
||||
}
|
||||
|
||||
if (pkg.name.empty()) {
|
||||
pkg.name = source.stem().string();
|
||||
}
|
||||
if (pkg.id.empty()) {
|
||||
pkg.id = slugify(pkg.name);
|
||||
}
|
||||
if (pkg.id.empty()) {
|
||||
cleanup();
|
||||
lastError = "Unable to resolve package id from .modupak.";
|
||||
return false;
|
||||
}
|
||||
if (isInstalled(pkg.id)) {
|
||||
cleanup();
|
||||
lastError = "Package already installed: " + pkg.id;
|
||||
return false;
|
||||
}
|
||||
|
||||
pkg.localPath = normalizePath(packagesFolder() / pkg.id);
|
||||
if (fs::exists(pkg.localPath)) {
|
||||
cleanup();
|
||||
lastError = "Package target path already exists: " + pkg.localPath.string();
|
||||
return false;
|
||||
}
|
||||
|
||||
const fs::path payloadRoot = unpackRoot / "payload";
|
||||
const fs::path sourceContent =
|
||||
(fs::exists(payloadRoot) && fs::is_directory(payloadRoot)) ? payloadRoot : unpackRoot;
|
||||
|
||||
std::string installCopyError;
|
||||
if (!copyDirectoryRecursive(sourceContent, pkg.localPath, installCopyError)) {
|
||||
cleanup();
|
||||
lastError = installCopyError;
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const std::string& includeRel : includeHints) {
|
||||
pkg.includeDirs.push_back(normalizePath(pkg.localPath / includeRel));
|
||||
}
|
||||
if (pkg.includeDirs.empty()) {
|
||||
pkg.includeDirs.push_back(guessIncludeDir(pkg.localPath, "include"));
|
||||
}
|
||||
|
||||
registry.push_back(pkg);
|
||||
installedIds.push_back(pkg.id);
|
||||
saveManifest();
|
||||
outId = pkg.id;
|
||||
cleanup();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PackageManager::checkGitStatus(const std::string& id, std::string& outStatus) {
|
||||
lastError.clear();
|
||||
outStatus.clear();
|
||||
const PackageInfo* pkg = findPackage(id);
|
||||
if (!pkg || !pkg->external) {
|
||||
lastError = "Package is not external or not found.";
|
||||
if (!pkg || !pkg->external || pkg->gitUrl.empty()) {
|
||||
lastError = "Package is not a Git package or was not found.";
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -532,8 +847,8 @@ bool PackageManager::updateGitPackage(const std::string& id, std::string& outLog
|
||||
lastError.clear();
|
||||
outLog.clear();
|
||||
const PackageInfo* pkg = findPackage(id);
|
||||
if (!pkg || !pkg->external) {
|
||||
lastError = "Package is not external or not found.";
|
||||
if (!pkg || !pkg->external || pkg->gitUrl.empty()) {
|
||||
lastError = "Package is not a Git package or was not found.";
|
||||
return false;
|
||||
}
|
||||
std::string cmd = "git -C \"" + pkg->localPath.string() + "\" pull --ff-only";
|
||||
|
||||
@@ -10,7 +10,9 @@ struct PackageInfo {
|
||||
std::string description;
|
||||
bool builtIn = false;
|
||||
bool external = false;
|
||||
bool modupak = false;
|
||||
std::string gitUrl;
|
||||
fs::path modupakSourcePath;
|
||||
fs::path localPath; // Absolute path for external packages
|
||||
fs::path includeHint; // Absolute include root for external packages
|
||||
std::vector<fs::path> includeDirs;
|
||||
@@ -36,6 +38,7 @@ public:
|
||||
const std::string& nameHint,
|
||||
const std::string& includeRel,
|
||||
std::string& outId);
|
||||
bool installModuPak(const fs::path& modupakPath, std::string& outId);
|
||||
bool checkGitStatus(const std::string& id, std::string& outStatus);
|
||||
bool updateGitPackage(const std::string& id, std::string& outLog);
|
||||
bool remove(const std::string& id);
|
||||
|
||||
@@ -538,7 +538,7 @@ void PhysicsSystem::onPlayStart(const std::vector<SceneObject>& objects) {
|
||||
cookInfos.reserve(objects.size());
|
||||
|
||||
for (const auto& obj : objects) {
|
||||
if (!obj.enabled || !obj.hasCollider || !obj.collider.enabled) continue;
|
||||
if (!IsObjectEnabledInHierarchy(obj) || !obj.hasCollider || !obj.collider.enabled) continue;
|
||||
if (obj.collider.type == ColliderType::Box || obj.collider.type == ColliderType::Capsule) continue;
|
||||
const OBJLoader::LoadedMesh* meshInfo = nullptr;
|
||||
if (obj.hasRenderer && obj.renderType == RenderType::OBJMesh && obj.meshId >= 0) {
|
||||
@@ -574,7 +574,7 @@ void PhysicsSystem::onPlayStart(const std::vector<SceneObject>& objects) {
|
||||
}
|
||||
|
||||
for (const auto& obj : objects) {
|
||||
if (!obj.enabled) continue;
|
||||
if (!IsObjectEnabledInHierarchy(obj)) continue;
|
||||
ActorRecord rec = createActorFor(obj);
|
||||
if (!rec.actor) continue;
|
||||
mScene->addActor(*rec.actor);
|
||||
@@ -802,7 +802,7 @@ void PhysicsSystem::simulate(float deltaTime, std::vector<SceneObject>& objects)
|
||||
if (!rec.actor) continue;
|
||||
auto it = std::find_if(objects.begin(), objects.end(), [id](const SceneObject& o) { return o.id == id; });
|
||||
if (it == objects.end()) continue;
|
||||
if (!it->enabled) {
|
||||
if (!IsObjectEnabledInHierarchy(*it)) {
|
||||
rec.actor->setActorFlag(PxActorFlag::eDISABLE_SIMULATION, true);
|
||||
continue;
|
||||
} else {
|
||||
@@ -825,7 +825,7 @@ void PhysicsSystem::simulate(float deltaTime, std::vector<SceneObject>& objects)
|
||||
if (!rec.actor || !rec.isDynamic || rec.isKinematic) continue;
|
||||
PxTransform pose = rec.actor->getGlobalPose();
|
||||
auto it = std::find_if(objects.begin(), objects.end(), [id](const SceneObject& o) { return o.id == id; });
|
||||
if (it == objects.end() || !it->enabled) continue;
|
||||
if (it == objects.end() || !IsObjectEnabledInHierarchy(*it)) continue;
|
||||
|
||||
it->position = ToGlmVec3(pose.p);
|
||||
if (it->hasPlayerController && it->playerController.enabled) {
|
||||
|
||||
@@ -86,6 +86,9 @@ bool Project::create() {
|
||||
|
||||
std::ofstream packageManifest(projectPath / "packages.modu");
|
||||
packageManifest << "# Modularity package manifest\n";
|
||||
packageManifest << "# package=<id>\n";
|
||||
packageManifest << "# git=<id>|<name>|<url>|<path>|<includeDirs>|<defines>|<linuxLibs>|<windowsLibs>|<description>\n";
|
||||
packageManifest << "# modupak=<id>|<name>|<bundlePath>|<path>|<includeDirs>|<defines>|<linuxLibs>|<windowsLibs>|<description>\n";
|
||||
packageManifest.close();
|
||||
|
||||
currentSceneName = "Main";
|
||||
@@ -566,14 +569,25 @@ bool SceneSerializer::saveScene(const fs::path& filePath,
|
||||
}
|
||||
file << "hasAnimation=" << (obj.hasAnimation ? 1 : 0) << "\n";
|
||||
if (obj.hasAnimation) {
|
||||
file << "animEnabled=" << (obj.animation.enabled ? 1 : 0) << "\n";
|
||||
file << "animClipLength=" << obj.animation.clipLength << "\n";
|
||||
file << "animPlaySpeed=" << obj.animation.playSpeed << "\n";
|
||||
file << "animLoop=" << (obj.animation.loop ? 1 : 0) << "\n";
|
||||
file << "animApplyOnScrub=" << (obj.animation.applyOnScrub ? 1 : 0) << "\n";
|
||||
file << "animKeyCount=" << obj.animation.keyframes.size() << "\n";
|
||||
for (size_t ki = 0; ki < obj.animation.keyframes.size(); ++ki) {
|
||||
const auto& key = obj.animation.keyframes[ki];
|
||||
AnimationComponent animation = obj.animation;
|
||||
NormalizeAnimationClipSlots(animation);
|
||||
file << "animEnabled=" << (animation.enabled ? 1 : 0) << "\n";
|
||||
file << "animClipAsset=" << animation.clipAssetPath << "\n";
|
||||
file << "animClipCount=" << animation.clips.size() << "\n";
|
||||
file << "animActiveClipIndex=" << animation.activeClipIndex << "\n";
|
||||
for (size_t ci = 0; ci < animation.clips.size(); ++ci) {
|
||||
const auto& clip = animation.clips[ci];
|
||||
file << "animClip" << ci << "_name=" << clip.name << "\n";
|
||||
file << "animClip" << ci << "_asset=" << clip.assetPath << "\n";
|
||||
}
|
||||
file << "animClipLength=" << animation.clipLength << "\n";
|
||||
file << "animPlaySpeed=" << animation.playSpeed << "\n";
|
||||
file << "animLoop=" << (animation.loop ? 1 : 0) << "\n";
|
||||
file << "animPlayOnAwake=" << (animation.playOnAwake ? 1 : 0) << "\n";
|
||||
file << "animApplyOnScrub=" << (animation.applyOnScrub ? 1 : 0) << "\n";
|
||||
file << "animKeyCount=" << animation.keyframes.size() << "\n";
|
||||
for (size_t ki = 0; ki < animation.keyframes.size(); ++ki) {
|
||||
const auto& key = animation.keyframes[ki];
|
||||
file << "animKey" << ki << "_time=" << key.time << "\n";
|
||||
file << "animKey" << ki << "_pos=" << key.position.x << "," << key.position.y << "," << key.position.z << "\n";
|
||||
file << "animKey" << ki << "_rot=" << key.rotation.x << "," << key.rotation.y << "," << key.rotation.z << "\n";
|
||||
@@ -583,16 +597,16 @@ bool SceneSerializer::saveScene(const fs::path& filePath,
|
||||
file << "animKey" << ki << "_in=" << key.bezierIn.x << "," << key.bezierIn.y << "\n";
|
||||
file << "animKey" << ki << "_out=" << key.bezierOut.x << "," << key.bezierOut.y << "\n";
|
||||
}
|
||||
file << "animEventCount=" << obj.animation.events.size() << "\n";
|
||||
for (size_t ei = 0; ei < obj.animation.events.size(); ++ei) {
|
||||
const auto& evt = obj.animation.events[ei];
|
||||
file << "animEventCount=" << animation.events.size() << "\n";
|
||||
for (size_t ei = 0; ei < animation.events.size(); ++ei) {
|
||||
const auto& evt = animation.events[ei];
|
||||
file << "animEvent" << ei << "_time=" << evt.time << "\n";
|
||||
file << "animEvent" << ei << "_id=" << evt.eventId << "\n";
|
||||
file << "animEvent" << ei << "_payload=" << evt.payload << "\n";
|
||||
}
|
||||
file << "animTrackCount=" << obj.animation.tracks.size() << "\n";
|
||||
for (size_t ti = 0; ti < obj.animation.tracks.size(); ++ti) {
|
||||
const auto& track = obj.animation.tracks[ti];
|
||||
file << "animTrackCount=" << animation.tracks.size() << "\n";
|
||||
for (size_t ti = 0; ti < animation.tracks.size(); ++ti) {
|
||||
const auto& track = animation.tracks[ti];
|
||||
file << "animTrack" << ti << "_enabled=" << (track.enabled ? 1 : 0) << "\n";
|
||||
file << "animTrack" << ti << "_path=" << track.path << "\n";
|
||||
file << "animTrack" << ti << "_label=" << track.label << "\n";
|
||||
@@ -676,6 +690,7 @@ bool SceneSerializer::saveScene(const fs::path& filePath,
|
||||
file << "uiPosition=" << obj.ui.position.x << "," << obj.ui.position.y << "\n";
|
||||
file << "uiRotation=" << obj.ui.rotation << "\n";
|
||||
file << "uiSize=" << obj.ui.size.x << "," << obj.ui.size.y << "\n";
|
||||
file << "uiMaskChildren=" << (obj.ui.maskChildren ? 1 : 0) << "\n";
|
||||
file << "uiSliderValue=" << obj.ui.sliderValue << "\n";
|
||||
file << "uiSliderMin=" << obj.ui.sliderMin << "\n";
|
||||
file << "uiSliderMax=" << obj.ui.sliderMax << "\n";
|
||||
@@ -686,6 +701,12 @@ bool SceneSerializer::saveScene(const fs::path& filePath,
|
||||
file << "uiButtonStyle=" << static_cast<int>(obj.ui.buttonStyle) << "\n";
|
||||
file << "uiStylePreset=" << obj.ui.stylePreset << "\n";
|
||||
file << "uiTextScale=" << obj.ui.textScale << "\n";
|
||||
file << "uiTextWrap=" << (obj.ui.textAutoWrap ? 1 : 0) << "\n";
|
||||
file << "uiTextHAlign=" << static_cast<int>(obj.ui.textHAlign) << "\n";
|
||||
file << "uiTextVAlign=" << static_cast<int>(obj.ui.textVAlign) << "\n";
|
||||
file << "uiTextEffectFlags=" << obj.ui.textEffectFlags << "\n";
|
||||
file << "uiTextEffectSpeed=" << obj.ui.textEffectSpeed << "\n";
|
||||
file << "uiTextEffectIntensity=" << obj.ui.textEffectIntensity << "\n";
|
||||
file << "uiRenderIn3D=" << (obj.ui.renderIn3D ? 1 : 0) << "\n";
|
||||
file << "uiRenderTargetSize=" << obj.ui.renderTargetSize.x << "," << obj.ui.renderTargetSize.y << "\n";
|
||||
file << "uiSpriteSheetEnabled=" << (obj.ui.spriteSheetEnabled ? 1 : 0) << "\n";
|
||||
@@ -695,6 +716,14 @@ bool SceneSerializer::saveScene(const fs::path& filePath,
|
||||
file << "uiSpriteSheetLoop=" << (obj.ui.spriteSheetLoop ? 1 : 0) << "\n";
|
||||
file << "uiSpriteCustomFramesEnabled=" << (obj.ui.spriteCustomFramesEnabled ? 1 : 0) << "\n";
|
||||
file << "uiSpriteSourceSize=" << obj.ui.spriteSourceWidth << "," << obj.ui.spriteSourceHeight << "\n";
|
||||
file << "uiNineSliceEnabled=" << (obj.ui.nineSliceEnabled ? 1 : 0) << "\n";
|
||||
file << "uiNineSliceBorder="
|
||||
<< obj.ui.nineSliceBorder.x << ","
|
||||
<< obj.ui.nineSliceBorder.y << ","
|
||||
<< obj.ui.nineSliceBorder.z << ","
|
||||
<< obj.ui.nineSliceBorder.w << "\n";
|
||||
file << "uiNineSliceTileEdges=" << (obj.ui.nineSliceTileEdges ? 1 : 0) << "\n";
|
||||
file << "uiNineSliceTileCenter=" << (obj.ui.nineSliceTileCenter ? 1 : 0) << "\n";
|
||||
if (!obj.ui.spriteCustomFrames.empty()) {
|
||||
file << "uiSpriteCustomFrames=";
|
||||
for (size_t i = 0; i < obj.ui.spriteCustomFrames.size(); ++i) {
|
||||
@@ -1121,9 +1150,18 @@ const std::unordered_map<std::string, KeyHandler>& GetSceneObjectKeyHandlers() {
|
||||
{"aiAgentDebugDrawPath", +[](SceneObject& obj, const std::string& value) { obj.aiAgent.debugDrawPath = std::stoi(value) != 0; }},
|
||||
{"hasAnimation", +[](SceneObject& obj, const std::string& value) { obj.hasAnimation = std::stoi(value) != 0; }},
|
||||
{"animEnabled", +[](SceneObject& obj, const std::string& value) { obj.animation.enabled = std::stoi(value) != 0; }},
|
||||
{"animClipAsset", +[](SceneObject& obj, const std::string& value) { obj.animation.clipAssetPath = value; }},
|
||||
{"animClipCount", +[](SceneObject& obj, const std::string& value) {
|
||||
int count = std::stoi(value);
|
||||
obj.animation.clips.resize(std::max(0, count));
|
||||
}},
|
||||
{"animActiveClipIndex", +[](SceneObject& obj, const std::string& value) {
|
||||
obj.animation.activeClipIndex = std::stoi(value);
|
||||
}},
|
||||
{"animClipLength", +[](SceneObject& obj, const std::string& value) { obj.animation.clipLength = std::stof(value); }},
|
||||
{"animPlaySpeed", +[](SceneObject& obj, const std::string& value) { obj.animation.playSpeed = std::stof(value); }},
|
||||
{"animLoop", +[](SceneObject& obj, const std::string& value) { obj.animation.loop = std::stoi(value) != 0; }},
|
||||
{"animPlayOnAwake", +[](SceneObject& obj, const std::string& value) { obj.animation.playOnAwake = std::stoi(value) != 0; }},
|
||||
{"animApplyOnScrub", +[](SceneObject& obj, const std::string& value) { obj.animation.applyOnScrub = std::stoi(value) != 0; }},
|
||||
{"animKeyCount", +[](SceneObject& obj, const std::string& value) {
|
||||
int count = std::stoi(value);
|
||||
@@ -1203,6 +1241,7 @@ const std::unordered_map<std::string, KeyHandler>& GetSceneObjectKeyHandlers() {
|
||||
{"uiPosition", +[](SceneObject& obj, const std::string& value) { ParseVec2(value, obj.ui.position); }},
|
||||
{"uiRotation", +[](SceneObject& obj, const std::string& value) { obj.ui.rotation = std::stof(value); }},
|
||||
{"uiSize", +[](SceneObject& obj, const std::string& value) { ParseVec2(value, obj.ui.size); }},
|
||||
{"uiMaskChildren", +[](SceneObject& obj, const std::string& value) { obj.ui.maskChildren = (std::stoi(value) != 0); }},
|
||||
{"uiSliderValue", +[](SceneObject& obj, const std::string& value) { obj.ui.sliderValue = std::stof(value); }},
|
||||
{"uiSliderMin", +[](SceneObject& obj, const std::string& value) { obj.ui.sliderMin = std::stof(value); }},
|
||||
{"uiSliderMax", +[](SceneObject& obj, const std::string& value) { obj.ui.sliderMax = std::stof(value); }},
|
||||
@@ -1213,6 +1252,16 @@ const std::unordered_map<std::string, KeyHandler>& GetSceneObjectKeyHandlers() {
|
||||
{"uiButtonStyle", +[](SceneObject& obj, const std::string& value) { obj.ui.buttonStyle = static_cast<UIButtonStyle>(std::stoi(value)); }},
|
||||
{"uiStylePreset", +[](SceneObject& obj, const std::string& value) { obj.ui.stylePreset = value; }},
|
||||
{"uiTextScale", +[](SceneObject& obj, const std::string& value) { obj.ui.textScale = std::stof(value); }},
|
||||
{"uiTextWrap", +[](SceneObject& obj, const std::string& value) { obj.ui.textAutoWrap = (std::stoi(value) != 0); }},
|
||||
{"uiTextHAlign", +[](SceneObject& obj, const std::string& value) {
|
||||
obj.ui.textHAlign = static_cast<UITextHAlign>(std::clamp(std::stoi(value), 0, 2));
|
||||
}},
|
||||
{"uiTextVAlign", +[](SceneObject& obj, const std::string& value) {
|
||||
obj.ui.textVAlign = static_cast<UITextVAlign>(std::clamp(std::stoi(value), 0, 2));
|
||||
}},
|
||||
{"uiTextEffectFlags", +[](SceneObject& obj, const std::string& value) { obj.ui.textEffectFlags = std::stoi(value); }},
|
||||
{"uiTextEffectSpeed", +[](SceneObject& obj, const std::string& value) { obj.ui.textEffectSpeed = std::max(0.01f, std::stof(value)); }},
|
||||
{"uiTextEffectIntensity", +[](SceneObject& obj, const std::string& value) { obj.ui.textEffectIntensity = std::max(0.0f, std::stof(value)); }},
|
||||
{"uiRenderIn3D", +[](SceneObject& obj, const std::string& value) { obj.ui.renderIn3D = (std::stoi(value) != 0); }},
|
||||
{"uiRenderTargetSize", +[](SceneObject& obj, const std::string& value) { ParseIVec2(value, obj.ui.renderTargetSize); }},
|
||||
{"uiSpriteSheetEnabled", +[](SceneObject& obj, const std::string& value) { obj.ui.spriteSheetEnabled = (std::stoi(value) != 0); }},
|
||||
@@ -1232,6 +1281,22 @@ const std::unordered_map<std::string, KeyHandler>& GetSceneObjectKeyHandlers() {
|
||||
obj.ui.spriteSourceWidth = std::max(0, size.x);
|
||||
obj.ui.spriteSourceHeight = std::max(0, size.y);
|
||||
}},
|
||||
{"uiNineSliceEnabled", +[](SceneObject& obj, const std::string& value) {
|
||||
obj.ui.nineSliceEnabled = (std::stoi(value) != 0);
|
||||
}},
|
||||
{"uiNineSliceBorder", +[](SceneObject& obj, const std::string& value) {
|
||||
ParseVec4(value, obj.ui.nineSliceBorder);
|
||||
obj.ui.nineSliceBorder.x = std::max(0.0f, obj.ui.nineSliceBorder.x);
|
||||
obj.ui.nineSliceBorder.y = std::max(0.0f, obj.ui.nineSliceBorder.y);
|
||||
obj.ui.nineSliceBorder.z = std::max(0.0f, obj.ui.nineSliceBorder.z);
|
||||
obj.ui.nineSliceBorder.w = std::max(0.0f, obj.ui.nineSliceBorder.w);
|
||||
}},
|
||||
{"uiNineSliceTileEdges", +[](SceneObject& obj, const std::string& value) {
|
||||
obj.ui.nineSliceTileEdges = (std::stoi(value) != 0);
|
||||
}},
|
||||
{"uiNineSliceTileCenter", +[](SceneObject& obj, const std::string& value) {
|
||||
obj.ui.nineSliceTileCenter = (std::stoi(value) != 0);
|
||||
}},
|
||||
{"uiSpriteCustomFrames", +[](SceneObject& obj, const std::string& value) {
|
||||
obj.ui.spriteCustomFrames.clear();
|
||||
std::stringstream ss(value);
|
||||
@@ -1464,6 +1529,20 @@ bool SceneSerializer::loadScene(const fs::path& filePath,
|
||||
auto handlerIt = handlers.find(key);
|
||||
if (handlerIt != handlers.end()) {
|
||||
handlerIt->second(*currentObj, value);
|
||||
} else if (key.rfind("animClip", 0) == 0) {
|
||||
size_t underscore = key.find('_');
|
||||
if (underscore != std::string::npos && underscore > 8) {
|
||||
int clipIdx = std::stoi(key.substr(8, underscore - 8));
|
||||
if (clipIdx >= 0 && clipIdx < static_cast<int>(currentObj->animation.clips.size())) {
|
||||
std::string sub = key.substr(underscore + 1);
|
||||
auto& clip = currentObj->animation.clips[clipIdx];
|
||||
if (sub == "name") {
|
||||
clip.name = value;
|
||||
} else if (sub == "asset") {
|
||||
clip.assetPath = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (key.rfind("animKey", 0) == 0) {
|
||||
size_t underscore = key.find('_');
|
||||
if (underscore != std::string::npos && underscore > 7) {
|
||||
@@ -1619,6 +1698,9 @@ bool SceneSerializer::loadScene(const fs::path& filePath,
|
||||
|
||||
file.close();
|
||||
for (auto& obj : objects) {
|
||||
if (obj.hasAnimation) {
|
||||
NormalizeAnimationClipSlots(obj.animation);
|
||||
}
|
||||
obj.type = GetLegacyTypeFromComponents(obj);
|
||||
}
|
||||
outVersion = sceneVersion;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "Rendering.h"
|
||||
#include "Camera.h"
|
||||
#include "ModelLoader.h"
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstring>
|
||||
#include <future>
|
||||
@@ -16,6 +17,21 @@ extern float vertices[288];
|
||||
extern float mirrorPlaneVertices[48];
|
||||
|
||||
namespace {
|
||||
struct Runtime2DRenderCounters {
|
||||
uint64_t textureBindCount = 0;
|
||||
uint64_t stateBindCount = 0;
|
||||
};
|
||||
|
||||
Runtime2DRenderCounters gRuntime2DRenderCounters;
|
||||
|
||||
inline void Runtime2DCountTextureBind() {
|
||||
++gRuntime2DRenderCounters.textureBindCount;
|
||||
}
|
||||
|
||||
inline void Runtime2DCountStateBind() {
|
||||
++gRuntime2DRenderCounters.stateBindCount;
|
||||
}
|
||||
|
||||
glm::vec4 BuildSpriteUvRect(const SceneObject& obj) {
|
||||
if (obj.ui.spriteCustomFramesEnabled &&
|
||||
!obj.ui.spriteCustomFrames.empty() &&
|
||||
@@ -211,6 +227,15 @@ bool IsFiniteVec3(const glm::vec3& value) {
|
||||
return std::isfinite(value.x) && std::isfinite(value.y) && std::isfinite(value.z);
|
||||
}
|
||||
|
||||
uint64_t HashStringFNV1a(const std::string& value) {
|
||||
uint64_t hash = 1469598103934665603ull;
|
||||
for (unsigned char c : value) {
|
||||
hash ^= static_cast<uint64_t>(c);
|
||||
hash *= 1099511628211ull;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
bool TryGetObjectLocalBounds(const SceneObject& obj, glm::vec3& outCenter, glm::vec3& outExtents) {
|
||||
outCenter = glm::vec3(0.0f);
|
||||
switch (obj.renderType) {
|
||||
@@ -364,7 +389,7 @@ const std::vector<float>& GetPrimitiveTriangleVertices(RenderType type) {
|
||||
}
|
||||
|
||||
bool IsStaticMergeCandidate(const SceneObject& obj) {
|
||||
if (!obj.enabled || !HasRendererComponent(obj)) return false;
|
||||
if (!IsObjectEnabledInHierarchy(obj) || !HasRendererComponent(obj)) return false;
|
||||
if (obj.hasUI || obj.hasLight || obj.hasCamera || obj.hasPostFX) return false;
|
||||
if (obj.hasRigidbody || obj.hasRigidbody2D || obj.hasPlayerController) return false;
|
||||
if (obj.hasAnimation || obj.hasSkeletalAnimation) return false;
|
||||
@@ -463,6 +488,20 @@ void AppendTransformedTriangleVertices(const std::vector<float>& src, const glm:
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void ModuRuntime2DRenderCounters_Reset() {
|
||||
gRuntime2DRenderCounters = Runtime2DRenderCounters{};
|
||||
}
|
||||
|
||||
void ModuRuntime2DRenderCounters_Read(uint64_t* outTextureBindCount,
|
||||
uint64_t* outStateBindCount) {
|
||||
if (outTextureBindCount) {
|
||||
*outTextureBindCount = gRuntime2DRenderCounters.textureBindCount;
|
||||
}
|
||||
if (outStateBindCount) {
|
||||
*outStateBindCount = gRuntime2DRenderCounters.stateBindCount;
|
||||
}
|
||||
}
|
||||
|
||||
// Global OBJ loader instance
|
||||
OBJLoader g_objLoader;
|
||||
|
||||
@@ -1615,7 +1654,26 @@ void Renderer::releaseRenderTarget(RenderTarget& target) {
|
||||
void Renderer::updateMirrorTargets(const Camera& camera, const std::vector<SceneObject>& sceneObjects, int width, int height, float fovDeg, float nearPlane, float farPlane) {
|
||||
if (camera.orthographic || sceneObjects.empty() || width <= 0 || height <= 0) return;
|
||||
|
||||
bool hasEnabledMirror = false;
|
||||
for (const auto& obj : sceneObjects) {
|
||||
if (IsObjectEnabledInHierarchy(obj) && obj.hasRenderer && obj.renderType == RenderType::Mirror) {
|
||||
hasEnabledMirror = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!hasEnabledMirror) {
|
||||
if (!mirrorTargets.empty()) {
|
||||
for (auto& entry : mirrorTargets) {
|
||||
releaseRenderTarget(entry.second);
|
||||
}
|
||||
mirrorTargets.clear();
|
||||
mirrorUpdateStates.clear();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
std::unordered_set<int> active;
|
||||
active.reserve(mirrorTargets.size() + 4);
|
||||
const double nowSec = glfwGetTime();
|
||||
GLint prevFBO = 0;
|
||||
glGetIntegerv(GL_FRAMEBUFFER_BINDING, &prevFBO);
|
||||
@@ -1634,7 +1692,7 @@ void Renderer::updateMirrorTargets(const Camera& camera, const std::vector<Scene
|
||||
};
|
||||
|
||||
for (const auto& obj : sceneObjects) {
|
||||
if (!obj.enabled || !obj.hasRenderer || obj.renderType != RenderType::Mirror) continue;
|
||||
if (!IsObjectEnabledInHierarchy(obj) || !obj.hasRenderer || obj.renderType != RenderType::Mirror) continue;
|
||||
active.insert(obj.id);
|
||||
|
||||
glm::vec3 n = planeNormal(obj);
|
||||
@@ -1877,18 +1935,22 @@ void Renderer::resize(int w, int h) {
|
||||
|
||||
void Renderer::beginRender(const glm::mat4& view, const glm::mat4& proj, const glm::vec3& cameraPos) {
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
|
||||
Runtime2DCountStateBind();
|
||||
glViewport(0, 0, currentWidth, currentHeight);
|
||||
glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
|
||||
displayTexture = viewportTexture;
|
||||
|
||||
shader->use();
|
||||
Runtime2DCountStateBind();
|
||||
shader->setMat4("view", view);
|
||||
shader->setMat4("projection", proj);
|
||||
shader->setVec3("viewPos", cameraPos);
|
||||
shader->setFloat("uTime", static_cast<float>(glfwGetTime()));
|
||||
texture1->Bind(GL_TEXTURE0);
|
||||
texture2->Bind(GL_TEXTURE1);
|
||||
Runtime2DCountTextureBind();
|
||||
Runtime2DCountTextureBind();
|
||||
shader->setInt("texture1", 0);
|
||||
shader->setInt("overlayTex", 1);
|
||||
shader->setInt("normalMap", 2);
|
||||
@@ -2084,7 +2146,7 @@ void Renderer::renderSceneInternal(const Camera& camera, const std::vector<Scene
|
||||
candidates.reserve(sceneObjects.size());
|
||||
|
||||
for (const auto& obj : sceneObjects) {
|
||||
if (!obj.enabled || !obj.hasLight || !obj.light.enabled) continue;
|
||||
if (!IsObjectEnabledInHierarchy(obj) || !obj.hasLight || !obj.light.enabled) continue;
|
||||
if (obj.light.type == LightType::Directional) {
|
||||
LightUniform l;
|
||||
l.type = 0;
|
||||
@@ -2162,11 +2224,19 @@ void Renderer::renderSceneInternal(const Camera& camera, const std::vector<Scene
|
||||
}
|
||||
|
||||
if (lights.size() < kMaxLights && !candidates.empty()) {
|
||||
std::sort(candidates.begin(), candidates.end(),
|
||||
[](const LightCandidate& a, const LightCandidate& b) {
|
||||
if (a.distSq != b.distSq) return a.distSq < b.distSq;
|
||||
return a.id < b.id;
|
||||
});
|
||||
const auto candidateLess = [](const LightCandidate& a, const LightCandidate& b) {
|
||||
if (a.distSq != b.distSq) return a.distSq < b.distSq;
|
||||
return a.id < b.id;
|
||||
};
|
||||
const size_t remainingSlots = kMaxLights - lights.size();
|
||||
if (candidates.size() > remainingSlots) {
|
||||
std::nth_element(candidates.begin(),
|
||||
candidates.begin() + remainingSlots,
|
||||
candidates.end(),
|
||||
candidateLess);
|
||||
candidates.resize(remainingSlots);
|
||||
}
|
||||
std::sort(candidates.begin(), candidates.end(), candidateLess);
|
||||
for (const auto& c : candidates) {
|
||||
if (lights.size() >= kMaxLights) break;
|
||||
lights.push_back(c.light);
|
||||
@@ -2187,7 +2257,10 @@ void Renderer::renderSceneInternal(const Camera& camera, const std::vector<Scene
|
||||
map.depthCube = 0;
|
||||
map.resolution = 0;
|
||||
};
|
||||
if (shadowDepthShader && shadowDepthShader->ID != 0) {
|
||||
const bool hasShadowCasters = std::any_of(lights.begin(), lights.end(), [](const LightUniform& light) {
|
||||
return light.castShadows && light.type != 0 && light.sourceId >= 0;
|
||||
});
|
||||
if (shadowDepthShader && shadowDepthShader->ID != 0 && hasShadowCasters) {
|
||||
GLint prevViewport[4] = {0, 0, width, height};
|
||||
GLint prevFbo = 0;
|
||||
GLboolean blendWasEnabled = glIsEnabled(GL_BLEND);
|
||||
@@ -2281,7 +2354,7 @@ void Renderer::renderSceneInternal(const Camera& camera, const std::vector<Scene
|
||||
shadowDepthShader->setMat4("lightSpaceMatrix", shadowProj * shadowViews[face]);
|
||||
|
||||
for (const auto& obj : sceneObjects) {
|
||||
if (!obj.enabled || !HasRendererComponent(obj)) continue;
|
||||
if (!IsObjectEnabledInHierarchy(obj) || !HasRendererComponent(obj)) continue;
|
||||
if (obj.renderType == RenderType::Mirror) continue;
|
||||
if (obj.renderType == RenderType::Sprite) continue;
|
||||
if (obj.id == light.sourceId) continue;
|
||||
@@ -2349,19 +2422,40 @@ void Renderer::renderSceneInternal(const Camera& camera, const std::vector<Scene
|
||||
renderSkybox(view, proj);
|
||||
rebuildStaticMergeBatches(sceneObjects);
|
||||
|
||||
const std::string emptyPath;
|
||||
auto combineHash = [](uint64_t seed, uint64_t value) {
|
||||
seed ^= value + 0x9e3779b97f4a7c15ull + (seed << 6) + (seed >> 2);
|
||||
return seed;
|
||||
};
|
||||
auto buildOpaqueSortKey = [&](const std::string& vert,
|
||||
const std::string& frag,
|
||||
const std::string& material,
|
||||
const std::string& albedo,
|
||||
const std::string& overlay,
|
||||
const std::string& normal) {
|
||||
uint64_t key = 1469598103934665603ull;
|
||||
key = combineHash(key, HashStringFNV1a(vert));
|
||||
key = combineHash(key, HashStringFNV1a(frag));
|
||||
key = combineHash(key, HashStringFNV1a(material));
|
||||
key = combineHash(key, HashStringFNV1a(albedo));
|
||||
key = combineHash(key, HashStringFNV1a(overlay));
|
||||
key = combineHash(key, HashStringFNV1a(normal));
|
||||
return key;
|
||||
};
|
||||
|
||||
struct RenderItem {
|
||||
const SceneObject* obj = nullptr;
|
||||
const StaticMergeBatch* staticBatch = nullptr;
|
||||
Mesh* mesh = nullptr;
|
||||
glm::mat4 model = glm::mat4(1.0f);
|
||||
glm::vec3 sortCenter = glm::vec3(0.0f);
|
||||
std::string vertPath;
|
||||
std::string fragPath;
|
||||
const std::string* vertPath = nullptr;
|
||||
const std::string* fragPath = nullptr;
|
||||
MaterialProperties material;
|
||||
std::string materialPath;
|
||||
std::string albedoTexturePath;
|
||||
std::string overlayTexturePath;
|
||||
std::string normalMapPath;
|
||||
const std::string* materialPath = nullptr;
|
||||
const std::string* albedoTexturePath = nullptr;
|
||||
const std::string* overlayTexturePath = nullptr;
|
||||
const std::string* normalMapPath = nullptr;
|
||||
bool useOverlay = false;
|
||||
int boneLimit = 0;
|
||||
int availableBones = 0;
|
||||
@@ -2370,10 +2464,11 @@ void Renderer::renderSceneInternal(const Camera& camera, const std::vector<Scene
|
||||
bool sortOpaque = false;
|
||||
bool unlit = false;
|
||||
float cameraDistanceSq = 0.0f;
|
||||
uint64_t opaqueSortKey = 0;
|
||||
};
|
||||
|
||||
std::vector<RenderItem> drawItems;
|
||||
drawItems.reserve(sceneObjects.size());
|
||||
drawItems.reserve(sceneObjects.size() + staticMergeBatches.size());
|
||||
|
||||
auto gatherRenderItemsRange = [&](size_t beginIndex, size_t endIndex) {
|
||||
std::vector<RenderItem> localItems;
|
||||
@@ -2381,7 +2476,7 @@ void Renderer::renderSceneInternal(const Camera& camera, const std::vector<Scene
|
||||
|
||||
for (size_t objIndex = beginIndex; objIndex < endIndex; ++objIndex) {
|
||||
const auto& obj = sceneObjects[objIndex];
|
||||
if (!obj.enabled) continue;
|
||||
if (!IsObjectEnabledInHierarchy(obj)) continue;
|
||||
if (!drawMirrorObjects && obj.hasRenderer && obj.renderType == RenderType::Mirror) continue;
|
||||
if (!HasRendererComponent(obj)) continue;
|
||||
if (staticMergeSourceIds.find(obj.id) != staticMergeSourceIds.end()) continue;
|
||||
@@ -2402,13 +2497,13 @@ void Renderer::renderSceneInternal(const Camera& camera, const std::vector<Scene
|
||||
item.mesh = mesh;
|
||||
item.model = model;
|
||||
item.sortCenter = obj.position;
|
||||
item.vertPath = obj.vertexShaderPath;
|
||||
item.fragPath = obj.fragmentShaderPath;
|
||||
item.vertPath = &obj.vertexShaderPath;
|
||||
item.fragPath = &obj.fragmentShaderPath;
|
||||
item.material = obj.material;
|
||||
item.materialPath = obj.materialPath;
|
||||
item.albedoTexturePath = obj.albedoTexturePath;
|
||||
item.overlayTexturePath = obj.overlayTexturePath;
|
||||
item.normalMapPath = obj.normalMapPath;
|
||||
item.materialPath = &obj.materialPath;
|
||||
item.albedoTexturePath = &obj.albedoTexturePath;
|
||||
item.overlayTexturePath = &obj.overlayTexturePath;
|
||||
item.normalMapPath = &obj.normalMapPath;
|
||||
item.useOverlay = obj.useOverlay;
|
||||
item.boneLimit = obj.skeletal.maxBones;
|
||||
item.availableBones = static_cast<int>(obj.skeletal.finalMatrices.size());
|
||||
@@ -2417,8 +2512,8 @@ void Renderer::renderSceneInternal(const Camera& camera, const std::vector<Scene
|
||||
item.boneLimit > 0 && item.availableBones > item.boneLimit;
|
||||
item.wantsGpuSkinning = obj.hasSkeletalAnimation && obj.skeletal.enabled &&
|
||||
obj.skeletal.useGpuSkinning && !needsFallback;
|
||||
if (item.vertPath.empty() && item.wantsGpuSkinning) {
|
||||
item.vertPath = skinnedVertPath;
|
||||
if (item.wantsGpuSkinning && item.vertPath->empty()) {
|
||||
item.vertPath = &skinnedVertPath;
|
||||
}
|
||||
item.isUiCanvas3D = obj.hasUI && obj.ui.type == UIElementType::Canvas && obj.ui.renderIn3D;
|
||||
item.sortOpaque = item.material.alpha >= 0.999f &&
|
||||
@@ -2426,6 +2521,15 @@ void Renderer::renderSceneInternal(const Camera& camera, const std::vector<Scene
|
||||
obj.renderType != RenderType::Sprite &&
|
||||
obj.renderType != RenderType::Mirror;
|
||||
item.unlit = obj.renderType == RenderType::Mirror || obj.renderType == RenderType::Sprite || item.isUiCanvas3D;
|
||||
if (item.sortOpaque) {
|
||||
item.opaqueSortKey = buildOpaqueSortKey(
|
||||
*item.vertPath,
|
||||
*item.fragPath,
|
||||
*item.materialPath,
|
||||
*item.albedoTexturePath,
|
||||
*item.overlayTexturePath,
|
||||
*item.normalMapPath);
|
||||
}
|
||||
glm::vec3 toCamera = item.sortCenter - camera.position;
|
||||
item.cameraDistanceSq = glm::dot(toCamera, toCamera);
|
||||
localItems.push_back(std::move(item));
|
||||
@@ -2435,9 +2539,9 @@ void Renderer::renderSceneInternal(const Camera& camera, const std::vector<Scene
|
||||
};
|
||||
|
||||
const unsigned int hardwareThreads = std::thread::hardware_concurrency();
|
||||
const size_t minItemsPerTask = 128;
|
||||
const size_t minItemsPerTask = 2048;
|
||||
const size_t maxTaskCountByWork = std::max<size_t>(1, sceneObjects.size() / minItemsPerTask);
|
||||
const size_t taskCount = (hardwareThreads > 1 && sceneObjects.size() >= minItemsPerTask * 2)
|
||||
const size_t taskCount = (hardwareThreads > 4 && sceneObjects.size() >= minItemsPerTask * 2)
|
||||
? std::min<size_t>(hardwareThreads, maxTaskCountByWork)
|
||||
: 1;
|
||||
|
||||
@@ -2480,15 +2584,24 @@ void Renderer::renderSceneInternal(const Camera& camera, const std::vector<Scene
|
||||
item.mesh = batch.mesh.get();
|
||||
item.model = glm::mat4(1.0f);
|
||||
item.sortCenter = batch.boundsCenter;
|
||||
item.vertPath = batch.vertPath;
|
||||
item.fragPath = batch.fragPath;
|
||||
item.vertPath = &batch.vertPath;
|
||||
item.fragPath = &batch.fragPath;
|
||||
item.material = batch.material;
|
||||
item.materialPath = batch.materialPath;
|
||||
item.albedoTexturePath = batch.albedoTexturePath;
|
||||
item.overlayTexturePath = batch.overlayTexturePath;
|
||||
item.normalMapPath = batch.normalMapPath;
|
||||
item.materialPath = &batch.materialPath;
|
||||
item.albedoTexturePath = &batch.albedoTexturePath;
|
||||
item.overlayTexturePath = &batch.overlayTexturePath;
|
||||
item.normalMapPath = &batch.normalMapPath;
|
||||
item.useOverlay = batch.useOverlay;
|
||||
item.sortOpaque = item.material.alpha >= 0.999f;
|
||||
if (item.sortOpaque) {
|
||||
item.opaqueSortKey = buildOpaqueSortKey(
|
||||
*item.vertPath,
|
||||
*item.fragPath,
|
||||
*item.materialPath,
|
||||
*item.albedoTexturePath,
|
||||
*item.overlayTexturePath,
|
||||
*item.normalMapPath);
|
||||
}
|
||||
glm::vec3 toCamera = item.sortCenter - camera.position;
|
||||
item.cameraDistanceSq = glm::dot(toCamera, toCamera);
|
||||
drawItems.push_back(std::move(item));
|
||||
@@ -2505,12 +2618,7 @@ void Renderer::renderSceneInternal(const Camera& camera, const std::vector<Scene
|
||||
int bId = b.obj ? b.obj->id : -1;
|
||||
return aId < bId;
|
||||
}
|
||||
if (a.vertPath != b.vertPath) return a.vertPath < b.vertPath;
|
||||
if (a.fragPath != b.fragPath) return a.fragPath < b.fragPath;
|
||||
if (a.materialPath != b.materialPath) return a.materialPath < b.materialPath;
|
||||
if (a.albedoTexturePath != b.albedoTexturePath) return a.albedoTexturePath < b.albedoTexturePath;
|
||||
if (a.overlayTexturePath != b.overlayTexturePath) return a.overlayTexturePath < b.overlayTexturePath;
|
||||
if (a.normalMapPath != b.normalMapPath) return a.normalMapPath < b.normalMapPath;
|
||||
if (a.opaqueSortKey != b.opaqueSortKey) return a.opaqueSortKey < b.opaqueSortKey;
|
||||
int aId = a.obj ? a.obj->id : -1;
|
||||
int bId = b.obj ? b.obj->id : -1;
|
||||
return aId < bId;
|
||||
@@ -2527,6 +2635,7 @@ void Renderer::renderSceneInternal(const Camera& camera, const std::vector<Scene
|
||||
if (currentActiveTexture != static_cast<GLint>(unit)) {
|
||||
glActiveTexture(unit);
|
||||
currentActiveTexture = static_cast<GLint>(unit);
|
||||
Runtime2DCountStateBind();
|
||||
}
|
||||
int slot = static_cast<int>(unit - GL_TEXTURE0);
|
||||
if (slot >= 0 && slot < static_cast<int>(boundTexture2D.size()) &&
|
||||
@@ -2534,20 +2643,69 @@ void Renderer::renderSceneInternal(const Camera& camera, const std::vector<Scene
|
||||
return;
|
||||
}
|
||||
glBindTexture(GL_TEXTURE_2D, textureId);
|
||||
Runtime2DCountTextureBind();
|
||||
if (slot >= 0 && slot < static_cast<int>(boundTexture2D.size())) {
|
||||
boundTexture2D[slot] = textureId;
|
||||
}
|
||||
};
|
||||
|
||||
struct LightUniformNameCache {
|
||||
std::array<std::string, kMaxLights> type;
|
||||
std::array<std::string, kMaxLights> dir;
|
||||
std::array<std::string, kMaxLights> pos;
|
||||
std::array<std::string, kMaxLights> color;
|
||||
std::array<std::string, kMaxLights> intensity;
|
||||
std::array<std::string, kMaxLights> range;
|
||||
std::array<std::string, kMaxLights> innerCos;
|
||||
std::array<std::string, kMaxLights> outerCos;
|
||||
std::array<std::string, kMaxLights> areaSize;
|
||||
std::array<std::string, kMaxLights> areaFade;
|
||||
std::array<std::string, kMaxLights> shadowMap;
|
||||
std::array<std::string, kMaxLights> shadowMode;
|
||||
std::array<std::string, kMaxLights> shadowBias;
|
||||
std::array<std::string, kMaxLights> shadowSoftness;
|
||||
std::array<std::string, kMaxLights> shadowFar;
|
||||
};
|
||||
static const LightUniformNameCache kLightNames = []() {
|
||||
LightUniformNameCache names;
|
||||
for (size_t i = 0; i < kMaxLights; ++i) {
|
||||
const std::string idx = "[" + std::to_string(i) + "]";
|
||||
names.type[i] = "lightTypeArr" + idx;
|
||||
names.dir[i] = "lightDirArr" + idx;
|
||||
names.pos[i] = "lightPosArr" + idx;
|
||||
names.color[i] = "lightColorArr" + idx;
|
||||
names.intensity[i] = "lightIntensityArr" + idx;
|
||||
names.range[i] = "lightRangeArr" + idx;
|
||||
names.innerCos[i] = "lightInnerCosArr" + idx;
|
||||
names.outerCos[i] = "lightOuterCosArr" + idx;
|
||||
names.areaSize[i] = "lightAreaSizeArr" + idx;
|
||||
names.areaFade[i] = "lightAreaFadeArr" + idx;
|
||||
names.shadowMap[i] = "lightShadowMapArr" + idx;
|
||||
names.shadowMode[i] = "lightShadowModeArr" + idx;
|
||||
names.shadowBias[i] = "lightShadowBiasArr" + idx;
|
||||
names.shadowSoftness[i] = "lightShadowSoftnessArr" + idx;
|
||||
names.shadowFar[i] = "lightShadowFarArr" + idx;
|
||||
}
|
||||
return names;
|
||||
}();
|
||||
|
||||
Shader* currentShader = nullptr;
|
||||
for (const RenderItem& item : drawItems) {
|
||||
const SceneObject* objPtr = item.obj;
|
||||
Shader* active = getShader(item.vertPath, item.fragPath);
|
||||
const std::string& vertPath = item.vertPath ? *item.vertPath : emptyPath;
|
||||
const std::string& fragPath = item.fragPath ? *item.fragPath : emptyPath;
|
||||
const std::string& materialPath = item.materialPath ? *item.materialPath : emptyPath;
|
||||
const std::string& albedoTexturePath = item.albedoTexturePath ? *item.albedoTexturePath : emptyPath;
|
||||
const std::string& overlayTexturePath = item.overlayTexturePath ? *item.overlayTexturePath : emptyPath;
|
||||
const std::string& normalMapPath = item.normalMapPath ? *item.normalMapPath : emptyPath;
|
||||
|
||||
Shader* active = getShader(vertPath, fragPath);
|
||||
if (!active) continue;
|
||||
shader = active;
|
||||
if (currentShader != shader) {
|
||||
currentShader = shader;
|
||||
shader->use();
|
||||
Runtime2DCountStateBind();
|
||||
shader->setMat4("view", view);
|
||||
shader->setMat4("projection", proj);
|
||||
shader->setVec3("viewPos", camera.position);
|
||||
@@ -2563,28 +2721,27 @@ void Renderer::renderSceneInternal(const Camera& camera, const std::vector<Scene
|
||||
shader->setInt("lightCount", static_cast<int>(lights.size()));
|
||||
for (size_t i = 0; i < lights.size() && i < kMaxLights; ++i) {
|
||||
const auto& l = lights[i];
|
||||
std::string idx = "[" + std::to_string(i) + "]";
|
||||
shader->setInt("lightTypeArr" + idx, l.type);
|
||||
shader->setVec3("lightDirArr" + idx, l.dir);
|
||||
shader->setVec3("lightPosArr" + idx, l.pos);
|
||||
shader->setVec3("lightColorArr" + idx, l.color);
|
||||
shader->setFloat("lightIntensityArr" + idx, l.intensity);
|
||||
shader->setFloat("lightRangeArr" + idx, l.range);
|
||||
shader->setFloat("lightInnerCosArr" + idx, l.inner);
|
||||
shader->setFloat("lightOuterCosArr" + idx, l.outer);
|
||||
shader->setVec2("lightAreaSizeArr" + idx, l.areaSize);
|
||||
shader->setFloat("lightAreaFadeArr" + idx, l.areaFade);
|
||||
shader->setInt("lightShadowMapArr" + idx, l.shadowMapIndex);
|
||||
shader->setInt("lightShadowModeArr" + idx, (l.shadowMapIndex >= 0) ? l.shadowMode : 0);
|
||||
shader->setFloat("lightShadowBiasArr" + idx, l.shadowBias);
|
||||
shader->setFloat("lightShadowSoftnessArr" + idx, l.shadowSoftness);
|
||||
shader->setFloat("lightShadowFarArr" + idx, l.shadowFar);
|
||||
shader->setInt(kLightNames.type[i], l.type);
|
||||
shader->setVec3(kLightNames.dir[i], l.dir);
|
||||
shader->setVec3(kLightNames.pos[i], l.pos);
|
||||
shader->setVec3(kLightNames.color[i], l.color);
|
||||
shader->setFloat(kLightNames.intensity[i], l.intensity);
|
||||
shader->setFloat(kLightNames.range[i], l.range);
|
||||
shader->setFloat(kLightNames.innerCos[i], l.inner);
|
||||
shader->setFloat(kLightNames.outerCos[i], l.outer);
|
||||
shader->setVec2(kLightNames.areaSize[i], l.areaSize);
|
||||
shader->setFloat(kLightNames.areaFade[i], l.areaFade);
|
||||
shader->setInt(kLightNames.shadowMap[i], l.shadowMapIndex);
|
||||
shader->setInt(kLightNames.shadowMode[i], (l.shadowMapIndex >= 0) ? l.shadowMode : 0);
|
||||
shader->setFloat(kLightNames.shadowBias[i], l.shadowBias);
|
||||
shader->setFloat(kLightNames.shadowSoftness[i], l.shadowSoftness);
|
||||
shader->setFloat(kLightNames.shadowFar[i], l.shadowFar);
|
||||
}
|
||||
}
|
||||
|
||||
bool hasMaterialAsset = !item.materialPath.empty();
|
||||
bool hasCustomShader = !item.vertPath.empty() || !item.fragPath.empty();
|
||||
bool hasAnySurfaceInput = !item.albedoTexturePath.empty() || !item.overlayTexturePath.empty() || !item.normalMapPath.empty();
|
||||
bool hasMaterialAsset = !materialPath.empty();
|
||||
bool hasCustomShader = !vertPath.empty() || !fragPath.empty();
|
||||
bool hasAnySurfaceInput = !albedoTexturePath.empty() || !overlayTexturePath.empty() || !normalMapPath.empty();
|
||||
bool missingMaterialAndShader = !hasMaterialAsset && !hasCustomShader && !hasAnySurfaceInput;
|
||||
|
||||
shader->setBool("unlit", item.unlit || missingMaterialAndShader);
|
||||
@@ -2625,8 +2782,8 @@ void Renderer::renderSceneInternal(const Camera& camera, const std::vector<Scene
|
||||
if (missingMaterialAndShader && missingMaterialFallbackTexture != 0) {
|
||||
bindTexture2D(GL_TEXTURE0, missingMaterialFallbackTexture);
|
||||
} else {
|
||||
if (!item.albedoTexturePath.empty()) {
|
||||
if (auto* t = getTexture(item.albedoTexturePath, item.material.textureFilter)) baseTex = t;
|
||||
if (!albedoTexturePath.empty()) {
|
||||
if (auto* t = getTexture(albedoTexturePath, item.material.textureFilter)) baseTex = t;
|
||||
}
|
||||
if (baseTex) bindTexture2D(GL_TEXTURE0, baseTex->GetID());
|
||||
}
|
||||
@@ -2640,8 +2797,8 @@ void Renderer::renderSceneInternal(const Camera& camera, const std::vector<Scene
|
||||
overlayUsed = true;
|
||||
}
|
||||
}
|
||||
if (!overlayUsed && item.useOverlay && !item.overlayTexturePath.empty()) {
|
||||
if (auto* t = getTexture(item.overlayTexturePath, item.material.textureFilter)) {
|
||||
if (!overlayUsed && item.useOverlay && !overlayTexturePath.empty()) {
|
||||
if (auto* t = getTexture(overlayTexturePath, item.material.textureFilter)) {
|
||||
bindTexture2D(GL_TEXTURE1, t->GetID());
|
||||
overlayUsed = true;
|
||||
}
|
||||
@@ -2652,8 +2809,8 @@ void Renderer::renderSceneInternal(const Camera& camera, const std::vector<Scene
|
||||
shader->setBool("hasOverlay", overlayUsed);
|
||||
|
||||
bool normalUsed = false;
|
||||
if (!item.normalMapPath.empty()) {
|
||||
if (auto* t = getTexture(item.normalMapPath, item.material.textureFilter)) {
|
||||
if (!normalMapPath.empty()) {
|
||||
if (auto* t = getTexture(normalMapPath, item.material.textureFilter)) {
|
||||
bindTexture2D(GL_TEXTURE2, t->GetID());
|
||||
normalUsed = true;
|
||||
}
|
||||
@@ -2675,9 +2832,11 @@ void Renderer::renderSceneInternal(const Camera& camera, const std::vector<Scene
|
||||
bool doubleSided = objPtr && (objPtr->renderType == RenderType::Sprite || objPtr->renderType == RenderType::Mirror);
|
||||
if (doubleSided) {
|
||||
glDisable(GL_CULL_FACE);
|
||||
Runtime2DCountStateBind();
|
||||
} else {
|
||||
glEnable(GL_CULL_FACE);
|
||||
glCullFace(GL_BACK);
|
||||
Runtime2DCountStateBind();
|
||||
}
|
||||
|
||||
bool wantsTransparency = !item.sortOpaque;
|
||||
@@ -2686,18 +2845,23 @@ void Renderer::renderSceneInternal(const Camera& camera, const std::vector<Scene
|
||||
if (!currentBlendEnabled) {
|
||||
glEnable(GL_BLEND);
|
||||
currentBlendEnabled = true;
|
||||
Runtime2DCountStateBind();
|
||||
}
|
||||
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||
Runtime2DCountStateBind();
|
||||
} else if (currentBlendEnabled) {
|
||||
glDisable(GL_BLEND);
|
||||
currentBlendEnabled = false;
|
||||
Runtime2DCountStateBind();
|
||||
}
|
||||
if ((wantsTransparency || item.isUiCanvas3D) && currentDepthMask) {
|
||||
glDepthMask(GL_FALSE);
|
||||
currentDepthMask = false;
|
||||
Runtime2DCountStateBind();
|
||||
} else if (!(wantsTransparency || item.isUiCanvas3D) && !currentDepthMask) {
|
||||
glDepthMask(GL_TRUE);
|
||||
currentDepthMask = true;
|
||||
Runtime2DCountStateBind();
|
||||
}
|
||||
recordMeshDraw();
|
||||
item.mesh->draw();
|
||||
@@ -2830,22 +2994,23 @@ unsigned int Renderer::applyPostProcessing(const Camera& camera, const std::vect
|
||||
postStats.activeEffectCount = CountEnabledPostEffects(settings);
|
||||
postStats.hdrEnabled = settings.hdrEnabled;
|
||||
|
||||
GLint polygonMode[2] = { GL_FILL, GL_FILL };
|
||||
#ifdef GL_POLYGON_MODE
|
||||
glGetIntegerv(GL_POLYGON_MODE, polygonMode);
|
||||
#endif
|
||||
bool wireframe = (polygonMode[0] == GL_LINE || polygonMode[1] == GL_LINE);
|
||||
|
||||
bool wantsEffects = settings.enabled &&
|
||||
(settings.hdrEnabled || settings.bloomEnabled || settings.colorAdjustEnabled ||
|
||||
settings.motionBlurEnabled || settings.vignetteEnabled ||
|
||||
settings.chromaticAberrationEnabled || settings.sharpenEnabled ||
|
||||
settings.ambientOcclusionEnabled);
|
||||
|
||||
if (wireframe) {
|
||||
wantsEffects = false;
|
||||
postStats.activeEffectCount = 0;
|
||||
postStats.hdrEnabled = false;
|
||||
if (wantsEffects) {
|
||||
GLint polygonMode[2] = { GL_FILL, GL_FILL };
|
||||
#ifdef GL_POLYGON_MODE
|
||||
glGetIntegerv(GL_POLYGON_MODE, polygonMode);
|
||||
#endif
|
||||
bool wireframe = (polygonMode[0] == GL_LINE || polygonMode[1] == GL_LINE);
|
||||
if (wireframe) {
|
||||
wantsEffects = false;
|
||||
postStats.activeEffectCount = 0;
|
||||
postStats.hdrEnabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!wantsEffects || !postShader || width <= 0 || height <= 0 || sourceTexture == 0) {
|
||||
@@ -3109,7 +3274,7 @@ void Renderer::renderCollisionOverlay(const Camera& camera, const std::vector<Sc
|
||||
|
||||
for (const auto& obj : sceneObjects) {
|
||||
if (!previewSet.empty() && previewSet.find(obj.id) == previewSet.end()) continue;
|
||||
if (!obj.enabled) continue;
|
||||
if (!IsObjectEnabledInHierarchy(obj)) continue;
|
||||
if (!(obj.hasCollider && obj.collider.enabled) && !(obj.hasRigidbody && obj.rigidbody.enabled)) continue;
|
||||
|
||||
Mesh* meshToDraw = nullptr;
|
||||
@@ -3287,7 +3452,7 @@ void Renderer::renderSelectionOutline(const Camera& camera, const std::vector<Sc
|
||||
drawItems.reserve(selectedSet.size());
|
||||
|
||||
for (const auto& obj : sceneObjects) {
|
||||
if (!obj.enabled) continue;
|
||||
if (!IsObjectEnabledInHierarchy(obj)) continue;
|
||||
if (selectedSet.find(obj.id) == selectedSet.end()) continue;
|
||||
if (!HasRendererComponent(obj)) continue;
|
||||
|
||||
@@ -3520,4 +3685,5 @@ void Renderer::renderSelectionOutline(const Camera& camera, const std::vector<Sc
|
||||
|
||||
void Renderer::endRender() {
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
Runtime2DCountStateBind();
|
||||
}
|
||||
|
||||
@@ -42,6 +42,13 @@ public:
|
||||
class OBJLoader {
|
||||
public:
|
||||
struct LoadedMesh {
|
||||
struct SubMesh {
|
||||
std::unique_ptr<Mesh> mesh;
|
||||
int materialIndex = 0;
|
||||
int vertexCount = 0;
|
||||
int faceCount = 0;
|
||||
};
|
||||
|
||||
std::string path;
|
||||
std::unique_ptr<Mesh> mesh;
|
||||
std::string name;
|
||||
@@ -60,6 +67,8 @@ public:
|
||||
std::vector<glm::ivec4> boneIds;
|
||||
std::vector<glm::vec4> boneWeights;
|
||||
std::vector<float> baseVertices;
|
||||
std::vector<SubMesh> subMeshes;
|
||||
std::vector<std::string> materialSlots;
|
||||
};
|
||||
|
||||
private:
|
||||
|
||||
@@ -81,6 +81,18 @@ enum class UIAnchor {
|
||||
BottomRight = 4
|
||||
};
|
||||
|
||||
enum class UITextHAlign {
|
||||
Left = 0,
|
||||
Center = 1,
|
||||
Right = 2
|
||||
};
|
||||
|
||||
enum class UITextVAlign {
|
||||
Top = 0,
|
||||
Middle = 1,
|
||||
Bottom = 2
|
||||
};
|
||||
|
||||
enum class UISliderStyle {
|
||||
ImGui = 0,
|
||||
Fill = 1,
|
||||
@@ -161,17 +173,127 @@ struct AnimationPropertyTrack {
|
||||
std::vector<AnimationPropertyKeyframe> keyframes;
|
||||
};
|
||||
|
||||
struct AnimationClipSlot {
|
||||
std::string name;
|
||||
std::string assetPath;
|
||||
};
|
||||
|
||||
struct AnimationComponent {
|
||||
bool enabled = true;
|
||||
std::string clipAssetPath;
|
||||
std::vector<AnimationClipSlot> clips;
|
||||
int activeClipIndex = -1;
|
||||
float clipLength = 2.0f;
|
||||
float playSpeed = 1.0f;
|
||||
bool loop = true;
|
||||
bool playOnAwake = true;
|
||||
bool applyOnScrub = true;
|
||||
bool runtimePlaying = false;
|
||||
bool runtimePaused = false;
|
||||
float runtimeTime = 0.0f;
|
||||
float runtimeDirection = 1.0f;
|
||||
bool runtimeInitialized = false;
|
||||
std::string runtimeClipPath;
|
||||
std::vector<AnimationKeyframe> keyframes;
|
||||
std::vector<AnimationEvent> events;
|
||||
std::vector<AnimationPropertyTrack> tracks;
|
||||
};
|
||||
|
||||
inline std::string AnimationClipNameFromPath(const std::string& assetPath) {
|
||||
if (assetPath.empty()) return "Animation";
|
||||
fs::path path(assetPath);
|
||||
std::string stem = path.stem().string();
|
||||
if (!stem.empty()) return stem;
|
||||
std::string fileName = path.filename().string();
|
||||
if (!fileName.empty()) return fileName;
|
||||
return "Animation";
|
||||
}
|
||||
|
||||
inline int AnimationGetActiveClipIndex(const AnimationComponent& animation) {
|
||||
if (animation.clips.empty()) return -1;
|
||||
if (animation.activeClipIndex >= 0 &&
|
||||
animation.activeClipIndex < static_cast<int>(animation.clips.size())) {
|
||||
return animation.activeClipIndex;
|
||||
}
|
||||
if (!animation.clipAssetPath.empty()) {
|
||||
for (int i = 0; i < static_cast<int>(animation.clips.size()); ++i) {
|
||||
if (animation.clips[i].assetPath == animation.clipAssetPath) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
inline const AnimationClipSlot* AnimationGetActiveClip(const AnimationComponent& animation) {
|
||||
const int index = AnimationGetActiveClipIndex(animation);
|
||||
if (index < 0 || index >= static_cast<int>(animation.clips.size())) return nullptr;
|
||||
return &animation.clips[index];
|
||||
}
|
||||
|
||||
inline AnimationClipSlot* AnimationGetActiveClip(AnimationComponent& animation) {
|
||||
const int index = AnimationGetActiveClipIndex(animation);
|
||||
if (index < 0 || index >= static_cast<int>(animation.clips.size())) return nullptr;
|
||||
return &animation.clips[index];
|
||||
}
|
||||
|
||||
inline std::string AnimationGetActiveClipAssetPath(const AnimationComponent& animation) {
|
||||
const AnimationClipSlot* clip = AnimationGetActiveClip(animation);
|
||||
if (clip) return clip->assetPath;
|
||||
return animation.clipAssetPath;
|
||||
}
|
||||
|
||||
inline std::string AnimationGetActiveClipName(const AnimationComponent& animation) {
|
||||
const AnimationClipSlot* clip = AnimationGetActiveClip(animation);
|
||||
if (clip == nullptr) {
|
||||
if (!animation.clipAssetPath.empty()) {
|
||||
return AnimationClipNameFromPath(animation.clipAssetPath);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
if (!clip->name.empty()) return clip->name;
|
||||
return AnimationClipNameFromPath(clip->assetPath);
|
||||
}
|
||||
|
||||
inline void NormalizeAnimationClipSlots(AnimationComponent& animation) {
|
||||
if (!animation.clipAssetPath.empty()) {
|
||||
bool foundLegacyPath = false;
|
||||
for (AnimationClipSlot& clip : animation.clips) {
|
||||
if (clip.assetPath == animation.clipAssetPath) {
|
||||
foundLegacyPath = true;
|
||||
}
|
||||
if (clip.name.empty()) {
|
||||
clip.name = AnimationClipNameFromPath(clip.assetPath);
|
||||
}
|
||||
}
|
||||
if (!foundLegacyPath) {
|
||||
AnimationClipSlot clip;
|
||||
clip.assetPath = animation.clipAssetPath;
|
||||
clip.name = AnimationClipNameFromPath(clip.assetPath);
|
||||
animation.clips.push_back(std::move(clip));
|
||||
}
|
||||
} else {
|
||||
for (AnimationClipSlot& clip : animation.clips) {
|
||||
if (clip.name.empty()) {
|
||||
clip.name = AnimationClipNameFromPath(clip.assetPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (animation.clips.empty()) {
|
||||
animation.activeClipIndex = -1;
|
||||
animation.clipAssetPath.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
int resolvedIndex = AnimationGetActiveClipIndex(animation);
|
||||
if (resolvedIndex < 0 || resolvedIndex >= static_cast<int>(animation.clips.size())) {
|
||||
resolvedIndex = 0;
|
||||
}
|
||||
animation.activeClipIndex = resolvedIndex;
|
||||
animation.clipAssetPath = animation.clips[resolvedIndex].assetPath;
|
||||
}
|
||||
|
||||
struct SkeletalAnimationComponent {
|
||||
bool enabled = true;
|
||||
bool useGpuSkinning = true;
|
||||
@@ -347,6 +469,7 @@ struct UIElementComponent {
|
||||
UIAnchor anchor = UIAnchor::Center;
|
||||
glm::vec2 position = glm::vec2(0.0f); // offset in pixels from anchor
|
||||
glm::vec2 size = glm::vec2(160.0f, 40.0f);
|
||||
bool maskChildren = true; // Canvas-only: clip descendants to this canvas rect.
|
||||
float rotation = 0.0f;
|
||||
float sliderValue = 0.5f;
|
||||
float sliderMin = 0.0f;
|
||||
@@ -359,6 +482,12 @@ struct UIElementComponent {
|
||||
UIButtonStyle buttonStyle = UIButtonStyle::ImGui;
|
||||
std::string stylePreset = "Default";
|
||||
float textScale = 1.0f;
|
||||
bool textAutoWrap = true;
|
||||
UITextHAlign textHAlign = UITextHAlign::Left;
|
||||
UITextVAlign textVAlign = UITextVAlign::Top;
|
||||
int textEffectFlags = 0;
|
||||
float textEffectSpeed = 1.0f;
|
||||
float textEffectIntensity = 1.0f;
|
||||
bool renderIn3D = false;
|
||||
glm::ivec2 renderTargetSize = glm::ivec2(512, 512);
|
||||
bool spriteSheetEnabled = false;
|
||||
@@ -373,6 +502,10 @@ struct UIElementComponent {
|
||||
std::vector<glm::ivec4> spriteCustomFrames;
|
||||
std::vector<std::string> spriteCustomFrameNames;
|
||||
std::vector<glm::vec2> spriteCustomFrameScales;
|
||||
bool nineSliceEnabled = false;
|
||||
glm::vec4 nineSliceBorder = glm::vec4(12.0f, 12.0f, 12.0f, 12.0f); // left, right, top, bottom in source pixels
|
||||
bool nineSliceTileEdges = true;
|
||||
bool nineSliceTileCenter = false;
|
||||
};
|
||||
|
||||
struct Rigidbody2DComponent {
|
||||
@@ -486,6 +619,8 @@ public:
|
||||
std::string name;
|
||||
ObjectType type;
|
||||
bool enabled = true;
|
||||
// Derived each hierarchy update: true when all ancestors are locally enabled.
|
||||
bool hierarchyEnabled = true;
|
||||
int layer = 0;
|
||||
std::string tag = "Untagged";
|
||||
bool hasRenderer = false;
|
||||
@@ -569,6 +704,10 @@ inline bool HasRendererComponent(const SceneObject& obj) {
|
||||
return obj.hasRenderer && obj.renderType != RenderType::None;
|
||||
}
|
||||
|
||||
inline bool IsObjectEnabledInHierarchy(const SceneObject& obj) {
|
||||
return obj.enabled && obj.hierarchyEnabled;
|
||||
}
|
||||
|
||||
inline bool HasUIComponent(const SceneObject& obj) {
|
||||
return obj.hasUI && obj.ui.type != UIElementType::None;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <optional>
|
||||
#include <sstream>
|
||||
#include <regex>
|
||||
#include <unordered_set>
|
||||
#if defined(_WIN32)
|
||||
#include <windows.h>
|
||||
#endif
|
||||
@@ -75,6 +76,128 @@ namespace {
|
||||
return t;
|
||||
}
|
||||
|
||||
struct DependencyInfo {
|
||||
bool hasDepFile = false;
|
||||
bool missingDependency = false;
|
||||
std::optional<fs::file_time_type> newestInput;
|
||||
};
|
||||
|
||||
DependencyInfo readDependencyInfo(const fs::path& depFilePath) {
|
||||
DependencyInfo info;
|
||||
if (depFilePath.empty()) {
|
||||
return info;
|
||||
}
|
||||
|
||||
info.hasDepFile = true;
|
||||
std::error_code ec;
|
||||
if (!fs::exists(depFilePath, ec) || ec) {
|
||||
info.missingDependency = true;
|
||||
return info;
|
||||
}
|
||||
|
||||
std::ifstream depFile(depFilePath, std::ios::binary);
|
||||
if (!depFile.is_open()) {
|
||||
info.missingDependency = true;
|
||||
return info;
|
||||
}
|
||||
|
||||
std::ostringstream depStream;
|
||||
depStream << depFile.rdbuf();
|
||||
std::string content = depStream.str();
|
||||
if (content.empty()) {
|
||||
info.missingDependency = true;
|
||||
return info;
|
||||
}
|
||||
|
||||
// Flatten line continuations used by Make-style dep files.
|
||||
std::string flattened;
|
||||
flattened.reserve(content.size());
|
||||
for (size_t i = 0; i < content.size(); ++i) {
|
||||
if (content[i] == '\\') {
|
||||
if (i + 1 < content.size() && content[i + 1] == '\n') {
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
if (i + 2 < content.size() && content[i + 1] == '\r' && content[i + 2] == '\n') {
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
flattened.push_back(content[i]);
|
||||
}
|
||||
|
||||
const size_t colonPos = flattened.find(':');
|
||||
if (colonPos == std::string::npos || colonPos + 1 >= flattened.size()) {
|
||||
info.missingDependency = true;
|
||||
return info;
|
||||
}
|
||||
|
||||
std::string depList = flattened.substr(colonPos + 1);
|
||||
std::vector<std::string> tokens;
|
||||
std::string current;
|
||||
bool escaped = false;
|
||||
for (char c : depList) {
|
||||
if (escaped) {
|
||||
current.push_back(c);
|
||||
escaped = false;
|
||||
continue;
|
||||
}
|
||||
if (c == '\\') {
|
||||
escaped = true;
|
||||
continue;
|
||||
}
|
||||
if (std::isspace(static_cast<unsigned char>(c))) {
|
||||
if (!current.empty()) {
|
||||
tokens.push_back(current);
|
||||
current.clear();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
current.push_back(c);
|
||||
}
|
||||
if (escaped) {
|
||||
current.push_back('\\');
|
||||
}
|
||||
if (!current.empty()) {
|
||||
tokens.push_back(current);
|
||||
}
|
||||
|
||||
if (tokens.empty()) {
|
||||
info.missingDependency = true;
|
||||
return info;
|
||||
}
|
||||
|
||||
std::unordered_set<std::string> seen;
|
||||
for (const std::string& rawToken : tokens) {
|
||||
if (rawToken.empty() || !seen.insert(rawToken).second) continue;
|
||||
|
||||
fs::path depPath = fs::path(rawToken);
|
||||
if (depPath.is_relative()) {
|
||||
depPath = fs::absolute(depPath, ec);
|
||||
ec.clear();
|
||||
}
|
||||
|
||||
if (!fs::exists(depPath, ec) || ec) {
|
||||
info.missingDependency = true;
|
||||
ec.clear();
|
||||
continue;
|
||||
}
|
||||
|
||||
auto depTime = fs::last_write_time(depPath, ec);
|
||||
if (ec) {
|
||||
info.missingDependency = true;
|
||||
ec.clear();
|
||||
continue;
|
||||
}
|
||||
info.newestInput = info.newestInput ? std::max(*info.newestInput, depTime) : depTime;
|
||||
}
|
||||
|
||||
if (!info.newestInput) {
|
||||
info.missingDependency = true;
|
||||
}
|
||||
return info;
|
||||
}
|
||||
|
||||
#if !defined(_WIN32)
|
||||
std::string posixCompileDriver(bool cxx) {
|
||||
static int ccacheAvailable = -1;
|
||||
@@ -372,6 +495,8 @@ bool ScriptCompiler::makeCommands(const ScriptBuildConfig& config, const fs::pat
|
||||
std::string baseName = scriptAbs.stem().string();
|
||||
fs::path objectPath = config.outDir / relativeParent / (baseName + ".o");
|
||||
fs::path secondaryObjectPath;
|
||||
fs::path dependencyPath;
|
||||
fs::path secondaryDependencyPath;
|
||||
|
||||
fs::path binaryPath = config.outDir / relativeParent;
|
||||
#ifdef _WIN32
|
||||
@@ -379,6 +504,7 @@ bool ScriptCompiler::makeCommands(const ScriptBuildConfig& config, const fs::pat
|
||||
binaryPath /= baseName + ".dll";
|
||||
#else
|
||||
binaryPath /= baseName + ".so";
|
||||
dependencyPath = config.outDir / relativeParent / (baseName + ".d");
|
||||
#endif
|
||||
|
||||
std::string extLower = scriptAbs.extension().string();
|
||||
@@ -511,6 +637,7 @@ bool ScriptCompiler::makeCommands(const ScriptBuildConfig& config, const fs::pat
|
||||
secondaryObjectPath = config.outDir / relativeParent / (baseName + ".wrap.obj");
|
||||
#else
|
||||
secondaryObjectPath = config.outDir / relativeParent / (baseName + ".wrap.o");
|
||||
secondaryDependencyPath = config.outDir / relativeParent / (baseName + ".wrap.d");
|
||||
#endif
|
||||
std::ostringstream wrapper;
|
||||
|
||||
@@ -626,6 +753,50 @@ bool ScriptCompiler::makeCommands(const ScriptBuildConfig& config, const fs::pat
|
||||
wrapper << " ScriptContext* cpp = ModuAsCpp(ctx);\n";
|
||||
wrapper << " return (cpp && cpp->EnsureRigidbody(useGravity != 0, kinematic != 0)) ? 1 : 0;\n";
|
||||
wrapper << "}\n\n";
|
||||
wrapper << "int Modu_HasAnimation(ModuScriptContext* ctx) {\n";
|
||||
wrapper << " ScriptContext* cpp = ModuAsCpp(ctx);\n";
|
||||
wrapper << " return (cpp && cpp->HasAnimation()) ? 1 : 0;\n";
|
||||
wrapper << "}\n\n";
|
||||
wrapper << "int Modu_PlayAnimation(ModuScriptContext* ctx, int restart) {\n";
|
||||
wrapper << " ScriptContext* cpp = ModuAsCpp(ctx);\n";
|
||||
wrapper << " return (cpp && cpp->PlayAnimation(restart != 0)) ? 1 : 0;\n";
|
||||
wrapper << "}\n\n";
|
||||
wrapper << "int Modu_StopAnimation(ModuScriptContext* ctx, int resetTime) {\n";
|
||||
wrapper << " ScriptContext* cpp = ModuAsCpp(ctx);\n";
|
||||
wrapper << " return (cpp && cpp->StopAnimation(resetTime != 0)) ? 1 : 0;\n";
|
||||
wrapper << "}\n\n";
|
||||
wrapper << "int Modu_PauseAnimation(ModuScriptContext* ctx, int pause) {\n";
|
||||
wrapper << " ScriptContext* cpp = ModuAsCpp(ctx);\n";
|
||||
wrapper << " return (cpp && cpp->PauseAnimation(pause != 0)) ? 1 : 0;\n";
|
||||
wrapper << "}\n\n";
|
||||
wrapper << "int Modu_ReverseAnimation(ModuScriptContext* ctx, int restartIfStopped) {\n";
|
||||
wrapper << " ScriptContext* cpp = ModuAsCpp(ctx);\n";
|
||||
wrapper << " return (cpp && cpp->ReverseAnimation(restartIfStopped != 0)) ? 1 : 0;\n";
|
||||
wrapper << "}\n\n";
|
||||
wrapper << "int Modu_SetAnimationTime(ModuScriptContext* ctx, float timeSeconds) {\n";
|
||||
wrapper << " ScriptContext* cpp = ModuAsCpp(ctx);\n";
|
||||
wrapper << " return (cpp && cpp->SetAnimationTime(timeSeconds)) ? 1 : 0;\n";
|
||||
wrapper << "}\n\n";
|
||||
wrapper << "float Modu_GetAnimationTime(ModuScriptContext* ctx) {\n";
|
||||
wrapper << " ScriptContext* cpp = ModuAsCpp(ctx);\n";
|
||||
wrapper << " return cpp ? cpp->GetAnimationTime() : 0.0f;\n";
|
||||
wrapper << "}\n\n";
|
||||
wrapper << "int Modu_IsAnimationPlaying(ModuScriptContext* ctx) {\n";
|
||||
wrapper << " ScriptContext* cpp = ModuAsCpp(ctx);\n";
|
||||
wrapper << " return (cpp && cpp->IsAnimationPlaying()) ? 1 : 0;\n";
|
||||
wrapper << "}\n\n";
|
||||
wrapper << "int Modu_SetAnimationLoop(ModuScriptContext* ctx, int loop) {\n";
|
||||
wrapper << " ScriptContext* cpp = ModuAsCpp(ctx);\n";
|
||||
wrapper << " return (cpp && cpp->SetAnimationLoop(loop != 0)) ? 1 : 0;\n";
|
||||
wrapper << "}\n\n";
|
||||
wrapper << "int Modu_SetAnimationPlaySpeed(ModuScriptContext* ctx, float speed) {\n";
|
||||
wrapper << " ScriptContext* cpp = ModuAsCpp(ctx);\n";
|
||||
wrapper << " return (cpp && cpp->SetAnimationPlaySpeed(speed)) ? 1 : 0;\n";
|
||||
wrapper << "}\n\n";
|
||||
wrapper << "int Modu_SetAnimationPlayOnAwake(ModuScriptContext* ctx, int playOnAwake) {\n";
|
||||
wrapper << " ScriptContext* cpp = ModuAsCpp(ctx);\n";
|
||||
wrapper << " return (cpp && cpp->SetAnimationPlayOnAwake(playOnAwake != 0)) ? 1 : 0;\n";
|
||||
wrapper << "}\n\n";
|
||||
wrapper << "int Modu_IsSprintDown(ModuScriptContext* ctx) {\n";
|
||||
wrapper << " ScriptContext* cpp = ModuAsCpp(ctx);\n";
|
||||
wrapper << " return (cpp && cpp->IsSprintDown()) ? 1 : 0;\n";
|
||||
@@ -948,10 +1119,12 @@ bool ScriptCompiler::makeCommands(const ScriptBuildConfig& config, const fs::pat
|
||||
#else
|
||||
compileCmd << posixCompileDriver(false) << " -std=c11 -fPIC -O0 -g";
|
||||
appendPosixIncludesAndDefines(compileCmd);
|
||||
compileCmd << " -MMD -MF \"" << dependencyPath.string() << "\"";
|
||||
compileCmd << " -c \"" << scriptAbs.string() << "\" -o \"" << objectPath.string() << "\"";
|
||||
compileCmd << " && ";
|
||||
compileCmd << posixCompileDriver(true) << " -std=" << config.cppStandard << " -fPIC -O0 -g";
|
||||
appendPosixIncludesAndDefines(compileCmd);
|
||||
compileCmd << " -MMD -MF \"" << secondaryDependencyPath.string() << "\"";
|
||||
compileCmd << " -c \"" << wrapperPath.string() << "\" -o \"" << secondaryObjectPath.string() << "\"";
|
||||
linkCmd << "g++ -shared \"" << objectPath.string() << "\" \"" << secondaryObjectPath.string()
|
||||
<< "\" -o \"" << binaryPath.string() << "\"";
|
||||
@@ -1076,6 +1249,7 @@ bool ScriptCompiler::makeCommands(const ScriptBuildConfig& config, const fs::pat
|
||||
#else
|
||||
compileCmd << posixCompileDriver(true) << " -std=" << config.cppStandard << " -fPIC -O0 -g";
|
||||
appendPosixIncludesAndDefines(compileCmd);
|
||||
compileCmd << " -MMD -MF \"" << dependencyPath.string() << "\"";
|
||||
compileCmd << " -c \"" << sourceToCompile.string() << "\" -o \"" << objectPath.string() << "\"";
|
||||
linkCmd << "g++ -shared \"" << objectPath.string() << "\" -o \"" << binaryPath.string() << "\"";
|
||||
for (const auto& lib : config.linuxLinkLibs) {
|
||||
@@ -1108,6 +1282,8 @@ bool ScriptCompiler::makeCommands(const ScriptBuildConfig& config, const fs::pat
|
||||
outCommands.link = linkStr;
|
||||
outCommands.objectPath = objectPath;
|
||||
outCommands.secondaryObjectPath = secondaryObjectPath;
|
||||
outCommands.dependencyPath = dependencyPath;
|
||||
outCommands.secondaryDependencyPath = secondaryDependencyPath;
|
||||
outCommands.binaryPath = binaryPath;
|
||||
outCommands.wrapperPath = wrapperPath;
|
||||
outCommands.sourcePath = scriptAbs;
|
||||
@@ -1169,22 +1345,34 @@ bool ScriptCompiler::compile(const ScriptBuildCommands& commands, ScriptCompileO
|
||||
: std::optional<fs::file_time_type>{};
|
||||
const auto binaryTime = getFileWriteTime(commands.binaryPath);
|
||||
|
||||
std::optional<fs::file_time_type> newestInput = sourceTime;
|
||||
std::optional<fs::file_time_type> fallbackNewestInput = sourceTime;
|
||||
if (wrapperTime) {
|
||||
newestInput = newestInput ? std::max(*newestInput, *wrapperTime) : wrapperTime;
|
||||
fallbackNewestInput = fallbackNewestInput
|
||||
? std::max(*fallbackNewestInput, *wrapperTime)
|
||||
: wrapperTime;
|
||||
}
|
||||
|
||||
if (objectTime && newestInput) {
|
||||
needsCompile = (*objectTime < *newestInput);
|
||||
if (hasSecondaryObject) {
|
||||
if (!secondaryObjectTime) {
|
||||
needsCompile = true;
|
||||
} else {
|
||||
needsCompile = needsCompile || (*secondaryObjectTime < *newestInput);
|
||||
auto objectNeedsCompile = [&](const std::optional<fs::file_time_type>& builtTime,
|
||||
const fs::path& depPath) {
|
||||
if (!builtTime) return true;
|
||||
|
||||
std::optional<fs::file_time_type> newestInput = fallbackNewestInput;
|
||||
if (!depPath.empty()) {
|
||||
DependencyInfo depInfo = readDependencyInfo(depPath);
|
||||
if (!depInfo.hasDepFile || depInfo.missingDependency || !depInfo.newestInput) {
|
||||
return true;
|
||||
}
|
||||
newestInput = depInfo.newestInput;
|
||||
}
|
||||
} else {
|
||||
needsCompile = true;
|
||||
|
||||
if (!newestInput) return true;
|
||||
return *builtTime < *newestInput;
|
||||
};
|
||||
|
||||
needsCompile = objectNeedsCompile(objectTime, commands.dependencyPath);
|
||||
if (hasSecondaryObject) {
|
||||
needsCompile = needsCompile ||
|
||||
objectNeedsCompile(secondaryObjectTime, commands.secondaryDependencyPath);
|
||||
}
|
||||
|
||||
if (!needsCompile) {
|
||||
|
||||
@@ -17,6 +17,8 @@ struct ScriptBuildCommands {
|
||||
std::string link;
|
||||
fs::path objectPath;
|
||||
fs::path secondaryObjectPath;
|
||||
fs::path dependencyPath;
|
||||
fs::path secondaryDependencyPath;
|
||||
fs::path binaryPath;
|
||||
fs::path wrapperPath;
|
||||
fs::path sourcePath;
|
||||
|
||||
@@ -168,7 +168,7 @@ SceneObject* ScriptContext::ResolveObjectRef(const std::string& ref) {
|
||||
}
|
||||
|
||||
bool ScriptContext::IsObjectEnabled() const {
|
||||
return object ? object->enabled : false;
|
||||
return object ? IsObjectEnabledInHierarchy(*object) : false;
|
||||
}
|
||||
|
||||
void ScriptContext::SetObjectEnabled(bool enabled) {
|
||||
@@ -917,6 +917,66 @@ bool ScriptContext::SetAudioClip(const std::string& path) {
|
||||
return engine->setAudioClipFromScript(object->id, path);
|
||||
}
|
||||
|
||||
bool ScriptContext::PlayAudioOneShot(const std::string& clipPath, float volumeScale) {
|
||||
if (!engine || !object || !object->hasAudioSource) return false;
|
||||
return engine->playAudioOneShotFromScript(object->id, clipPath, std::max(0.0f, volumeScale));
|
||||
}
|
||||
|
||||
bool ScriptContext::HasAnimation() const {
|
||||
if (!engine || !object) return false;
|
||||
return engine->hasAnimationFromScript(object->id);
|
||||
}
|
||||
|
||||
bool ScriptContext::PlayAnimation(bool restart) {
|
||||
if (!engine || !object) return false;
|
||||
return engine->playAnimationFromScript(object->id, restart);
|
||||
}
|
||||
|
||||
bool ScriptContext::StopAnimation(bool resetTime) {
|
||||
if (!engine || !object) return false;
|
||||
return engine->stopAnimationFromScript(object->id, resetTime);
|
||||
}
|
||||
|
||||
bool ScriptContext::PauseAnimation(bool pause) {
|
||||
if (!engine || !object) return false;
|
||||
return engine->pauseAnimationFromScript(object->id, pause);
|
||||
}
|
||||
|
||||
bool ScriptContext::ReverseAnimation(bool restartIfStopped) {
|
||||
if (!engine || !object) return false;
|
||||
return engine->reverseAnimationFromScript(object->id, restartIfStopped);
|
||||
}
|
||||
|
||||
bool ScriptContext::SetAnimationTime(float timeSeconds) {
|
||||
if (!engine || !object) return false;
|
||||
return engine->setAnimationTimeFromScript(object->id, timeSeconds);
|
||||
}
|
||||
|
||||
float ScriptContext::GetAnimationTime() const {
|
||||
if (!engine || !object) return 0.0f;
|
||||
return engine->getAnimationTimeFromScript(object->id);
|
||||
}
|
||||
|
||||
bool ScriptContext::IsAnimationPlaying() const {
|
||||
if (!engine || !object) return false;
|
||||
return engine->isAnimationPlayingFromScript(object->id);
|
||||
}
|
||||
|
||||
bool ScriptContext::SetAnimationLoop(bool loop) {
|
||||
if (!engine || !object) return false;
|
||||
return engine->setAnimationLoopFromScript(object->id, loop);
|
||||
}
|
||||
|
||||
bool ScriptContext::SetAnimationPlaySpeed(float speed) {
|
||||
if (!engine || !object) return false;
|
||||
return engine->setAnimationPlaySpeedFromScript(object->id, speed);
|
||||
}
|
||||
|
||||
bool ScriptContext::SetAnimationPlayOnAwake(bool playOnAwake) {
|
||||
if (!engine || !object) return false;
|
||||
return engine->setAnimationPlayOnAwakeFromScript(object->id, playOnAwake);
|
||||
}
|
||||
|
||||
std::string ScriptContext::GetSetting(const std::string& key, const std::string& fallback) const {
|
||||
if (!script) return fallback;
|
||||
auto it = std::find_if(script->settings.begin(), script->settings.end(),
|
||||
|
||||
@@ -142,6 +142,19 @@ struct ScriptContext {
|
||||
bool SetAudioLoop(bool loop);
|
||||
bool SetAudioVolume(float volume);
|
||||
bool SetAudioClip(const std::string& path);
|
||||
bool PlayAudioOneShot(const std::string& clipPath = "", float volumeScale = 1.0f);
|
||||
// Animation helpers
|
||||
bool HasAnimation() const;
|
||||
bool PlayAnimation(bool restart = true);
|
||||
bool StopAnimation(bool resetTime = true);
|
||||
bool PauseAnimation(bool pause = true);
|
||||
bool ReverseAnimation(bool restartIfStopped = true);
|
||||
bool SetAnimationTime(float timeSeconds);
|
||||
float GetAnimationTime() const;
|
||||
bool IsAnimationPlaying() const;
|
||||
bool SetAnimationLoop(bool loop);
|
||||
bool SetAnimationPlaySpeed(float speed);
|
||||
bool SetAnimationPlayOnAwake(bool playOnAwake);
|
||||
// Settings helpers (auto-mark dirty)
|
||||
std::string GetSetting(const std::string& key, const std::string& fallback = "") const;
|
||||
void SetSetting(const std::string& key, const std::string& value);
|
||||
|
||||
@@ -1805,7 +1805,7 @@ void VulkanRenderer::setSceneDataForTarget(SceneCameraData& targetCamera,
|
||||
std::vector<OrderedInstance> ordered;
|
||||
ordered.reserve(sceneObjects.size());
|
||||
for (const SceneObject& obj : sceneObjects) {
|
||||
if (!obj.enabled || !obj.hasRenderer || obj.renderType == RenderType::None) {
|
||||
if (!IsObjectEnabledInHierarchy(obj) || !obj.hasRenderer || obj.renderType == RenderType::None) {
|
||||
continue;
|
||||
}
|
||||
if (obj.renderType == RenderType::Sprite) {
|
||||
@@ -1873,7 +1873,7 @@ void VulkanRenderer::setSceneDataForTarget(SceneCameraData& targetCamera,
|
||||
targetLights.clear();
|
||||
targetLights.reserve(std::min<size_t>(sceneObjects.size(), kMaxSceneLights));
|
||||
for (const SceneObject& obj : sceneObjects) {
|
||||
if (!obj.enabled || !obj.hasLight || !obj.light.enabled) {
|
||||
if (!IsObjectEnabledInHierarchy(obj) || !obj.hasLight || !obj.light.enabled) {
|
||||
continue;
|
||||
}
|
||||
if (obj.light.intensity <= 0.0f) {
|
||||
|
||||
@@ -9,6 +9,8 @@ int width, height, channels;
|
||||
namespace
|
||||
{
|
||||
constexpr int kAnyProfile = 0;
|
||||
constexpr int kDefaultWindowWidth = 1000;
|
||||
constexpr int kDefaultWindowHeight = 800;
|
||||
|
||||
void glfwErrorCallback(int code, const char* description)
|
||||
{
|
||||
@@ -17,26 +19,66 @@ namespace
|
||||
<< "\n";
|
||||
}
|
||||
|
||||
void centerWindowOnPrimaryMonitor(GLFWwindow* window)
|
||||
{
|
||||
if (!window) return;
|
||||
#if defined(__linux__)
|
||||
// Wayland does not allow clients to set absolute window positions.
|
||||
if (glfwGetPlatform() == GLFW_PLATFORM_WAYLAND) {
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
GLFWmonitor* monitor = glfwGetPrimaryMonitor();
|
||||
if (!monitor) return;
|
||||
|
||||
int workX = 0;
|
||||
int workY = 0;
|
||||
int workW = 0;
|
||||
int workH = 0;
|
||||
glfwGetMonitorWorkarea(monitor, &workX, &workY, &workW, &workH);
|
||||
if (workW <= 0 || workH <= 0) {
|
||||
const GLFWvidmode* mode = glfwGetVideoMode(monitor);
|
||||
if (!mode) return;
|
||||
workW = mode->width;
|
||||
workH = mode->height;
|
||||
workX = 0;
|
||||
workY = 0;
|
||||
}
|
||||
|
||||
int windowW = 0;
|
||||
int windowH = 0;
|
||||
glfwGetWindowSize(window, &windowW, &windowH);
|
||||
if (windowW <= 0) windowW = kDefaultWindowWidth;
|
||||
if (windowH <= 0) windowH = kDefaultWindowHeight;
|
||||
|
||||
const int centeredX = workX + (workW - windowW) / 2;
|
||||
const int centeredY = workY + (workH - windowH) / 2;
|
||||
glfwSetWindowPos(window, centeredX, centeredY);
|
||||
}
|
||||
|
||||
GLFWwindow* tryCreateWindow(int major, int minor, int profile)
|
||||
{
|
||||
glfwDefaultWindowHints();
|
||||
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, major);
|
||||
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, minor);
|
||||
glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE);
|
||||
|
||||
if (profile != kAnyProfile)
|
||||
{
|
||||
glfwWindowHint(GLFW_OPENGL_PROFILE, profile);
|
||||
}
|
||||
|
||||
return glfwCreateWindow(1000, 800, "Modularity", nullptr, nullptr);
|
||||
return glfwCreateWindow(kDefaultWindowWidth, kDefaultWindowHeight, "Modularity", nullptr, nullptr);
|
||||
}
|
||||
|
||||
GLFWwindow* tryCreateVulkanWindow()
|
||||
{
|
||||
glfwDefaultWindowHints();
|
||||
glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
|
||||
glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE);
|
||||
|
||||
return glfwCreateWindow(1000, 800, "Modularity", nullptr, nullptr);
|
||||
return glfwCreateWindow(kDefaultWindowWidth, kDefaultWindowHeight, "Modularity", nullptr, nullptr);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -146,5 +188,8 @@ GLFWwindow* Window::makeWindow(Modularity::GraphicsBackend backend)
|
||||
stbi_image_free(pixels);
|
||||
}
|
||||
|
||||
centerWindowOnPrimaryMonitor(window);
|
||||
glfwShowWindow(window);
|
||||
|
||||
return window;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user