Add data-only proxy animation debug logging
Track and log per-slot animation/movement diagnostics for runtime proxy actors without applying any visual animation. Adds a ProxyAnimationSpeedBucket enum and GetProxyAnimationSpeedBucket/GetProxyAnimationSpeedBucketLabel helpers, a new UpdateProxyAnimationStateDebug() routine, and fields on ProxyActorSlot to remember last-observed moving, sprinting, sneaking, jumping, weapon-drawn, movement type, and coarse speed-bucket state. Resets observation state when slots are created/reused and emits initial/transition-only logs including playerId and proxy FormID. Documentation and dev-log entries updated; visual animation, animation graph changes, and AI/movement behavior remain intentionally unmodified.
This commit is contained in:
+17
-3
@@ -32,7 +32,7 @@ both send and receive paths:
|
|||||||
12. For each eligible remote `playerId`, the controller maintains a
|
12. For each eligible remote `playerId`, the controller maintains a
|
||||||
`remotePlayerId -> runtime proxy slot` mapping. Each slot stores its
|
`remotePlayerId -> runtime proxy slot` mapping. Each slot stores its
|
||||||
`ObjectRefHandle`, lifecycle state, PlaceAtMe-backed spawn/candidate state,
|
`ObjectRefHandle`, lifecycle state, PlaceAtMe-backed spawn/candidate state,
|
||||||
movement smoothing state, and sneak observation state. Stage 4 caps runtime
|
movement smoothing state, and data-only animation debug state. Stage 4 caps runtime
|
||||||
proxy slots at four actors for testing.
|
proxy slots at four actors for testing.
|
||||||
13. Runtime proxies are demand-driven. A slot executes the Stage 3.7
|
13. Runtime proxies are demand-driven. A slot executes the Stage 3.7
|
||||||
PlaceAtMe-backed spawn flow only when that remote player needs a same-cell
|
PlaceAtMe-backed spawn flow only when that remote player needs a same-cell
|
||||||
@@ -53,8 +53,9 @@ both send and receive paths:
|
|||||||
actor.
|
actor.
|
||||||
15. Proxy visual updates are independent per slot. Normal movement is smoothed
|
15. Proxy visual updates are independent per slot. Normal movement is smoothed
|
||||||
toward that player’s latest target, while `cell_change`, `worldspace_change`,
|
toward that player’s latest target, while `cell_change`, `worldspace_change`,
|
||||||
and `teleport` snap directly. Sneak state is still observed and logged only;
|
and `teleport` snap directly. Movement and animation-related remote state is
|
||||||
no visual crouch/sneak application is attempted.
|
still observed and logged only; no visual crouch/sneak, jump, weapon drawn,
|
||||||
|
locomotion, animation graph, or actor-state application is attempted.
|
||||||
16. When a remote player leaves the cell or disconnects, only that player’s
|
16. When a remote player leaves the cell or disconnects, only that player’s
|
||||||
runtime proxy is moved to the hidden holding position inside
|
runtime proxy is moved to the hidden holding position inside
|
||||||
`F4TTestCell01`. Runtime actors are not disabled or deleted. Connected
|
`F4TTestCell01`. Runtime actors are not disabled or deleted. Connected
|
||||||
@@ -118,6 +119,15 @@ reused.
|
|||||||
The placed proxy reference remains as a single fallback path if runtime spawning
|
The placed proxy reference remains as a single fallback path if runtime spawning
|
||||||
fails for one selected remote player.
|
fails for one selected remote player.
|
||||||
|
|
||||||
|
Current animation-state handling is data/debug only:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
RemotePlayerState[RemotePlayerState] --> ProxyActorSlot[ProxyActorSlot animation debug state]
|
||||||
|
ProxyActorSlot --> Logs[Initial and transition logs]
|
||||||
|
ProxyActorSlot --> FutureVisualAnimation[Future visual animation application]
|
||||||
|
```
|
||||||
|
|
||||||
## Main Components
|
## Main Components
|
||||||
|
|
||||||
### F4SE Plugin
|
### F4SE Plugin
|
||||||
@@ -153,6 +163,9 @@ The native plugin is responsible for:
|
|||||||
path after spawn promotion, during reusable-slot reassignment, periodically
|
path after spawn promotion, during reusable-slot reassignment, periodically
|
||||||
while active, and while held so they remain passive visual puppets rather than
|
while active, and while held so they remain passive visual puppets rather than
|
||||||
independent combat actors
|
independent combat actors
|
||||||
|
- Logging initial and transition-only data/debug animation state per resolved
|
||||||
|
runtime proxy slot from `RemotePlayerState`, including moving, speed bucket,
|
||||||
|
sprinting, sneaking, jumping, weapon drawn, and movement type
|
||||||
- Suppressing exposed AI movement intent for runtime proxies by clearing safe
|
- Suppressing exposed AI movement intent for runtime proxies by clearing safe
|
||||||
process target handles and using validated package interruption/do-nothing
|
process target handles and using validated package interruption/do-nothing
|
||||||
calls when combat, pathing, flee/alarm, bump, or package movement state is
|
calls when combat, pathing, flee/alarm, bump, or package movement state is
|
||||||
@@ -237,6 +250,7 @@ Game update attempts at most one PlaceAtMe-backed runtime actor spawn at a time
|
|||||||
Game update snapshots matching candidates, isolates the new actor, and waits for the settle window
|
Game update snapshots matching candidates, isolates the new actor, and waits for the settle window
|
||||||
Game update moves isolated Stage 4 runtime spawn candidates to the remote player's current transform
|
Game update moves isolated Stage 4 runtime spawn candidates to the remote player's current transform
|
||||||
Game update stores promoted runtime actors as ObjectRefHandle values in slots
|
Game update stores promoted runtime actors as ObjectRefHandle values in slots
|
||||||
|
Game update logs data-only animation debug state for resolved runtime proxy slots
|
||||||
Game update neutralizes resolved runtime proxy actors after spawn promotion, reuse, active throttled checks, and holding
|
Game update neutralizes resolved runtime proxy actors after spawn promotion, reuse, active throttled checks, and holding
|
||||||
Game update suppresses exposed AI movement/pathing intent without forcing animation graph events
|
Game update suppresses exposed AI movement/pathing intent without forcing animation graph events
|
||||||
Game update moves each valid proxy from its assigned remote player state
|
Game update moves each valid proxy from its assigned remote player state
|
||||||
|
|||||||
@@ -2257,6 +2257,63 @@ starting near the local player before snapping away.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 2026-06-03 - Proxy Animation State Debug Pass
|
||||||
|
|
||||||
|
### Summary
|
||||||
|
|
||||||
|
Added data-only per-slot animation state diagnostics for runtime proxy actors so
|
||||||
|
the proxy controller can observe movement and animation-related state assigned to
|
||||||
|
each remote `playerId` without visually applying animation.
|
||||||
|
|
||||||
|
### Files Changed
|
||||||
|
|
||||||
|
- `plugin/src/F4TProxyActorController.cpp`
|
||||||
|
- `docs/architecture.md`
|
||||||
|
- `docs/dev-log.md`
|
||||||
|
|
||||||
|
### Details
|
||||||
|
|
||||||
|
- Added per-runtime-proxy-slot debug tracking for moving, speed bucket,
|
||||||
|
sprinting, sneaking, jumping, weapon drawn, and movement type.
|
||||||
|
- Added transition-only runtime proxy animation state logs that include the
|
||||||
|
remote `playerId` and resolved proxy actor FormID.
|
||||||
|
- Movement speed diagnostics use coarse buckets (`idle`, `slow`, `walk`, `run`,
|
||||||
|
`sprint`) so small float changes do not spam the log.
|
||||||
|
- Reset animation debug state when slots are created, reassigned, held, or pass
|
||||||
|
through the existing slot observation reset helper.
|
||||||
|
- Kept this pass data/debug only. It does not apply visual animation, force
|
||||||
|
animation graph events, write animation graph variables, call `PerformAction`,
|
||||||
|
write `ActorState`, or add package/pathing/AI movement changes.
|
||||||
|
- Networking, protocol, Python server code, fake-client tooling, placed fallback
|
||||||
|
behavior, spawn-at-remote-position, movement smoothing, snap behavior,
|
||||||
|
same-cell gating, held/reusable lifecycle, and combat/AI neutralization were
|
||||||
|
unchanged.
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
- Ran `xmake build` from `plugin`; it succeeded and rebuilt
|
||||||
|
`Fallout4Together.dll`.
|
||||||
|
- Pending manual validation: launch through F4SE, start the server, add fake
|
||||||
|
clients in `F4TTestCell01`, and confirm per-player initial and transition
|
||||||
|
animation state logs without visual animation changes.
|
||||||
|
|
||||||
|
### Known Issues
|
||||||
|
|
||||||
|
- Speed bucket thresholds are conservative diagnostics and still need tuning
|
||||||
|
against real Fallout 4 movement units before any future visual locomotion work.
|
||||||
|
- Visual animation application remains intentionally unimplemented.
|
||||||
|
|
||||||
|
### Next Steps
|
||||||
|
|
||||||
|
- Investigate safe visual sneak/crouch application.
|
||||||
|
- Investigate weapon drawn visual application.
|
||||||
|
- Investigate locomotion animation graph behavior.
|
||||||
|
- Investigate jump animation behavior.
|
||||||
|
- Research safe animation graph variable/event reads or writes before any visual
|
||||||
|
animation milestone.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Entry Template
|
## Entry Template
|
||||||
|
|
||||||
Use this format for future updates:
|
Use this format for future updates:
|
||||||
|
|||||||
@@ -141,6 +141,15 @@ namespace
|
|||||||
kHolding
|
kHolding
|
||||||
};
|
};
|
||||||
|
|
||||||
|
enum class ProxyAnimationSpeedBucket
|
||||||
|
{
|
||||||
|
kIdle,
|
||||||
|
kSlow,
|
||||||
|
kWalk,
|
||||||
|
kRun,
|
||||||
|
kSprint
|
||||||
|
};
|
||||||
|
|
||||||
struct RemotePlayerSelection
|
struct RemotePlayerSelection
|
||||||
{
|
{
|
||||||
std::optional<RemotePlayer> selectedPlayer;
|
std::optional<RemotePlayer> selectedPlayer;
|
||||||
@@ -218,7 +227,14 @@ namespace
|
|||||||
bool remoteStateMovedLogged{ false };
|
bool remoteStateMovedLogged{ false };
|
||||||
bool reusableLogged{ false };
|
bool reusableLogged{ false };
|
||||||
bool directRemoteSpawnLogged{ false };
|
bool directRemoteSpawnLogged{ false };
|
||||||
std::optional<bool> lastObservedSneakState;
|
bool hasLoggedInitialAnimationState{ false };
|
||||||
|
std::optional<bool> lastObservedMovingState;
|
||||||
|
std::optional<bool> lastObservedSprintingState;
|
||||||
|
std::optional<bool> lastObservedSneakingState;
|
||||||
|
std::optional<bool> lastObservedJumpingState;
|
||||||
|
std::optional<bool> lastObservedWeaponDrawnState;
|
||||||
|
std::optional<std::string> lastObservedMovementType;
|
||||||
|
std::optional<ProxyAnimationSpeedBucket> lastObservedMovementSpeedBucket;
|
||||||
};
|
};
|
||||||
|
|
||||||
constexpr auto kProxyMovementMode = ProxyMovementMode::kRemotePlayerStateTest;
|
constexpr auto kProxyMovementMode = ProxyMovementMode::kRemotePlayerStateTest;
|
||||||
@@ -361,6 +377,10 @@ namespace
|
|||||||
}
|
}
|
||||||
|
|
||||||
void ResetRemoteMovementStateTracking();
|
void ResetRemoteMovementStateTracking();
|
||||||
|
void UpdateProxyAnimationStateDebug(
|
||||||
|
ProxyActorSlot& a_slot,
|
||||||
|
const RemotePlayer& a_remotePlayer,
|
||||||
|
const RE::Actor& a_proxy);
|
||||||
|
|
||||||
const char* GetProxyNeutralizationReasonLabel(ProxyNeutralizationReason a_reason)
|
const char* GetProxyNeutralizationReasonLabel(ProxyNeutralizationReason a_reason)
|
||||||
{
|
{
|
||||||
@@ -378,6 +398,47 @@ namespace
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const char* GetProxyAnimationSpeedBucketLabel(ProxyAnimationSpeedBucket a_bucket)
|
||||||
|
{
|
||||||
|
switch (a_bucket) {
|
||||||
|
case ProxyAnimationSpeedBucket::kIdle:
|
||||||
|
return "idle";
|
||||||
|
case ProxyAnimationSpeedBucket::kSlow:
|
||||||
|
return "slow";
|
||||||
|
case ProxyAnimationSpeedBucket::kWalk:
|
||||||
|
return "walk";
|
||||||
|
case ProxyAnimationSpeedBucket::kRun:
|
||||||
|
return "run";
|
||||||
|
case ProxyAnimationSpeedBucket::kSprint:
|
||||||
|
return "sprint";
|
||||||
|
default:
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ProxyAnimationSpeedBucket GetProxyAnimationSpeedBucket(const RemotePlayer& a_remotePlayer)
|
||||||
|
{
|
||||||
|
if (!a_remotePlayer.isMoving || a_remotePlayer.movementSpeed < 1.0F) {
|
||||||
|
return ProxyAnimationSpeedBucket::kIdle;
|
||||||
|
}
|
||||||
|
if (a_remotePlayer.isSprinting) {
|
||||||
|
return ProxyAnimationSpeedBucket::kSprint;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Tune these diagnostic thresholds against real Fallout 4 movement units
|
||||||
|
// before using them for visual locomotion animation decisions.
|
||||||
|
if (a_remotePlayer.movementSpeed < 75.0F) {
|
||||||
|
return ProxyAnimationSpeedBucket::kSlow;
|
||||||
|
}
|
||||||
|
if (a_remotePlayer.movementSpeed < 150.0F) {
|
||||||
|
return ProxyAnimationSpeedBucket::kWalk;
|
||||||
|
}
|
||||||
|
if (a_remotePlayer.movementSpeed < 300.0F) {
|
||||||
|
return ProxyAnimationSpeedBucket::kRun;
|
||||||
|
}
|
||||||
|
return ProxyAnimationSpeedBucket::kSprint;
|
||||||
|
}
|
||||||
|
|
||||||
std::string FormatPosition(const RE::NiPoint3& a_position)
|
std::string FormatPosition(const RE::NiPoint3& a_position)
|
||||||
{
|
{
|
||||||
return std::format("({:.2f}, {:.2f}, {:.2f})", a_position.x, a_position.y, a_position.z);
|
return std::format("({:.2f}, {:.2f}, {:.2f})", a_position.x, a_position.y, a_position.z);
|
||||||
@@ -4001,7 +4062,14 @@ namespace
|
|||||||
|
|
||||||
void ResetSlotObservationState(ProxyActorSlot& a_slot)
|
void ResetSlotObservationState(ProxyActorSlot& a_slot)
|
||||||
{
|
{
|
||||||
a_slot.lastObservedSneakState.reset();
|
a_slot.hasLoggedInitialAnimationState = false;
|
||||||
|
a_slot.lastObservedMovingState.reset();
|
||||||
|
a_slot.lastObservedSprintingState.reset();
|
||||||
|
a_slot.lastObservedSneakingState.reset();
|
||||||
|
a_slot.lastObservedJumpingState.reset();
|
||||||
|
a_slot.lastObservedWeaponDrawnState.reset();
|
||||||
|
a_slot.lastObservedMovementType.reset();
|
||||||
|
a_slot.lastObservedMovementSpeedBucket.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool HasSlotHeldSinceTime(const ProxyActorSlot& a_slot)
|
bool HasSlotHeldSinceTime(const ProxyActorSlot& a_slot)
|
||||||
@@ -4161,7 +4229,6 @@ namespace
|
|||||||
a_slot.restoredForCurrentState = true;
|
a_slot.restoredForCurrentState = true;
|
||||||
a_slot.firstSmoothedMovementLogged = false;
|
a_slot.firstSmoothedMovementLogged = false;
|
||||||
a_slot.remoteStateMovedLogged = false;
|
a_slot.remoteStateMovedLogged = false;
|
||||||
a_slot.lastObservedSneakState = a_remotePlayer.isSneaking;
|
|
||||||
|
|
||||||
LogInfoWithLocalPlayerPrefix(std::format(
|
LogInfoWithLocalPlayerPrefix(std::format(
|
||||||
"Runtime proxy actor {:08X} restored/reused for remote player {} at X={:.2f}, Y={:.2f}, Z={:.2f}.",
|
"Runtime proxy actor {:08X} restored/reused for remote player {} at X={:.2f}, Y={:.2f}, Z={:.2f}.",
|
||||||
@@ -4170,6 +4237,7 @@ namespace
|
|||||||
a_remotePlayer.x,
|
a_remotePlayer.x,
|
||||||
a_remotePlayer.y,
|
a_remotePlayer.y,
|
||||||
a_remotePlayer.z));
|
a_remotePlayer.z));
|
||||||
|
UpdateProxyAnimationStateDebug(a_slot, a_remotePlayer, a_proxy);
|
||||||
LogProxyMovementDiagnostic(
|
LogProxyMovementDiagnostic(
|
||||||
a_proxy,
|
a_proxy,
|
||||||
a_remotePlayer,
|
a_remotePlayer,
|
||||||
@@ -4179,22 +4247,52 @@ namespace
|
|||||||
ProxyMovementDiagnosticMode::kRestore);
|
ProxyMovementDiagnosticMode::kRestore);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ApplyRemoteMovementStateToSlot(ProxyActorSlot& a_slot, const RemotePlayer& a_remotePlayer)
|
void UpdateProxyAnimationStateDebug(
|
||||||
|
ProxyActorSlot& a_slot,
|
||||||
|
const RemotePlayer& a_remotePlayer,
|
||||||
|
const RE::Actor& a_proxy)
|
||||||
{
|
{
|
||||||
if (!a_slot.lastObservedSneakState) {
|
const auto speedBucket = GetProxyAnimationSpeedBucket(a_remotePlayer);
|
||||||
a_slot.lastObservedSneakState = a_remotePlayer.isSneaking;
|
const auto shouldLogInitial = !a_slot.hasLoggedInitialAnimationState;
|
||||||
return;
|
const auto changed =
|
||||||
|
!a_slot.lastObservedMovingState || *a_slot.lastObservedMovingState != a_remotePlayer.isMoving ||
|
||||||
|
!a_slot.lastObservedSprintingState || *a_slot.lastObservedSprintingState != a_remotePlayer.isSprinting ||
|
||||||
|
!a_slot.lastObservedSneakingState || *a_slot.lastObservedSneakingState != a_remotePlayer.isSneaking ||
|
||||||
|
!a_slot.lastObservedJumpingState || *a_slot.lastObservedJumpingState != a_remotePlayer.isJumping ||
|
||||||
|
!a_slot.lastObservedWeaponDrawnState || *a_slot.lastObservedWeaponDrawnState != a_remotePlayer.weaponDrawn ||
|
||||||
|
!a_slot.lastObservedMovementType || *a_slot.lastObservedMovementType != a_remotePlayer.movementType ||
|
||||||
|
!a_slot.lastObservedMovementSpeedBucket || *a_slot.lastObservedMovementSpeedBucket != speedBucket;
|
||||||
|
|
||||||
|
if (shouldLogInitial || changed) {
|
||||||
|
LogInfoWithLocalPlayerPrefix(std::format(
|
||||||
|
"Runtime proxy animation state {} for remote player {}: actor={:08X}, holdingIndex={}, moving={}, speed={:.1f}, speedBucket={}, sprinting={}, sneaking={}, jumping={}, weaponDrawn={}, movementType={}.",
|
||||||
|
shouldLogInitial ? "initial" : "changed",
|
||||||
|
a_remotePlayer.playerId,
|
||||||
|
a_proxy.GetFormID(),
|
||||||
|
a_slot.holdingPositionIndex,
|
||||||
|
a_remotePlayer.isMoving,
|
||||||
|
a_remotePlayer.movementSpeed,
|
||||||
|
GetProxyAnimationSpeedBucketLabel(speedBucket),
|
||||||
|
a_remotePlayer.isSprinting,
|
||||||
|
a_remotePlayer.isSneaking,
|
||||||
|
a_remotePlayer.isJumping,
|
||||||
|
a_remotePlayer.weaponDrawn,
|
||||||
|
a_remotePlayer.movementType));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (*a_slot.lastObservedSneakState == a_remotePlayer.isSneaking) {
|
a_slot.hasLoggedInitialAnimationState = true;
|
||||||
return;
|
a_slot.lastObservedMovingState = a_remotePlayer.isMoving;
|
||||||
}
|
a_slot.lastObservedSprintingState = a_remotePlayer.isSprinting;
|
||||||
|
a_slot.lastObservedSneakingState = a_remotePlayer.isSneaking;
|
||||||
|
a_slot.lastObservedJumpingState = a_remotePlayer.isJumping;
|
||||||
|
a_slot.lastObservedWeaponDrawnState = a_remotePlayer.weaponDrawn;
|
||||||
|
a_slot.lastObservedMovementType = a_remotePlayer.movementType;
|
||||||
|
a_slot.lastObservedMovementSpeedBucket = speedBucket;
|
||||||
|
|
||||||
a_slot.lastObservedSneakState = a_remotePlayer.isSneaking;
|
// TODO: Future visual milestones should investigate safe sneak/crouch
|
||||||
LogInfoWithLocalPlayerPrefix(std::format(
|
// application, weapon drawn visuals, locomotion graph behavior, jump
|
||||||
"Remote player {} sneak state changed: isSneaking={}",
|
// animation behavior, and read-only research for animation graph
|
||||||
a_remotePlayer.playerId,
|
// variables/events before any visual animation state is applied.
|
||||||
a_remotePlayer.isSneaking));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void MoveSlotProxyToRemotePlayer(
|
void MoveSlotProxyToRemotePlayer(
|
||||||
@@ -4218,6 +4316,7 @@ namespace
|
|||||||
a_slot.holdReason.reset();
|
a_slot.holdReason.reset();
|
||||||
a_slot.heldSinceTime = {};
|
a_slot.heldSinceTime = {};
|
||||||
a_slot.reusableLogged = false;
|
a_slot.reusableLogged = false;
|
||||||
|
UpdateProxyAnimationStateDebug(a_slot, a_remotePlayer, a_proxy);
|
||||||
|
|
||||||
if (shouldSnap) {
|
if (shouldSnap) {
|
||||||
LogThrottledInfo(
|
LogThrottledInfo(
|
||||||
@@ -4362,6 +4461,7 @@ namespace
|
|||||||
CaptureSlotInitialRemoteSpawnTransform(slot->second, a_remotePlayer);
|
CaptureSlotInitialRemoteSpawnTransform(slot->second, a_remotePlayer);
|
||||||
if (inserted) {
|
if (inserted) {
|
||||||
slot->second.holdingPositionIndex = g_nextProxyHoldingPositionIndex++;
|
slot->second.holdingPositionIndex = g_nextProxyHoldingPositionIndex++;
|
||||||
|
ResetSlotObservationState(slot->second);
|
||||||
LogInfoWithLocalPlayerPrefix(std::format(
|
LogInfoWithLocalPlayerPrefix(std::format(
|
||||||
"Runtime proxy slot created for remote player {} with holding index {}.",
|
"Runtime proxy slot created for remote player {} with holding index {}.",
|
||||||
a_remotePlayer.playerId,
|
a_remotePlayer.playerId,
|
||||||
@@ -4416,7 +4516,6 @@ namespace
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
ApplyRemoteMovementStateToSlot(a_slot, a_remotePlayer);
|
|
||||||
MoveSlotProxyToRemotePlayer(a_slot, *proxy, a_remotePlayer, a_player, a_sameCellPassed);
|
MoveSlotProxyToRemotePlayer(a_slot, *proxy, a_remotePlayer, a_player, a_sameCellPassed);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user