|
|
|
@@ -0,0 +1,327 @@
|
|
|
|
|
#include "pch.h"
|
|
|
|
|
|
|
|
|
|
#include "F4TProxyActorController.h"
|
|
|
|
|
|
|
|
|
|
#include "F4TNetworking.h"
|
|
|
|
|
|
|
|
|
|
#include <format>
|
|
|
|
|
#include <string>
|
|
|
|
|
#include <string_view>
|
|
|
|
|
#include <unordered_map>
|
|
|
|
|
|
|
|
|
|
namespace
|
|
|
|
|
{
|
|
|
|
|
constexpr auto kTestCellEditorId = "F4TTestCell01";
|
|
|
|
|
constexpr auto kProxyEditorId = "F4TProxyRemotePlayer01REF";
|
|
|
|
|
constexpr auto kProxyBaseActorEditorId = "F4T_RemotePlayerProxy";
|
|
|
|
|
constexpr auto kTestPluginName = "Fallout4Together_Test.esp";
|
|
|
|
|
constexpr RE::TESFormID kProxyFallbackLocalFormId = 0x0020A2;
|
|
|
|
|
constexpr auto kProxyOffsetX = 150.0F;
|
|
|
|
|
constexpr auto kMovementInterval = 200ms;
|
|
|
|
|
constexpr auto kWarningLogInterval = 5s;
|
|
|
|
|
constexpr auto kNotInTestCellLogInterval = 30s;
|
|
|
|
|
|
|
|
|
|
enum class ProxyLookupResult
|
|
|
|
|
{
|
|
|
|
|
kReferenceEditorId,
|
|
|
|
|
kBaseActorEditorId,
|
|
|
|
|
kFallbackFormId
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
RE::ObjectRefHandle g_proxyHandle;
|
|
|
|
|
bool g_proxyResolvedLogged = false;
|
|
|
|
|
bool g_proxyMovedLogged = false;
|
|
|
|
|
auto g_lastMovementTime = std::chrono::steady_clock::time_point{};
|
|
|
|
|
std::unordered_map<std::string, std::chrono::steady_clock::time_point> g_lastWarningLogTimes;
|
|
|
|
|
|
|
|
|
|
std::string GetLocalPlayerLogMessage(std::string_view a_message)
|
|
|
|
|
{
|
|
|
|
|
return F4T::Networking::GetLocalPlayerLogPrefix() + " " + std::string(a_message);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void LogInfoWithLocalPlayerPrefix(std::string_view a_message)
|
|
|
|
|
{
|
|
|
|
|
const auto message = GetLocalPlayerLogMessage(a_message);
|
|
|
|
|
REX::INFO(std::string_view{ message });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void LogWarningWithLocalPlayerPrefix(std::string_view a_message)
|
|
|
|
|
{
|
|
|
|
|
const auto message = GetLocalPlayerLogMessage(a_message);
|
|
|
|
|
REX::WARN(std::string_view{ message });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool ShouldLog(std::string a_key, std::chrono::steady_clock::duration a_interval)
|
|
|
|
|
{
|
|
|
|
|
const auto now = std::chrono::steady_clock::now();
|
|
|
|
|
const auto lastLog = g_lastWarningLogTimes.find(a_key);
|
|
|
|
|
if (lastLog != g_lastWarningLogTimes.end() && now - lastLog->second < a_interval) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
g_lastWarningLogTimes[std::move(a_key)] = now;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void LogThrottledWarning(
|
|
|
|
|
const std::string& a_key,
|
|
|
|
|
std::string_view a_message,
|
|
|
|
|
std::chrono::steady_clock::duration a_interval = kWarningLogInterval)
|
|
|
|
|
{
|
|
|
|
|
if (ShouldLog(a_key, a_interval)) {
|
|
|
|
|
LogWarningWithLocalPlayerPrefix(a_message);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void LogThrottledInfo(
|
|
|
|
|
const std::string& a_key,
|
|
|
|
|
std::string_view a_message,
|
|
|
|
|
std::chrono::steady_clock::duration a_interval = kWarningLogInterval)
|
|
|
|
|
{
|
|
|
|
|
if (ShouldLog(a_key, a_interval)) {
|
|
|
|
|
LogInfoWithLocalPlayerPrefix(a_message);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool HasEditorId(const RE::TESForm& a_form, std::string_view a_editorId)
|
|
|
|
|
{
|
|
|
|
|
const auto* editorId = a_form.GetFormEditorID();
|
|
|
|
|
return editorId && a_editorId == editorId;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool IsTestCell(const RE::TESObjectCELL& a_cell)
|
|
|
|
|
{
|
|
|
|
|
const auto* testCell = RE::TESForm::GetFormByEditorID<RE::TESObjectCELL>(RE::BSFixedString(kTestCellEditorId));
|
|
|
|
|
if (testCell) {
|
|
|
|
|
return testCell == std::addressof(a_cell) || testCell->GetFormID() == a_cell.GetFormID();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (HasEditorId(a_cell, kTestCellEditorId)) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
LogThrottledWarning(
|
|
|
|
|
"test_cell_unresolved",
|
|
|
|
|
"Proxy actor safety test could not resolve F4TTestCell01; actor movement is disabled.");
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool IsValidProxyActor(const RE::Actor& a_proxy, const RE::TESObjectCELL& a_expectedCell, const RE::PlayerCharacter& a_player)
|
|
|
|
|
{
|
|
|
|
|
return std::addressof(a_proxy) != std::addressof(a_player) && a_proxy.GetParentCell() == std::addressof(a_expectedCell);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool HasProxyBaseActorEditorId(const RE::Actor& a_actor)
|
|
|
|
|
{
|
|
|
|
|
const auto* baseActor = a_actor.GetNPC();
|
|
|
|
|
return baseActor && HasEditorId(*baseActor, kProxyBaseActorEditorId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
RE::Actor* TryGetCachedProxy(const RE::TESObjectCELL& a_currentCell, const RE::PlayerCharacter& a_player)
|
|
|
|
|
{
|
|
|
|
|
if (!g_proxyHandle) {
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const auto proxyRef = g_proxyHandle.get();
|
|
|
|
|
auto* proxy = proxyRef ? proxyRef->As<RE::Actor>() : nullptr;
|
|
|
|
|
if (proxy && IsValidProxyActor(*proxy, a_currentCell, a_player)) {
|
|
|
|
|
return proxy;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
g_proxyHandle.reset();
|
|
|
|
|
g_proxyResolvedLogged = false;
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
RE::Actor* TryLookupProxyByEditorId(
|
|
|
|
|
const RE::TESObjectCELL& a_currentCell,
|
|
|
|
|
const RE::PlayerCharacter& a_player,
|
|
|
|
|
ProxyLookupResult& a_lookupResult)
|
|
|
|
|
{
|
|
|
|
|
auto* proxy = RE::TESForm::GetFormByEditorID<RE::Actor>(RE::BSFixedString(kProxyEditorId));
|
|
|
|
|
if (!proxy) {
|
|
|
|
|
auto* proxyRef = RE::TESForm::GetFormByEditorID<RE::TESObjectREFR>(RE::BSFixedString(kProxyEditorId));
|
|
|
|
|
proxy = proxyRef ? proxyRef->As<RE::Actor>() : nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!proxy || !IsValidProxyActor(*proxy, a_currentCell, a_player)) {
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
a_lookupResult = ProxyLookupResult::kReferenceEditorId;
|
|
|
|
|
return proxy;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
RE::Actor* TryFindProxyInCurrentCell(
|
|
|
|
|
RE::TESObjectCELL& a_currentCell,
|
|
|
|
|
const RE::PlayerCharacter& a_player,
|
|
|
|
|
ProxyLookupResult& a_lookupResult)
|
|
|
|
|
{
|
|
|
|
|
RE::Actor* proxy = nullptr;
|
|
|
|
|
|
|
|
|
|
a_currentCell.ForEachReference([&](RE::TESObjectREFR* a_ref) {
|
|
|
|
|
if (!a_ref) {
|
|
|
|
|
return RE::BSContainer::ForEachResult::kContinue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto* actor = a_ref->As<RE::Actor>();
|
|
|
|
|
if (!actor || !IsValidProxyActor(*actor, a_currentCell, a_player)) {
|
|
|
|
|
return RE::BSContainer::ForEachResult::kContinue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (HasEditorId(*a_ref, kProxyEditorId)) {
|
|
|
|
|
proxy = actor;
|
|
|
|
|
a_lookupResult = ProxyLookupResult::kReferenceEditorId;
|
|
|
|
|
return RE::BSContainer::ForEachResult::kStop;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!HasProxyBaseActorEditorId(*actor)) {
|
|
|
|
|
return RE::BSContainer::ForEachResult::kContinue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
LogThrottledInfo("proxy_base_actor_found", std::format(
|
|
|
|
|
"Found proxy actor by base actor editor ID: {}",
|
|
|
|
|
kProxyBaseActorEditorId));
|
|
|
|
|
proxy = actor;
|
|
|
|
|
a_lookupResult = ProxyLookupResult::kBaseActorEditorId;
|
|
|
|
|
return RE::BSContainer::ForEachResult::kStop;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return proxy;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
RE::Actor* TryLookupProxyByFallbackFormId(
|
|
|
|
|
const RE::TESObjectCELL& a_currentCell,
|
|
|
|
|
const RE::PlayerCharacter& a_player,
|
|
|
|
|
ProxyLookupResult& a_lookupResult)
|
|
|
|
|
{
|
|
|
|
|
auto* dataHandler = RE::TESDataHandler::GetSingleton();
|
|
|
|
|
if (!dataHandler) {
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Temporary safety-test fallback: this local form ID comes from the current
|
|
|
|
|
// test ESP and may change when Fallout4Together_Test.esp is edited.
|
|
|
|
|
auto* proxy = dataHandler->LookupForm<RE::Actor>(kProxyFallbackLocalFormId, kTestPluginName);
|
|
|
|
|
if (!proxy || !IsValidProxyActor(*proxy, a_currentCell, a_player)) {
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
a_lookupResult = ProxyLookupResult::kFallbackFormId;
|
|
|
|
|
return proxy;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const char* GetLookupResultDescription(ProxyLookupResult a_lookupResult)
|
|
|
|
|
{
|
|
|
|
|
switch (a_lookupResult) {
|
|
|
|
|
case ProxyLookupResult::kReferenceEditorId:
|
|
|
|
|
return "placed reference editor ID";
|
|
|
|
|
case ProxyLookupResult::kBaseActorEditorId:
|
|
|
|
|
return "base actor editor ID";
|
|
|
|
|
case ProxyLookupResult::kFallbackFormId:
|
|
|
|
|
return "fallback ESP-local form ID";
|
|
|
|
|
default:
|
|
|
|
|
return "unknown lookup path";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
RE::Actor* ResolveProxy(RE::TESObjectCELL& a_currentCell, const RE::PlayerCharacter& a_player)
|
|
|
|
|
{
|
|
|
|
|
if (auto* cachedProxy = TryGetCachedProxy(a_currentCell, a_player)) {
|
|
|
|
|
return cachedProxy;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto lookupResult = ProxyLookupResult::kReferenceEditorId;
|
|
|
|
|
auto* proxy = TryLookupProxyByEditorId(a_currentCell, a_player, lookupResult);
|
|
|
|
|
if (!proxy) {
|
|
|
|
|
proxy = TryFindProxyInCurrentCell(a_currentCell, a_player, lookupResult);
|
|
|
|
|
}
|
|
|
|
|
if (!proxy) {
|
|
|
|
|
proxy = TryLookupProxyByFallbackFormId(a_currentCell, a_player, lookupResult);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!proxy) {
|
|
|
|
|
LogThrottledWarning(
|
|
|
|
|
"proxy_missing",
|
|
|
|
|
"Proxy actor safety test could not find F4TProxyRemotePlayer01REF in F4TTestCell01.");
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
g_proxyHandle = proxy->GetHandle();
|
|
|
|
|
if (!g_proxyResolvedLogged) {
|
|
|
|
|
LogInfoWithLocalPlayerPrefix(std::format(
|
|
|
|
|
"Proxy actor safety test resolved {} by {} as form {:08X}.",
|
|
|
|
|
kProxyEditorId,
|
|
|
|
|
GetLookupResultDescription(lookupResult),
|
|
|
|
|
proxy->GetFormID()));
|
|
|
|
|
g_proxyResolvedLogged = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return proxy;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool ShouldMoveProxy()
|
|
|
|
|
{
|
|
|
|
|
const auto now = std::chrono::steady_clock::now();
|
|
|
|
|
if (g_lastMovementTime.time_since_epoch().count() != 0 && now - g_lastMovementTime < kMovementInterval) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
g_lastMovementTime = now;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void MoveProxyToPlayerOffset(RE::Actor& a_proxy, const RE::PlayerCharacter& a_player)
|
|
|
|
|
{
|
|
|
|
|
auto targetPosition = a_player.GetPosition();
|
|
|
|
|
targetPosition.x += kProxyOffsetX;
|
|
|
|
|
|
|
|
|
|
a_proxy.SetPosition(targetPosition, true);
|
|
|
|
|
a_proxy.SetHeading(a_player.data.angle.z);
|
|
|
|
|
|
|
|
|
|
if (!g_proxyMovedLogged) {
|
|
|
|
|
LogInfoWithLocalPlayerPrefix(std::format(
|
|
|
|
|
"Proxy actor safety test moved {} to player position plus X offset {:.2f}.",
|
|
|
|
|
kProxyEditorId,
|
|
|
|
|
kProxyOffsetX));
|
|
|
|
|
g_proxyMovedLogged = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
namespace F4T::ProxyActorController
|
|
|
|
|
{
|
|
|
|
|
void UpdateSafetyTest()
|
|
|
|
|
{
|
|
|
|
|
// This temporary safety test intentionally ignores remote-player network state.
|
|
|
|
|
// Later visual-sync milestones can replace the local-player offset with a
|
|
|
|
|
// game-thread snapshot of remote transforms.
|
|
|
|
|
auto* player = RE::PlayerCharacter::GetSingleton();
|
|
|
|
|
if (!player) {
|
|
|
|
|
LogThrottledWarning("player_missing", "Proxy actor safety test skipped: local player is not available yet.");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto* parentCell = player->GetParentCell();
|
|
|
|
|
if (!parentCell) {
|
|
|
|
|
LogThrottledWarning("cell_missing", "Proxy actor safety test skipped: local player parent cell is unavailable.");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!IsTestCell(*parentCell)) {
|
|
|
|
|
LogThrottledWarning(
|
|
|
|
|
"not_in_test_cell",
|
|
|
|
|
"Proxy actor safety test skipped: player is not in F4TTestCell01.",
|
|
|
|
|
kNotInTestCellLogInterval);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto* proxy = ResolveProxy(*parentCell, *player);
|
|
|
|
|
if (!proxy || !ShouldMoveProxy()) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
MoveProxyToPlayerOffset(*proxy, *player);
|
|
|
|
|
}
|
|
|
|
|
}
|