feat: implement proxy pool architecture using pre-placed proxies
ARCHITECTURE SHIFT: Instead of dynamically spawning proxies (which lack proper AI), use a pre-placed proxy pool from the Creation Kit. BENEFITS: - ✅ All proxies have full AI, character controllers, and animations - ✅ Pre-placed = guaranteed proper setup and initialization - ✅ Simpler architecture (no dynamic spawning complexity) - ✅ Predictable player limits (# of placed proxies = max players) IMPLEMENTATION: - Added InitializeProxyPool() to discover pre-placed proxies in cell - Added TryGetProxyFromPool() to get available proxies - Logs number of available proxies and max concurrent players - Falls back gracefully if no proxies found (warns user) USAGE: 1. Create proxy actor references in Creation Kit 2. Place them in the game cell 3. Ensure they have AI packages (for animations to work) 4. Plugin finds and manages them automatically LIMITATIONS: - Max concurrent players = number of pre-placed proxies - Users must place proxies in CK (documented requirement) This is a pragmatic solution for testing and can remain for release. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -46,10 +46,15 @@ namespace
|
||||
constexpr bool kDebugUsePlaceAtMeCandidateIsolation = true;
|
||||
constexpr float kDebugPlaceAtMePostSpawnSettleSeconds = 2.0F;
|
||||
constexpr float kDebugPlaceAtMeCandidateSearchSeconds = 5.0F;
|
||||
// CRITICAL FIX: PlaceAtMe spawned actors don't have proper AIProcess!
|
||||
// Use pre-placed proxy references from CK instead (they have full AI).
|
||||
// This is why the CK-placed proxy has animations but spawned ones don't.
|
||||
constexpr bool kUsePlaceAtMeBackedRuntimeProxySpawn = false;
|
||||
// PROXY POOL ARCHITECTURE
|
||||
// Instead of spawning proxies dynamically, we use pre-placed proxy references from the Creation Kit.
|
||||
// This ensures proxies have proper AI, character controllers, and animation systems.
|
||||
// Max players = number of pre-placed proxies in the world.
|
||||
constexpr bool kUsePlaceAtMeBackedRuntimeProxySpawn = false; // Use pre-placed proxies
|
||||
|
||||
// Pre-placed proxies we've discovered and can reuse
|
||||
static std::vector<RE::ObjectRefHandle> g_availableProxyPool;
|
||||
static bool g_proxyPoolInitialized = false;
|
||||
constexpr bool kDebugPlaceAtMeSpawnUseCustomProxyBase = true;
|
||||
constexpr bool kDebugPlaceAtMeSpawnFallbackToCodsworth = true;
|
||||
constexpr float kPlaceAtMeProxySettleSeconds = 2.0F;
|
||||
@@ -597,9 +602,6 @@ namespace
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the proxy is already set up for movement
|
||||
auto* process = a_proxy.currentProcess;
|
||||
|
||||
// Key insight: We want the proxy to think it's following the player
|
||||
// But we DON'T want actual pathfinding (Creation Engine path bugs)
|
||||
// Solution: The proxy will idle, and we'll move it via our network updates
|
||||
@@ -609,7 +611,7 @@ namespace
|
||||
LogInfoWithLocalPlayerPrefix(std::format(
|
||||
"Proxy {:08X} has active AIProcess; AI animation mode is available.",
|
||||
a_proxy.GetFormID()));
|
||||
}
|
||||
}
|
||||
|
||||
// Diagnostic: Log proxy's AI state
|
||||
void DiagnosticProxyAIState(
|
||||
@@ -2125,8 +2127,71 @@ namespace
|
||||
return true;
|
||||
}
|
||||
|
||||
bool IsConsolePlaceAtMeCandidateActor(const RE::Actor& a_actor, const RE::PlayerCharacter& a_player)
|
||||
void InitializeProxyPool(RE::TESObjectCELL& a_cell, const RE::PlayerCharacter& a_player)
|
||||
{
|
||||
if (g_proxyPoolInitialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
g_availableProxyPool.clear();
|
||||
|
||||
// Search current cell for pre-placed proxy actors
|
||||
a_cell.ForEachReference([&](RE::TESObjectREFR* a_ref) {
|
||||
if (!a_ref) {
|
||||
return RE::BSContainer::ForEachResult::kContinue;
|
||||
}
|
||||
|
||||
auto* actor = a_ref->As<RE::Actor>();
|
||||
if (!actor || actor == &a_player) {
|
||||
return RE::BSContainer::ForEachResult::kContinue;
|
||||
}
|
||||
|
||||
// Check if this looks like a remote player proxy
|
||||
// (actor with AI in the cell)
|
||||
if (actor->currentProcess) {
|
||||
auto handle = actor->GetHandle();
|
||||
g_availableProxyPool.push_back(handle);
|
||||
LogInfoWithLocalPlayerPrefix(std::format(
|
||||
"Proxy pool: Found pre-placed proxy {:08X} (base: {:08X}), total proxies: {}",
|
||||
actor->GetFormID(),
|
||||
actor->GetNPC() ? actor->GetNPC()->GetFormID() : 0,
|
||||
g_availableProxyPool.size()));
|
||||
}
|
||||
|
||||
return RE::BSContainer::ForEachResult::kContinue;
|
||||
});
|
||||
|
||||
g_proxyPoolInitialized = true;
|
||||
|
||||
if (g_availableProxyPool.empty()) {
|
||||
LogWarningWithLocalPlayerPrefix(
|
||||
"Proxy pool initialized but NO pre-placed proxies found in cell! "
|
||||
"Place proxy actors in the Creation Kit and ensure they have AI packages.");
|
||||
} else {
|
||||
LogInfoWithLocalPlayerPrefix(std::format(
|
||||
"Proxy pool ready: {} pre-placed proxies available. Max concurrent players: {}",
|
||||
g_availableProxyPool.size(),
|
||||
g_availableProxyPool.size()));
|
||||
}
|
||||
}
|
||||
|
||||
RE::Actor* TryGetProxyFromPool()
|
||||
{
|
||||
for (auto& handle : g_availableProxyPool) {
|
||||
if (auto ptr = handle.get()) {
|
||||
if (auto actor = ptr.get()->As<RE::Actor>()) {
|
||||
// Proxy is available (not currently in use)
|
||||
if (!actor->IsVisible() || actor->GetHandle() == RE::ObjectRefHandle()) {
|
||||
return actor;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nullptr; // No available proxies
|
||||
}
|
||||
|
||||
bool IsConsolePlaceAtMeCandidateActor(const RE::Actor& a_actor, const RE::PlayerCharacter& a_player)
|
||||
{
|
||||
if (std::addressof(a_actor) == std::addressof(a_player)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user