24 Commits

Author SHA1 Message Date
432741af52 feat(combat): cues de hit react + som punch + camera shake (cliente UE)
3 GameplayCueNotify_Burst BPs (taxonomia Lyra GameplayCue.Zeus.Combat.Hit.*):
  GC_Combat_Hit_React   -> PlayAnimMontage AM_HitReact_Light no alvo
  GC_Combat_Hit_Impact  -> SpawnSoundAtLocation SW_Punch_Cartoon na pos do alvo
  GC_Combat_Hit_Self    -> ClientStartCameraShake CS_HitReact no PC do alvo

Assets novos:
  AS_HitReact (+ AM_HitReact_Light/Medium/Heavy) -- montages de reacao
  SW_Punch_Cartoon -- SFX de impacto (0.3s)
  CS_HitReact -- LegacyCameraShake (osc 0.35s, pitch/roll/locZ)

DT_Abilities recriado (RowStruct ZeusAbilityDataRow): rows Dash+Kick com
AbilityClass + ActivateCueTag GameplayCue.Zeus.*. GC_Kick/GC_Dash com tags
renomeadas pro prefixo GameplayCue.* (filter do Asset Picker).

ZeusServerTags.ini: regenerado via ZeusTool tags sync (106 tags canonicas).

Source/Combat/ZeusAnimNotifyState_BodyHitWindow: AnimNotifyState que abre a
janela de hit detection (FootR capsule) durante o swing do Kick.

GC_Kick_Target removido (substituido por Combat.Hit.React reutilizavel).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 21:37:40 -03:00
84f552b4fa feat(gas): Kick V0 end-to-end -- IA_Kick + BP_GA_Kick + GC_Kick + rename Pawn + limpeza ThirdPerson
Cliente UE Kick V0 funcionando end-to-end (testado com 2 PIEs: ativador chuta
+ proxies dos peers veem montage via GameplayCue replicado).

ASSETS NOVOS:
- IA_Kick.uasset: Enhanced Input Action (Boolean) -- tecla F.
- IMC_Default: mapping F -> IA_Kick (trigger Pressed).
- BP_ZeusPlayerCharacter.uasset (em /Game/ZMMO/Core/Player/): renomeado a
  partir do BP_ThirdPersonCharacter; agora possui handler IA_Kick que chama
  RequestActivateAbilityByTag("Zeus.Ability.Melee.Kick") no UZeusGASComponent
  do PlayerState. ZeusGameMode aponta pra ele via FClassFinder.
- GC_Kick.uasset (em /Game/ZMMO/GAS/Cues/): GameplayCueNotify_Burst BP, parent
  AZeusGameplayCueNotify_Burst, tag Zeus.Cue.Melee.Kick. Override do hook
  OnZeusCueExecute (Lyra-style) -> Cast To Character (MyTarget) -> Play Anim
  Montage (AM_Kick_Montage). Quando server emite S_ABILITY_CUE, cliente
  spawna esse GC e roda a animacao no Mesh do proxy.

ASSETS MODIFICADOS:
- GA_Kick: conexoes PlayMontageAndWait -> EndAbility ajustadas. Antes TODAS
  as 5 saidas (OnCompleted/OnBlendedIn/OnBlendOut/OnInterrupted/OnCancelled)
  ligavam no EndAbility -- OnBlendedIn dispara ~100ms apos inicio (fim do
  blend in) -> EndAbility -> bStopWhenAbilityEnds=true -> montage para
  prematuro. Fix: so OnCompleted/OnInterrupted/OnCancelled. Hook BP novo
  OnZeusAbilityConfirmedByServer -> Print "Kick CONFIRMED by server".
- DT_Abilities: row Kick adicionada (Tag=Zeus.Ability.Melee.Kick, AbilityClass
  =GA_Kick_C, ActivationMontage=AM_Kick_Montage, ActivateCueTag=Zeus.Cue.Melee.Kick,
  Category=Melee, KeybindHint=F).
- BP_PlayerState: salvo apos GameMode apontar pra ele (mark dirty).
- ABP_Unarmed: deletado 1 Slot DefaultSlot duplicado (warning AnimBlueprintLog
  "node ja existe" + PlayMontage interno falhava em escolher slot).

LIMPEZA Content/ThirdPerson/:
- BP_ThirdPersonCharacter.uasset DELETED (renomeado pra BP_ZeusPlayerCharacter).
- BP_ThirdPersonGameMode.uasset DELETED (sem refs externas).
- BP_ThirdPersonPlayerController.uasset DELETED (so usado por BP_ThirdPersonGameMode).
- Menu.umap DELETED (sample UE template, sem refs).
- MI_ThirdPersonColWay.uasset mantido (90 refs em L_Test_UI -- mover pra
  /Game/ZMMO/Materials/ fica pra batch futuro).

Content/_DevOnly/ NAO incluido.

Tested in PIE 2 players: PIE0 aperta F -> chuta visualmente; proxy do PIE0
no PIE1 tambem chuta (montage replicado via GC). Cooldown 1.5s ativo.
Stamina cai 100 -> 95 a cada kick.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-18 03:56:28 -03:00
1308338953 feat(gas): GameMode aponta pra BPs (Pawn/PlayerState) + cleanup ZeusCharacter
ZeusGameMode.cpp:
- DefaultPawnClass: usa FClassFinder pra /Game/ZMMO/Core/Player/BP_ZeusPlayerCharacter
  (fallback AZeusCharacter). Sem isso, engine instancia C++ direto e perde
  toda customizacao BP (EnhancedInputAction events, AnimBP wire, componentes
  BP-added).
- PlayerStateClass: usa FClassFinder pra /Game/ZMMO/Core/Player/BP_PlayerState
  (fallback AZeusPlayerState). Mesmo motivo -- BP filho tem componentes (ex:
  UZeusGASComponent via ComponentClasses no DefaultGame.ini) e overrides do
  designer que classes C++ puras nao tem.
- TODO: renomear BP_PlayerState -> BP_ZeusPlayerState (alinhar com convencao
  BP_ZeusPlayerCharacter); path do FClassFinder ja preparado pra renomear.

ZeusCharacter.cpp/h cleanup:
- Removida gambiarra anterior (PossessedBy + OnRep_PlayerState overrides +
  RefreshGASAvatar com FTimerHandle retry). Substituida pelo pattern
  event-driven em UZeusGASComponent::HandlePawnSet (commit server anterior).
- Includes desnecessarios (AbilitySystemComponent.h + ZeusAbilitySystemComponent.h)
  removidos.

ZMMO.uproject:
- GameplayAbilities adicionado em Plugins (UE 5.7 exige que modulo do jogo
  declare explicitamente plugins que ele consome -- antes vinha transitivamente
  do plugin ZeusGAS, gerava warning de build).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-18 03:56:00 -03:00
21d42d9846 chore(gas): sync GameplayTags do server (Stamina/Kick/Spell/Buff) + flags de debug GAS
ZeusServerTags.ini -- merge gerado por sync_tags.ps1 (14 tags novas vindo do
ZeusServer dump JSON via ZeusTool tags sync):
- Stamina:           Zeus.Attribute.Stamina + MaxStamina
- Kick:              Zeus.Ability.Melee + Zeus.Ability.Melee.Kick
                     Zeus.Effect.Cooldown.Kick + Zeus.Cooldown.Kick
                     Zeus.Cue.Melee + Zeus.Cue.Melee.Kick
- FireballRain rename: Zeus.Ability.Spell.* + Zeus.Effect.Cooldown.FireballRain
                       Zeus.Cooldown.FireballRain
- Buff reorg:        Zeus.Effect.Buff + Zeus.Effect.Buff.StrUp

Tag orfa removida manualmente: Zeus.Ability.Combat.Kick (Kick mudou
de Combat.* pra Melee.*; merge preservou a antiga, limpo aqui).

DefaultGame.ini: flags de debug do GAS pra Kick development:
- bUseDebugTargetFromHud=true   (showdebug abilitysystem aponta pra mira)
- AbilitySystem.LogAbilityTriggers=1
- AbilitySystem.DebugAbilityTags=1

Lembrete: GameplayTags.ini so' eh lido no boot do editor -- reiniciar UE
pra registrar as tags novas (BP_GA_Kick depende de Zeus.Ability.Melee.Kick).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-18 01:37:45 -03:00
2895889e2b feat(gas): rename BP_GA_* -> GA_* + DT_Abilities seed + wire no ThirdPersonCharacter
Convencao de nome: abilities passam de "BP_GA_<Nome>" pra "GA_<Nome>"
(remove o "BP_" redundante -- ja sabemos que GA_ e' Blueprint).

- BP_GA_Dash.uasset -> GA_Dash.uasset
- BP_GA_Kick.uasset -> GA_Kick.uasset (pasta Kick/ mantida)
- DT_Abilities: linha do Kick atualizada pra apontar pra GA_Kick
- BP_ThirdPersonCharacter: ability Kick ligada (input/grant)

Doc HOWTO ja' reflete a nova convencao (ver server commit).
Content/_DevOnly/ NAO incluido.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-18 00:50:09 -03:00
1114ddbb11 feat(gas): seed inicial do GASCombatSystem -- ABP_Unarmed + Kick ability
- ABP_Unarmed.uasset: AnimBP unarmed (modificado) - base de animacao p/ combate sem arma
- BP_GA_Kick.uasset: GameplayAbility blueprint do Kick
- AS_Kick.uasset: animation sequence do golpe
- AM_Kick_Montage.uasset: montage que a ability dispara

Primeiro asset GAS authored cliente-side -- pre-requisito pra Fase 2 (M9 cues + M10 AbilityTask cliente).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-18 00:29:10 -03:00
9a430dabc3 feat(aoi): UZeusAOIComponent ganha visibility tracking + delegates BP (1.4 do roteiro PvP)
Estende o componente (que ja existia como debug overlay) com:
- TSet<int64> VisibleEntityIds_ espelhando ENT_SPAWN/ENT_DESPAWN do V1
- Delegates BlueprintAssignable: OnEntityEnteredAOI(EntityId) + OnEntityExitedAOI(EntityId, Reason)
- BlueprintPure: GetVisibleEntityCount, GetVisibleEntityIds, IsEntityVisible, GetSelfEntityId
- Bind/unbind dos delegates DYNAMIC do UZeusNetworkingClientSubsystem (AddDynamic/RemoveDynamic)
  no BeginPlay/EndPlay
- Self-entity filtrada via OnSelfEntityAssigned -- proprio char nao trackeia

Sem histerese client-side (server ja filtra com margem ~2048cm; se aparecer
flapping visual, adicionar depois). Memoria: project_aoi_client_component.

NAO testado in-game.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-18 00:23:51 -03:00
a3f2fa9c06 feat(client): overlay de fronteira -- ajustes + materiais e external actor
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-17 22:44:11 -03:00
1f2441bde7 Merge pull request 'Consolidar GameplayAbilitySystem na main (baseline cliente tudo funcionando 2026-06-16)' (#6) from GameplayAbilitySystem into main
Reviewed-on: #6
2026-06-16 00:26:52 -03:00
216e580548 Overlay de fronteira (cliente): paredes do grid + labels Cell X,Y
- AZeusFrontierOverlayActor: paredes (ProceduralMesh, material de portal) em
  todas as fronteiras do grid; label DrawDebugString no centro de cada cell com
  notacao de quadtree ("Cell 0" / "Cell 0,2" pos-split) + nome do servidor.
- UZeusAOIComponent: liga via toggle "Fronteiras (Mesh)" / CVar zeus.debug.frontier,
  spawna/atualiza o ator, re-pede o 6163 a cada 2s (reflete split/merge).
- ZMMO.Build.cs: + ProceduralMeshComponent.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 00:51:42 -03:00
de294f4075 fix(yaw): proxy replica o yaw do CORPO do dono (orient-to-movement), nao a mira
[YAW-BODY-ORIENT] commit conjunto cliente + servidor.

Sintoma: ao apertar D o proxy virava ~45 graus de uma vez, em vez de virar
igual ao player local (suave, so um pouco), e parado seguia o mouse.

Raiz: a fonte de verdade do yaw estava errada. O servidor derivava
simpleYawDeg de atan2(velocidade) INSTANTANEO ao mover (snap pra direcao do
movimento) e da mira (ControlRotation) ao parar -- nenhum dos dois e o yaw
do corpo do dono.

Correcao ponta-a-ponta:
- Cliente: envia GetActorRotation().Yaw (yaw do CORPO; o CMC ja orienta pra
  direcao do movimento a 500 deg/s e bUseControllerRotationYaw=false faz o
  MOUSE nao girar o corpo) no lugar de GetControlRotation().Yaw.
- Servidor: usa o yaw recebido DIRETO em simpleYawDeg (remove atan2
  instantaneo + mira-no-idle).
- Proxy: replica Snapshot.YawDeg interpolado shortest-path (remove a
  derivacao por Atan2 da velocidade).

Resultado: o proxy vira identico ao corpo do dono e o mouse nao gira o corpo.
Futuro (aim/combate/Motion Matching): adicionar um campo aimYawDeg dedicado em
vez de reusar este.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 02:22:07 -03:00
4d737829f5 ZeusPlayerProxy: fix jitter por fome de deltas (mitigacao + anti-stale) [Change-Set: JITTER-STARVATION-2026-06-13]
Player remoto parado nao gera DELTA_BATCH (ComputeDelta suprime no server) -> o
proxy fica "mudo": NewestMs congela, renderLagMs cresce 1:1 com o wall-clock ->
extrapola sempre -> "anda no lugar" (moonwalk) e salta quando o player volta.
Provado em log (secsSinceSnap subindo enquanto o remoto esta moving=0). NAO e'
clock errado nem self-proxy (red-herrings descartados); o seed do relogio esta' OK.

- Tick: quando faminto (extrap alem de MaxExtrapolationSeconds+0.15s), zera a vel
  visual -> proxy vai pra Idle em vez de moonwalk.
- ApplyEntitySnapshot: anti-stale -- snapshot > 1000ms a frente do topo do buffer
  descarta o buffer + re-bootstrapa o ServerClockOffsetMs -> snap limpo (sem span
  gigante na interpolacao). Loga "buffer STALE gap=Xms -> reset+reseed".
- Logs DIAG PERMANENTES (Warning, mantidos de proposito -- ver memoria
  project_proxy_delta_starvation_jitter): SEED clock + renderLagMs/newestMs/
  secsSinceSnap/speed no Tick. Sonda anti-regressao.

Cobre tambem o interserver: shadow proxies parados sofrem a mesma fome.
Validado em jogo (user: "sem jitter nenhum"). Par server: PubV2-DIAG (mesmo Change-Set).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 01:26:33 -03:00
341e6895c9 Config/Tags: ZeusServerTags.ini gerado (tags do server p/ o GAS) [Change-Set: TAGS-SYNC-2026-06-12]
80 GameplayTags do ZeusServer (Zeus.Status/Ability/Effect/Cue/Attribute/Cooldown)
geradas pelo pipeline build-time (par com o commit de mesmo Change-Set no repo
ZeusServerEngine). O UE le Config/Tags/*.ini no boot e registra as tags, pro
cliente resolver os hashes de GE/Tag/Cue (FNV-1a64) -- fim do "tag nao resolvida".
Regeravel com sync_tags.ps1; nao editar a mao.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 23:44:05 -03:00
5f4c88637f ZN V1: overlay AOI canonico + ENT_SPAWN keyframe [Change-Set: AOI-VIS-2026-06-12]
Lado CLIENTE ZMMO (par com o commit de mesmo Change-Set no repo ZeusServerEngine -- devem
ir juntos, o wire do ENT_SPAWN mudou).

- UZeusAOIComponent: migrado do transporte legacy (SendDebugAoiRequest/OnDebugAoiInfo,
  desativado no V1) pro canonico (EmitDebugAoiRequest 6160 / OnDebugAoiInfo 6161). Esfera de
  debug 32 -> 48 segments + clamp visual de 500m (resolve "zona sem limite / poucas linhas").
- ZeusWorldSubsystem::OnNetEntitySpawned: recebe vel + grounded + serverTimeMs (delegate
  FZeusV1OnEntitySpawned 4 -> 7 params) e semeia o proxy recem-criado via HandlePlayerStateUpdate
  -> ancora o relogio de interpolacao + a vel inicial -> proxy nasce ja' animando no re-spawn
  (sai/volta do raio AOI segurando W), sem Idle deslizando.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 21:04:43 -03:00
77d52a703b ZMMO: CHAR_INFO via V1 (Fase A) + fix loading travado
- ZeusWorldSubsystem consome ENT_CHAR_INFO (delegate OnCharInfo): aplica
  nome/guild no PlayerState do entityId certo (self ou proxy), com cache porque
  o CHAR_INFO chega antes do ator existir (server emite antes do ENT_SPAWN).
- FIX loading travado: a etapa "Spawn" do loading dependia do legacy
  OnPlayerSpawned, que não dispara mais com V1 -> loading eterno. Agora a etapa
  é marcada pelo sinal V1 OnSelfEntityAssigned (ENT_SELF); bind legacy removido.
  Travel ainda passa por OnServerTravelRequested (ponte legacy) até a Fase D.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 13:06:42 -03:00
9f5ccd3a05 ZMMO: migração para ZeusNetworking V1 canônico + fix overshoot do proxy
- ZeusCharacter/ZeusWorldSubsystem passam a usar UZeusNetworkingClientSubsystem:
  ENT_SELF (entidade própria), INPUT 6077 via EmitInput, spawn/despawn/delta
  rebindados pro subsystem novo (legacy vira fallback).
- ZeusPlayerProxy consome velocidade/grounded/serverTimeMs (delta 0x02): anima,
  vira na direção do movimento e cola no chão (MOVE_Walking).
- Fix overshoot/inércia: FlushInputAxisToServer considera velocidade residual
  (braking) e envia a 30Hz até o dono parar de fato; o proxy desacelera suave,
  sem extrapolar ~1m além nem snap-back.
- Char-select -> ConnectWithTicket (handoff ticket) quando bUseZeusNetworkingV1.

Inclui logs [InputDbg] (ZeusCharacter) e DEBUG-PROXY (ZeusPlayerProxy) mantidos
para testes posteriores de input/jitter.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 02:51:37 -03:00
c0f6d32d95 feat(zmmo-client): logs AOI categorias + K4 cfg recv
Rodando sem Jitter visivel.

- DECLARE/DEFINE LogZeusHandoff/AOI/Proxy/HaloTransition no modulo
  ZMMO (nao no plugin) por causa do adaptive non-unity build do UBT
  que usa git status do repo do cliente -- plugin em outro repo nao
  dispara rebuild quando edit so o header dele.
- ZeusAOIComponent::HandleAoiConfig loga raios recebidos do server
  pra cruzar com Net.AOI server-side ("AOI cfg recv interestCm=6000
  (60m) despawnCm=8000 (80m)").
- Parte da milestone 2026-06-11: proxy/PIE estaveis apos fix de
  double-emit AOI no server (AOIRegistry::ForceTickNow).
2026-06-11 18:01:32 -03:00
f21d059b67 fix(zmmo-client): ZeusPlayerProxy clock offset EMA + asset cleanup L_TestWorld
ZeusPlayerProxy.cpp -- H3 fix (2026-06-06): EMA com clamp para
ServerClockOffsetMs
================================================================
Antes: offset congelado na 1a amostra de ApplyEntitySnapshot. Em
cross-server handoff, a fonte do ServerTimeMs muda (publisher antigo
emitia 12340000, novo emite 12351000) e o offset fica permanentemente
skewed. Causa tremor pos-handoff + drift cumulativo de clock cliente
vs server.

Depois: cada amostra recalcula candidato (LocalNowMs - Snapshot.ServerTimeMs)
e suaviza via EMA com alpha=0.02 (~50 amostras para convergir) + clamp
max 2ms por amostra (a 30Hz isso converge a 60ms/s, suficiente para
absorver drift normal sem causar tremor visual). 1a amostra continua
bootstrap direto.

Trecho:
  const double Alpha = 0.02;
  const int64 RawTarget = ServerClockOffsetMs*(1-Alpha) + OffsetCandidate*Alpha;
  const int64 Delta = RawTarget - ServerClockOffsetMs;
  const int64 ClampedDelta = clamp(Delta, -2, 2);
  ServerClockOffsetMs += ClampedDelta;

Asset cleanup L_TestWorld
==========================
DT_Maps.uasset modificado + 3 ExternalActors deletados (provavelmente
limpeza no UE editor ao reorganizar mapa). Empacotado junto pra branch
ficar limpa.

* Content/ZMMO/Data/World/DT_Maps.uasset (M)
* Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/2/BR/38W34OMKW0W3L1PGS3Y98G.uasset (D)
* Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/4/8L/X3PMRJRGAACIU1WG1P1KJ0.uasset (D)
* Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/8/L1/C6M65Q91PG5E7ML91VQZX3.uasset (D)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-11 14:52:39 -03:00
e87b2cec6c fix(zmmo-client): guard HandlePlayerSpawned ignore proprio entityId + ZeusV1 config
Acompanha commit server 54f2f9a (P10-FIX-LEGACY). Resolve bug "vejo proxy
de UM mas o outro NAO ve" + "proxy fantasma do proprio player" -- causado
por server emitir S_SPAWN_PLAYER local=0 do proprio entityId via AOI re-emit
(workflow wcyeqgrad finding).

Source/ZMMO/Game/Network/ZeusWorldSubsystem.cpp
================================================
HandlePlayerSpawned guard novo logo apos EntityId==0 check:

  if (LocalEntityId != 0 && EntityId == LocalEntityId)
  {
      UE_LOG(LogZMMO, Verbose,
          TEXT("ignoring S_SPAWN_PLAYER for own LocalEntityId=%lld (bIsLocal=%d)"),
          LocalEntityId, bIsLocal ? 1 : 0);
      return;
  }

Sem o guard, server re-emit S_SPAWN_PLAYER local=0 do proprio entityId
(via AOI SetOnEnter callback em catch-up cross-server ou recompute pos-
handoff) criava proxy fantasma do proprio player. GAS/HUD/PlayerArray
roteava via fantasma e o pawn real perdia vinculo de identidade visivel
pros outros players.

Defesa em profundidade -- complementa fix server-side
(AoiReplicationHelpers.cpp:28 SendSpawnPlayerToConn agora tem skip-self
guard antes de construir payload) + SpawnKey dedupe sem IsLocal no plugin
(ZeusNetworkSubsystem.cpp:949 removeu IsLocal do hash).

Config/DefaultZeusV1.ini
=========================
Config inicial para UZeusNetworkingClientSubsystem (P9-6/P9-7 plugin V1).
Aponta para Gateway 127.0.0.1:7777 (fluxo padrao producao). Documenta
alternativa de direct connect 9001 (debug only -- bug Windows multi-NIC
loopback). bUseZeusNetworkingV1=True ativa V1 plugin em paralelo ao
legacy (transport V1 hoje quebrado via Gateway -- channelId nao eh
routing key, Phase 12 territory; gameplay continua via legacy ate la).

Validado smoke 2 PIE 2026-06-08 20:32
======================================
* SP_01: players=2 conns=2 + AOI players=2 maxSet=1 enters=5 hyst=92 stable
* UE: [PIE0] e [PIE1] prefix funcionando (commit server 54f2f9a ZNET_PIE_PREFIX)
* "S_SPAWN_PLAYER duplicate ignored" dedupe pegando re-emit (Batch 2)
* S_CHAR_INFO trocado correto: PIE0 ve Olatudook, PIE1 ve Mateuus
* Saida limpa: UnregisterViewer + Broadcast S_DESPAWN_PLAYER targets=1

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-08 20:33:41 -03:00
9422522d6d Merge pull request 'feat: GameplayAbilitySystem (client) - GAS client + Admin Tools + AOI debug' (#5) from GameplayAbilitySystem into main 2026-06-04 18:40:37 -03:00
c3b32bc1a1 feat(admin-tools): UZeusAOIComponent desenha as 2 zonas reais de AOI
Recebe a config de AOI do servidor (S_DEBUG_AOI_INFO via OnDebugAoiInfo) e
desenha 2 esferas com os raios REAIS: Zona de Interesse (interna, cyan) e
Zona de Despawn (AOI) (externa/histerese, laranja). Pede a config no BeginPlay
e ao ligar o overlay; nao desenha enquanto o raio == 0 (loading).

- Overlay EZeusAOIOverlay::SpawnZone -> DespawnZone (AOI).
- Expoe os raios via interface (GetInterestRadiusCm/GetDespawnRadiusCm) p/ o
  painel admin mostrar o numero (read-only — config e' do servidor).
- CVar zeus.debug.spawn -> zeus.debug.despawn.

A interface, o opcode (1700/1701) e o painel vivem no repo Server (plugins).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 17:53:46 -03:00
cda5fea776 feat(admin-tools): consumidor client do painel debug (F8 + UZeusAOIComponent)
- UZeusAOIComponent (Game/Network): componente de gameplay do player local com
  plus de debug. Overlays via DrawDebug* (esfera AOI real; spawn/cell/handoff
  placeholders) dirigidos pela UI via IZeusAOIDebugTarget + console vars. Estado
  persiste no componente entre aberturas do menu (IsOverlayEnabled).
- AZeusCharacter: anexa o componente + abre/fecha o painel no F8 (UIOnly trava o
  jogo; Esc/F8 fecham); LoadClass lazy do painel no content do PLUGIN
  (/ZeusAdminTools/UI/WBP_AdminToolsAIO).
- ZMMO.Build.cs: depende de ZeusAdminToolsRuntime.
- UIProgressBar_Base: rename EaseOutCubic -> EaseOutCubicPB (fix unity-build).
- ZMMO.uproject: remove entrada NwiroIntegrationKit.

A UI e o C++ do plugin ZeusAdminTools vivem no repo Server (plugin autocontido).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 16:19:37 -03:00
16de963301 fix(net): S_CHAR_INFO so' aplica em si mesmo (anti name overwrite por proxy)
Bug: handler aceitava qualquer S_CHAR_INFO do server (proprio + catch-up de
proxies pre-existentes) e sobrescrevia o PlayerState local. Resultado: nome
do ultimo proxy recebido virava o nome do char local na tela.

Fix: bail se InEntityId != ZeusEntityId (proxies remotos sao roteados pelo
registry no UZeusWorldSubsystem; nameplate por EntityId fica pra futuro).
2026-06-03 21:56:49 -03:00
8d73cc9df8 feat(gas): SM6 client EntityId u64 + M8 cue assets + UI/data tweaks (sessao 1+2) 2026-06-03 18:18:02 -03:00
111 changed files with 1986 additions and 91 deletions

View File

@@ -71,6 +71,13 @@ ScreenSetAsset=/Game/ZMMO/UI/InGame/DA_InGameScreenSet.DA_InGameScreenSet
; (UWorldSubsystem) subscribe nos delegates do UZeusNetworkSubsystem. ; (UWorldSubsystem) subscribe nos delegates do UZeusNetworkSubsystem.
+ComponentClasses=/Script/ZeusGAS.ZeusGASComponent +ComponentClasses=/Script/ZeusGAS.ZeusGASComponent
; === GAS Cue paths (Batch 2.5 — S_ABILITY_CUE multicast cosmetico) ===
; UGameplayCueManager scaneia esses paths no boot pra mapear FGameplayTag
; -> AGameplayCueNotify_Actor BP. UZeusGASComponent::DispatchAbilityCue chama
; ASC->ExecuteGameplayCueLocal(CueTag, params) -> manager resolve pelo path.
[/Script/GameplayAbilities.AbilitySystemGlobals]
+GameplayCueNotifyPaths="/Game/ZMMO/GAS/Cues"
[/Script/Engine.AssetManagerSettings] [/Script/Engine.AssetManagerSettings]
-PrimaryAssetTypesToScan=(PrimaryAssetType="Map",AssetBaseClass=/Script/Engine.World,bHasBlueprintClasses=False,bIsEditorOnly=True,Directories=((Path="/Game/Maps")),SpecificAssets=,Rules=(Priority=-1,ChunkId=-1,bApplyRecursively=True,CookRule=Unknown)) -PrimaryAssetTypesToScan=(PrimaryAssetType="Map",AssetBaseClass=/Script/Engine.World,bHasBlueprintClasses=False,bIsEditorOnly=True,Directories=((Path="/Game/Maps")),SpecificAssets=,Rules=(Priority=-1,ChunkId=-1,bApplyRecursively=True,CookRule=Unknown))
-PrimaryAssetTypesToScan=(PrimaryAssetType="PrimaryAssetLabel",AssetBaseClass=/Script/Engine.PrimaryAssetLabel,bHasBlueprintClasses=False,bIsEditorOnly=True,Directories=((Path="/Game")),SpecificAssets=,Rules=(Priority=-1,ChunkId=-1,bApplyRecursively=True,CookRule=Unknown)) -PrimaryAssetTypesToScan=(PrimaryAssetType="PrimaryAssetLabel",AssetBaseClass=/Script/Engine.PrimaryAssetLabel,bHasBlueprintClasses=False,bIsEditorOnly=True,Directories=((Path="/Game")),SpecificAssets=,Rules=(Priority=-1,ChunkId=-1,bApplyRecursively=True,CookRule=Unknown))
@@ -84,3 +91,12 @@ bShouldAcquireMissingChunksOnLoad=False
bShouldWarnAboutInvalidAssets=True bShouldWarnAboutInvalidAssets=True
MetaDataTagsForAssetRegistry=() MetaDataTagsForAssetRegistry=()
[/Script/GameplayAbilities.AbilitySystemGlobals]
bUseDebugTargetFromHud=true
[ConsoleVariables]
; mostra cooldowns, ativações, falhas
AbilitySystem.LogAbilityTriggers=1
; mostra effects aplicados/removidos com magnitude
AbilitySystem.DebugAbilityTags=1

19
Config/DefaultZeusV1.ini Normal file
View File

@@ -0,0 +1,19 @@
; DefaultZeusV1.ini
; Config inicial pra UZeusNetworkingClientSubsystem (P9-6/P9-7 plugin).
; Editar via UE Project Settings -> Plugins -> Zeus V1 OU editar este arquivo
; e relancar o editor.
;
; PORT 7777: ZeusGateway. NodeService popula routes via charId lookup quando
; cliente conecta (validado: handshake routed charId=7 -> SP_01 cell 0). Fluxo
; padrao de producao -- UE conecta no Gateway, Gateway resolve cell ownership
; via lease em Valkey + encaminha pro ZS correto.
;
; ALTERNATIVO (debug only -- bug aberto pendente investigacao):
; ServerPort=9001 conecta DIRETO em SP_01 mas hoje pacote UE nao chega no
; server (FUdpSocketBuilder + Windows multi-NIC loopback issue). Use 7777.
[/Script/ZeusNetwork.ZeusNetworkingClientSubsystem]
ServerHost=127.0.0.1
ServerPort=7777
bUseZeusNetworkingV1=True
bAutoConnectOnStart=True

View File

@@ -0,0 +1,114 @@
; Gerado por ZeusTool tags sync -- tags do ZeusServer p/ o cliente UE.
; NAO editar a mao: regenere com `ZeusTool tags sync <json> <ini>`.
[/Script/GameplayTags.GameplayTagsList]
GameplayTagList=(Tag="GameplayCue",DevComment="")
GameplayTagList=(Tag="GameplayCue.Zeus",DevComment="")
GameplayTagList=(Tag="GameplayCue.Zeus.Melee",DevComment="")
GameplayTagList=(Tag="GameplayCue.Zeus.Melee.Kick",DevComment="")
GameplayTagList=(Tag="GameplayCue.Zeus.Combat",DevComment="Categoria root de cues compartilhados por TODO melee (reutilizaveis)")
GameplayTagList=(Tag="GameplayCue.Zeus.Combat.Hit",DevComment="Familia de cues do impacto de hit")
GameplayTagList=(Tag="GameplayCue.Zeus.Combat.Hit.Impact",DevComment="Broadcast AOI - SFX impact + VFX sparks na location (todos ouvem)")
GameplayTagList=(Tag="GameplayCue.Zeus.Combat.Hit.React",DevComment="Broadcast AOI - AnimMontage de hit react no target (todos veem)")
GameplayTagList=(Tag="GameplayCue.Zeus.Combat.Hit.Self",DevComment="Unicast - feedback privado do alvo (camera shake, hit flash, rumble)")
GameplayTagList=(Tag="GameplayCue.Zeus.Melee.Kick.Target",DevComment="")
GameplayTagList=(Tag="GameplayCue.Zeus.Movement",DevComment="")
GameplayTagList=(Tag="GameplayCue.Zeus.Movement.Dash",DevComment="")
GameplayTagList=(Tag="Zeus",DevComment="")
GameplayTagList=(Tag="Zeus.Ability",DevComment="")
GameplayTagList=(Tag="Zeus.Ability.Casting",DevComment="")
GameplayTagList=(Tag="Zeus.Ability.Channeling",DevComment="")
GameplayTagList=(Tag="Zeus.Ability.Melee",DevComment="")
GameplayTagList=(Tag="Zeus.Ability.Melee.Kick",DevComment="")
GameplayTagList=(Tag="Zeus.Ability.Movement",DevComment="")
GameplayTagList=(Tag="Zeus.Ability.Movement.Dash",DevComment="")
GameplayTagList=(Tag="Zeus.Ability.Recovery",DevComment="")
GameplayTagList=(Tag="Zeus.Ability.Spell",DevComment="")
GameplayTagList=(Tag="Zeus.Ability.Spell.FireballRain",DevComment="")
GameplayTagList=(Tag="Zeus.Attribute",DevComment="")
GameplayTagList=(Tag="Zeus.Attribute.Agi",DevComment="")
GameplayTagList=(Tag="Zeus.Attribute.Aspd",DevComment="")
GameplayTagList=(Tag="Zeus.Attribute.Atk",DevComment="")
GameplayTagList=(Tag="Zeus.Attribute.BaseExp",DevComment="")
GameplayTagList=(Tag="Zeus.Attribute.BaseLevel",DevComment="")
GameplayTagList=(Tag="Zeus.Attribute.Crit",DevComment="")
GameplayTagList=(Tag="Zeus.Attribute.Def",DevComment="")
GameplayTagList=(Tag="Zeus.Attribute.Dex",DevComment="")
GameplayTagList=(Tag="Zeus.Attribute.Flee",DevComment="")
GameplayTagList=(Tag="Zeus.Attribute.Hit",DevComment="")
GameplayTagList=(Tag="Zeus.Attribute.Hp",DevComment="")
GameplayTagList=(Tag="Zeus.Attribute.HpRegenRate",DevComment="")
GameplayTagList=(Tag="Zeus.Attribute.Int",DevComment="")
GameplayTagList=(Tag="Zeus.Attribute.JobExp",DevComment="")
GameplayTagList=(Tag="Zeus.Attribute.JobLevel",DevComment="")
GameplayTagList=(Tag="Zeus.Attribute.Luk",DevComment="")
GameplayTagList=(Tag="Zeus.Attribute.Matk",DevComment="")
GameplayTagList=(Tag="Zeus.Attribute.MatkMax",DevComment="")
GameplayTagList=(Tag="Zeus.Attribute.MatkMin",DevComment="")
GameplayTagList=(Tag="Zeus.Attribute.MaxHp",DevComment="")
GameplayTagList=(Tag="Zeus.Attribute.MaxSp",DevComment="")
GameplayTagList=(Tag="Zeus.Attribute.Mdef",DevComment="")
GameplayTagList=(Tag="Zeus.Attribute.SkillPoint",DevComment="")
GameplayTagList=(Tag="Zeus.Attribute.Sp",DevComment="")
GameplayTagList=(Tag="Zeus.Attribute.SpRegenRate",DevComment="")
GameplayTagList=(Tag="Zeus.Attribute.Stamina",DevComment="")
GameplayTagList=(Tag="Zeus.Attribute.StaminaRegenRate",DevComment="")
GameplayTagList=(Tag="Zeus.Attribute.StatusPoint",DevComment="")
GameplayTagList=(Tag="Zeus.Attribute.Str",DevComment="")
GameplayTagList=(Tag="Zeus.Attribute.Vit",DevComment="")
GameplayTagList=(Tag="Zeus.Combat",DevComment="")
GameplayTagList=(Tag="Zeus.Combat.Blocking",DevComment="")
GameplayTagList=(Tag="Zeus.Combat.Dodging",DevComment="")
GameplayTagList=(Tag="Zeus.Combat.InCombat",DevComment="")
GameplayTagList=(Tag="Zeus.Combat.LockOn",DevComment="")
GameplayTagList=(Tag="Zeus.Combo",DevComment="")
GameplayTagList=(Tag="Zeus.Combo.Slot1",DevComment="")
GameplayTagList=(Tag="Zeus.Combo.Slot2",DevComment="")
GameplayTagList=(Tag="Zeus.Combo.Slot3",DevComment="")
GameplayTagList=(Tag="Zeus.Combo.Window",DevComment="")
GameplayTagList=(Tag="Zeus.Cooldown",DevComment="")
GameplayTagList=(Tag="Zeus.Cooldown.FireballRain",DevComment="")
GameplayTagList=(Tag="Zeus.Damage",DevComment="")
GameplayTagList=(Tag="Zeus.Damage.Earth",DevComment="")
GameplayTagList=(Tag="Zeus.Damage.Fire",DevComment="")
GameplayTagList=(Tag="Zeus.Damage.Holy",DevComment="")
GameplayTagList=(Tag="Zeus.Damage.Magical",DevComment="")
GameplayTagList=(Tag="Zeus.Damage.Physical",DevComment="")
GameplayTagList=(Tag="Zeus.Damage.Poison",DevComment="")
GameplayTagList=(Tag="Zeus.Damage.Shadow",DevComment="")
GameplayTagList=(Tag="Zeus.Damage.Water",DevComment="")
GameplayTagList=(Tag="Zeus.Damage.Wind",DevComment="")
GameplayTagList=(Tag="Zeus.Effect",DevComment="")
GameplayTagList=(Tag="Zeus.Effect.Buff",DevComment="")
GameplayTagList=(Tag="Zeus.Effect.Buff.StrUp",DevComment="")
GameplayTagList=(Tag="Zeus.Effect.Cooldown",DevComment="")
GameplayTagList=(Tag="Zeus.Effect.Cooldown.Dash",DevComment="")
GameplayTagList=(Tag="Zeus.Effect.Cooldown.FireballRain",DevComment="")
GameplayTagList=(Tag="Zeus.Effect.Cooldown.Kick",DevComment="")
GameplayTagList=(Tag="Zeus.Effect.Cost",DevComment="")
GameplayTagList=(Tag="Zeus.Effect.Cost.Sp_10",DevComment="")
GameplayTagList=(Tag="Zeus.Effect.Cost.Sp_50",DevComment="")
GameplayTagList=(Tag="Zeus.Effect.Cost.Stamina_5",DevComment="")
GameplayTagList=(Tag="Zeus.Effect.Damage",DevComment="")
GameplayTagList=(Tag="Zeus.Effect.Damage.Kick",DevComment="")
GameplayTagList=(Tag="Zeus.State",DevComment="")
GameplayTagList=(Tag="Zeus.State.NoHpRegen",DevComment="")
GameplayTagList=(Tag="Zeus.State.NoRegen",DevComment="")
GameplayTagList=(Tag="Zeus.State.NoSpRegen",DevComment="")
GameplayTagList=(Tag="Zeus.State.NoStaminaRegen",DevComment="")
GameplayTagList=(Tag="Zeus.Status",DevComment="")
GameplayTagList=(Tag="Zeus.Status.Buff",DevComment="")
GameplayTagList=(Tag="Zeus.Status.CC",DevComment="")
GameplayTagList=(Tag="Zeus.Status.CC.Disarmed",DevComment="")
GameplayTagList=(Tag="Zeus.Status.CC.Feared",DevComment="")
GameplayTagList=(Tag="Zeus.Status.CC.Frozen",DevComment="")
GameplayTagList=(Tag="Zeus.Status.CC.KnockedDown",DevComment="")
GameplayTagList=(Tag="Zeus.Status.CC.Rooted",DevComment="")
GameplayTagList=(Tag="Zeus.Status.CC.Silenced",DevComment="")
GameplayTagList=(Tag="Zeus.Status.CC.Stunned",DevComment="")
GameplayTagList=(Tag="Zeus.Status.Dead",DevComment="")
GameplayTagList=(Tag="Zeus.Status.Debuff",DevComment="")
GameplayTagList=(Tag="Zeus.Status.Invulnerable",DevComment="")
GameplayTagList=(Tag="Zeus.Status.Regen",DevComment="")
GameplayTagList=(Tag="Zeus.Status.Regen.Blocked",DevComment="")
GameplayTagList=(Tag="Zeus.Status.Regen.Boosted",DevComment="")
GameplayTagList=(Tag="Zeus.Status.Regen.Poisoned",DevComment="")

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

@@ -0,0 +1,46 @@
// Copyright Zeus Server Engine. All rights reserved.
#include "ZeusAnimNotifyState_BodyHitWindow.h"
#include "Components/SkeletalMeshComponent.h"
#include "GameFramework/Actor.h"
#include "Game/Entity/ZeusCharacter.h"
void UZeusAnimNotifyState_BodyHitWindow::NotifyBegin(USkeletalMeshComponent* MeshComp,
UAnimSequenceBase* /*Animation*/, float /*TotalDuration*/,
const FAnimNotifyEventReference& /*EventReference*/)
{
if (!MeshComp) { return; }
if (AZeusCharacter* Char = Cast<AZeusCharacter>(MeshComp->GetOwner()))
{
Char->SetHitColliderEnabled(Source, /*bEnabled*/ true);
}
}
void UZeusAnimNotifyState_BodyHitWindow::NotifyEnd(USkeletalMeshComponent* MeshComp,
UAnimSequenceBase* /*Animation*/, const FAnimNotifyEventReference& /*EventReference*/)
{
if (!MeshComp) { return; }
if (AZeusCharacter* Char = Cast<AZeusCharacter>(MeshComp->GetOwner()))
{
Char->SetHitColliderEnabled(Source, /*bEnabled*/ false);
}
}
#if WITH_EDITOR
FString UZeusAnimNotifyState_BodyHitWindow::GetNotifyName_Implementation() const
{
const TCHAR* SourceLabel;
switch (Source)
{
case EZeusHitSource::FootR: SourceLabel = TEXT("FootR"); break;
case EZeusHitSource::FootL: SourceLabel = TEXT("FootL"); break;
case EZeusHitSource::HandR: SourceLabel = TEXT("HandR"); break;
case EZeusHitSource::HandL: SourceLabel = TEXT("HandL"); break;
case EZeusHitSource::WeaponR: SourceLabel = TEXT("WeaponR"); break;
case EZeusHitSource::WeaponL: SourceLabel = TEXT("WeaponL"); break;
default: SourceLabel = TEXT("?"); break;
}
return FString::Printf(TEXT("Zeus Body Hit (%s)"), SourceLabel);
}
#endif

View File

@@ -0,0 +1,74 @@
// Copyright Zeus Server Engine. All rights reserved.
#pragma once
#include "CoreMinimal.h"
#include "Animation/AnimNotifies/AnimNotifyState.h"
#include "Game/Entity/ZeusCharacter.h" // EZeusHitSource
#include "ZeusAnimNotifyState_BodyHitWindow.generated.h"
class USkeletalMeshComponent;
class UAnimSequenceBase;
/**
* AnimNotifyState que liga o HitCollider de uma parte do corpo durante a sua
* janela no montage. NotifyBegin liga + NotifyEnd desliga -- define o HIT
* WINDOW do golpe melee/arma branca.
*
* Generaliza para qualquer parte do corpo via EZeusHitSource:
* - FootR / FootL -> chute (capsule attachado em foot_*_Socket)
* - HandR / HandL -> soco (capsule em HandGrip_*)
* - WeaponR / WeaponL -> arma branca (capsule em weapon_*)
*
* === Como usar ===
*
* No editor de um AnimMontage (ex: AM_Kick_Montage):
* 1. Encontrar o frame onde o membro comeca o movimento de impacto.
* 2. Right-click na track de notifies -> Add Notify State -> Zeus Body Hit Window.
* 3. Definir Source = FootR (ou HandR pra soco, WeaponR pra arma, etc).
* 4. Arrastar o range pra cobrir o arco completo do golpe (ex: frame 8 a 18
* em 30fps = ~330ms de janela).
*
* Pq AnimNotifyState e nao AnimNotify pontual:
* - AnimNotify dispara em UM frame -> nao da' pra detectar entradas no
* capsule durante a janela.
* - AnimNotifyState tem NotifyBegin/NotifyTick/NotifyEnd. Pra "ligar
* collider de frame X ate Y", e' o pattern correto.
*
* === Replicacao ===
*
* Roda LOCAL em cada cliente. Cliente do ATIVADOR liga capsule local + reporta
* overlap via opcode MELEE_HIT_REPORT pro server. Server valida e aplica dano +
* dispatcha cue Kick.Target pra todos no AOI do alvo.
*
* Proxies de outros clientes tocam o montage via GameplayCue replicado (visual)
* mas o capsule deles roda no PROXY que e' Pawn local de outra coisa -- nao
* importa, o reportador autoritativo eh quem ativou a ability.
*/
UCLASS(meta = (DisplayName = "Zeus Body Hit Window"))
class ZMMO_API UZeusAnimNotifyState_BodyHitWindow : public UAnimNotifyState
{
GENERATED_BODY()
public:
/// Qual parte do corpo (capsule) tem o hit ligado durante a janela.
/// Default = FootR (chute direito e' o golpe mais comum em montages
/// melee unarmed). Trocar pra HandR pra soco, WeaponR pra arma, etc.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Zeus|Combat")
EZeusHitSource Source = EZeusHitSource::FootR;
// === AnimNotifyState overrides ===
virtual void NotifyBegin(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation,
float TotalDuration, const FAnimNotifyEventReference& EventReference) override;
virtual void NotifyEnd(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation,
const FAnimNotifyEventReference& EventReference) override;
#if WITH_EDITOR
/// Mostra "Zeus Body Hit (FootR/HandL/WeaponR/...)" no editor de montage em
/// vez do nome generico da classe -- ajuda a identificar qual parte sem
/// abrir details.
virtual FString GetNotifyName_Implementation() const override;
#endif
};

View File

@@ -20,9 +20,13 @@
#include "GameplayTagContainer.h" #include "GameplayTagContainer.h"
#include "GameplayTagsManager.h" #include "GameplayTagsManager.h"
#include "ZeusGASComponent.h" #include "ZeusGASComponent.h"
#include "ZeusAOIComponent.h"
#include "ZeusPlayerState.h" #include "ZeusPlayerState.h"
#include "Blueprint/UserWidget.h"
#include "GameFramework/PlayerController.h"
#include "ZeusWorldSubsystem.h" #include "ZeusWorldSubsystem.h"
#include "ZeusNetworkSubsystem.h" #include "ZeusNetworkSubsystem.h"
#include "ZeusNetworkingClientSubsystem.h"
#include "UI/FrontEnd/UIFrontEndFlowSubsystem.h" #include "UI/FrontEnd/UIFrontEndFlowSubsystem.h"
DEFINE_LOG_CATEGORY(LogZeusPlayer); DEFINE_LOG_CATEGORY(LogZeusPlayer);
@@ -57,6 +61,14 @@ AZeusCharacter::AZeusCharacter()
FollowCamera->SetupAttachment(CameraBoom, USpringArmComponent::SocketName); FollowCamera->SetupAttachment(CameraBoom, USpringArmComponent::SocketName);
FollowCamera->bUsePawnControlRotation = false; FollowCamera->bUsePawnControlRotation = false;
// AOI do cliente (+ overlays de debug). Implementa IZeusAOIDebugTarget; o
// Zeus Admin Tools acha este componente no pawn e dirige os overlays.
AOIComponent = CreateDefaultSubobject<UZeusAOIComponent>(TEXT("ZeusAOIComponent"));
// AdminPanelClass NAO e' resolvido aqui via FClassFinder: na criacao do CDO
// (load do modulo) o asset registry pode nao estar pronto e o static cacheia
// null pra sempre. Resolvido lazy em ToggleAdminPanel (LoadClass em runtime).
// AttributeComponent migrou pro PlayerState (AZeusPlayerState + Component // AttributeComponent migrou pro PlayerState (AZeusPlayerState + Component
// Registry config-driven). Pawn fica leve — so' movement/input/camera. // Registry config-driven). Pawn fica leve — so' movement/input/camera.
@@ -161,6 +173,12 @@ void AZeusCharacter::Tick(const float DeltaSeconds)
void AZeusCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent) void AZeusCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{ {
// IMPORTANTE: chamar Super:: pra que BPs filhos consigam auto-bindar
// nodes EnhancedInputAction no Event Graph (ex: GA_Kick / IA_Kick no
// BP_ThirdPersonCharacter). Sem isso, bindings em BP ficam orfaos --
// so' bindings C++ (Move/Look/Jump/Dash abaixo) recebem input.
Super::SetupPlayerInputComponent(PlayerInputComponent);
if (UEnhancedInputComponent* EnhancedInputComponent = Cast<UEnhancedInputComponent>(PlayerInputComponent)) if (UEnhancedInputComponent* EnhancedInputComponent = Cast<UEnhancedInputComponent>(PlayerInputComponent))
{ {
EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Started, this, &AZeusCharacter::OnJumpPressed); EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Started, this, &AZeusCharacter::OnJumpPressed);
@@ -181,6 +199,61 @@ void AZeusCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputCompo
{ {
UE_LOG(LogZeusPlayer, Error, TEXT("'%s' Failed to find an Enhanced Input component."), *GetNameSafe(this)); UE_LOG(LogZeusPlayer, Error, TEXT("'%s' Failed to find an Enhanced Input component."), *GetNameSafe(this));
} }
// Debug: F8 abre/fecha o Zeus Admin Tools (AIO Debug). Legacy BindKey
// convive com Enhanced Input. Trocar a tecla aqui se conflitar.
PlayerInputComponent->BindKey(EKeys::F8, IE_Pressed, this, &AZeusCharacter::ToggleAdminPanel);
}
void AZeusCharacter::ToggleAdminPanel()
{
APlayerController* PC = Cast<APlayerController>(GetController());
if (!PC)
{
return;
}
// Aberto -> fecha + devolve input pro jogo.
if (AdminPanelInstance && AdminPanelInstance->IsInViewport())
{
AdminPanelInstance->RemoveFromParent();
PC->SetInputMode(FInputModeGameOnly());
PC->bShowMouseCursor = false;
return;
}
// Lazy load (runtime): no F8 o asset ja' esta carregavel; evita o pitfall do
// FClassFinder na criacao do CDO. Path do generated class (_C).
// A UI vive no CONTENT DO PLUGIN (mount /ZeusAdminTools/), nao no /Game do projeto:
// o plugin ZeusAdminTools e' autocontido (C++ + UI no repo Server).
if (!AdminPanelClass)
{
AdminPanelClass = LoadClass<UUserWidget>(nullptr,
TEXT("/ZeusAdminTools/UI/WBP_AdminToolsAIO.WBP_AdminToolsAIO_C"));
}
if (!AdminPanelClass)
{
UE_LOG(LogZeusPlayer, Warning, TEXT("[AdminPanel] AdminPanelClass nao resolvido (WBP_AdminToolsAIO ausente?)."));
return;
}
if (!AdminPanelInstance)
{
AdminPanelInstance = CreateWidget<UUserWidget>(PC, AdminPanelClass);
}
if (AdminPanelInstance)
{
AdminPanelInstance->AddToViewport(1000);
// UIOnly: trava a acao do jogo enquanto o menu esta aberto e garante que
// os cliques vao 100% pra UI (no GameAndUI o viewport rouba o clique).
// O mundo continua renderizando + os overlays de debug seguem desenhando.
FInputModeUIOnly Mode;
Mode.SetWidgetToFocus(AdminPanelInstance->TakeWidget());
Mode.SetLockMouseToViewportBehavior(EMouseLockMode::DoNotLock);
PC->SetInputMode(Mode);
PC->bShowMouseCursor = true;
}
} }
void AZeusCharacter::Move(const FInputActionValue& Value) void AZeusCharacter::Move(const FInputActionValue& Value)
@@ -296,7 +369,7 @@ void AZeusCharacter::SetEntityRelevant(bool /*bRelevant*/)
void AZeusCharacter::ResolveZeusNetworkSubsystem() void AZeusCharacter::ResolveZeusNetworkSubsystem()
{ {
if (ZeusNetwork) if (ZeusNetwork && NetClient)
{ {
return; return;
} }
@@ -307,8 +380,15 @@ void AZeusCharacter::ResolveZeusNetworkSubsystem()
return; return;
} }
if (!ZeusNetwork)
{
ZeusNetwork = GI->GetSubsystem<UZeusNetworkSubsystem>(); ZeusNetwork = GI->GetSubsystem<UZeusNetworkSubsystem>();
} }
if (!NetClient)
{
NetClient = GI->GetSubsystem<UZeusNetworkingClientSubsystem>();
}
}
void AZeusCharacter::BindZeusSpawnDelegate() void AZeusCharacter::BindZeusSpawnDelegate()
{ {
@@ -316,49 +396,71 @@ void AZeusCharacter::BindZeusSpawnDelegate()
{ {
return; return;
} }
if (!ZeusNetwork) if (!NetClient && !ZeusNetwork)
{ {
return; return;
} }
ZeusNetwork->OnPlayerSpawned.AddDynamic(this, &AZeusCharacter::HandleZeusPlayerSpawned); // Self-entity (entityId do proprio char) vem do sistema de rede novo.
if (NetClient)
{
NetClient->OnSelfEntityAssigned.AddDynamic(this, &AZeusCharacter::HandleSelfEntityAssigned);
}
// CHAR_INFO (nome/guild) ainda nao tem equivalente no sistema novo -> legacy.
if (ZeusNetwork)
{
ZeusNetwork->OnCharInfoReceived.AddDynamic(this, &AZeusCharacter::HandleZeusCharInfo); ZeusNetwork->OnCharInfoReceived.AddDynamic(this, &AZeusCharacter::HandleZeusCharInfo);
}
bSpawnDelegateBound = true; bSpawnDelegateBound = true;
} }
void AZeusCharacter::UnbindZeusSpawnDelegate() void AZeusCharacter::UnbindZeusSpawnDelegate()
{ {
if (!bSpawnDelegateBound || !ZeusNetwork) if (!bSpawnDelegateBound)
{ {
bSpawnDelegateBound = false;
return; return;
} }
ZeusNetwork->OnPlayerSpawned.RemoveDynamic(this, &AZeusCharacter::HandleZeusPlayerSpawned); if (NetClient)
{
NetClient->OnSelfEntityAssigned.RemoveDynamic(this, &AZeusCharacter::HandleSelfEntityAssigned);
}
if (ZeusNetwork)
{
ZeusNetwork->OnCharInfoReceived.RemoveDynamic(this, &AZeusCharacter::HandleZeusCharInfo); ZeusNetwork->OnCharInfoReceived.RemoveDynamic(this, &AZeusCharacter::HandleZeusCharInfo);
}
bSpawnDelegateBound = false; bSpawnDelegateBound = false;
} }
void AZeusCharacter::TryRegisterLocalEntityFromCachedSpawn() void AZeusCharacter::TryRegisterLocalEntityFromCachedSpawn()
{ {
if (!ZeusNetwork) // Self-entity vem do sistema de rede novo (ENT_SELF). O subsystem e'
// per-GameInstance e sobrevive ao OpenLevel, entao o entityId pode ja' ter
// chegado antes deste pawn existir -- puxa o cache agora. Caso ainda nao
// tenha chegado, o bind em OnSelfEntityAssigned cobre quando chegar.
// A pos/yaw vem do proprio pawn (ja posicionado via PendingSpawnPose do DB);
// o sistema novo so' carrega o entityId.
if (NetClient)
{ {
return; const int64 SelfId = NetClient->GetLocalEntityId();
if (SelfId != 0)
{
HandleLocalSpawnReady(SelfId, GetActorLocation(), GetActorRotation().Yaw, 0);
}
} }
int64 CachedEntityId = 0; // Race fix CHAR_INFO (ainda legacy): o S_CHAR_INFO pode ter chegado antes do
FVector CachedPosCm = FVector::ZeroVector; // pawn existir; aplica o ultimo nome/guild cacheado.
float CachedYawDeg = 0.0f;
int64 CachedServerTimeMs = 0;
if (ZeusNetwork->TryGetLastLocalSpawn(CachedEntityId, CachedPosCm, CachedYawDeg, CachedServerTimeMs))
{
HandleLocalSpawnReady(CachedEntityId, CachedPosCm, CachedYawDeg, CachedServerTimeMs);
}
// Race fix: o S_CHAR_INFO chega ANTES de S_SPAWN_PLAYER mas o pawn pode
// nao existir ainda (OpenLevel em andamento). Aplica o ultimo cacheado.
TryApplyCachedCharInfo(); TryApplyCachedCharInfo();
} }
void AZeusCharacter::HandleSelfEntityAssigned(const int64 InEntityId)
{
// Sistema novo informou o entityId do proprio char (ENT_SELF). Reusa o mesmo
// caminho do spawn local legacy (seta ZeusEntityId + RegisterLocalEntity).
// pos/yaw vem do pawn (ja posicionado); o sistema novo so' traz o entityId.
HandleLocalSpawnReady(InEntityId, GetActorLocation(), GetActorRotation().Yaw, 0);
}
void AZeusCharacter::HandleZeusPlayerSpawned(const int64 InEntityId, const bool bIsLocal, void AZeusCharacter::HandleZeusPlayerSpawned(const int64 InEntityId, const bool bIsLocal,
const FVector PosCm, const float YawDeg, const int64 ServerTimeMs) const FVector PosCm, const float YawDeg, const int64 ServerTimeMs)
{ {
@@ -432,6 +534,17 @@ void AZeusCharacter::HandleZeusCharInfo(const int64 InEntityId, const FString& C
TEXT("AZeusCharacter: S_CHAR_INFO recebido EntityId=%lld name=%s guild=%s"), TEXT("AZeusCharacter: S_CHAR_INFO recebido EntityId=%lld name=%s guild=%s"),
InEntityId, *CharName, *GuildName); InEntityId, *CharName, *GuildName);
// 2026-06-03 fix: server envia S_CHAR_INFO de TODOS os players (proprio +
// catch-up de proxies pre-existentes). Sem este filtro, o nome do ultimo
// proxy recebido sobrescrevia o do pawn local (ex: player Mateuus loga e
// ve "Olatudook" em si mesmo porque o catch-up do Olatudook chegou depois
// do S_CHAR_INFO proprio). Proxies remotos sao roteados pelo registry no
// UZeusWorldSubsystem (futuro nameplate por EntityId).
if (ZeusEntityId != 0 && InEntityId != ZeusEntityId)
{
return;
}
APlayerState* PS = GetPlayerState(); APlayerState* PS = GetPlayerState();
if (!PS) if (!PS)
{ {
@@ -471,7 +584,10 @@ void AZeusCharacter::TryApplyCachedCharInfo()
void AZeusCharacter::FlushInputAxisToServer(const float DeltaSeconds) void AZeusCharacter::FlushInputAxisToServer(const float DeltaSeconds)
{ {
ResolveZeusNetworkSubsystem(); ResolveZeusNetworkSubsystem();
if (!ZeusNetwork || !ZeusNetwork->IsConnected()) // Sistema de rede novo (canonico) tem prioridade; legacy e' fallback.
const bool bV1 = (NetClient
&& NetClient->GetConnectionState() == EZeusV1ConnectionState::Accepted);
if (!bV1 && (!ZeusNetwork || !ZeusNetwork->IsConnected()))
{ {
return; return;
} }
@@ -480,7 +596,19 @@ void AZeusCharacter::FlushInputAxisToServer(const float DeltaSeconds)
SendAccumulatorSec += DeltaSeconds; SendAccumulatorSec += DeltaSeconds;
TimeSinceLastSendSec += DeltaSeconds; TimeSinceLastSendSec += DeltaSeconds;
const bool bMovingInput = !FMath::IsNearlyZero(PendingMoveForward) || !FMath::IsNearlyZero(PendingMoveRight); // Movimento p/ efeitos de rate NAO e' so' o input (W/A/S/D): o CMC do dono
// continua desacelerando (braking) por ~0.3s depois de soltar a tecla. Se
// pararmos de enviar no rate rapido assim que o input zera, o servidor fica
// congelado na ultima `simpleVelCmS` alta e o proxy remoto extrapola ~1m alem
// (NewPos = Last.Pos + Last.Vel*ExtrapSec) ate o heartbeat corrigir -> overshoot
// + snap-back ("velocidade grande que demora a zerar" + jitter). Tratar a
// velocidade residual como movimento mantem o envio a InputSendRateHz ate o
// dono realmente parar, alimentando o proxy com a curva de desaceleracao.
const bool bHasInputAxis = !FMath::IsNearlyZero(PendingMoveForward) || !FMath::IsNearlyZero(PendingMoveRight);
const FVector OwnerVelNow = GetVelocity();
constexpr float kResidualSpeedSqCmS = 25.0f * 25.0f; // abaixo de 25 cm/s tratamos como parado
const bool bHasResidualVel = OwnerVelNow.SizeSquared() > kResidualSpeedSqCmS;
const bool bMovingInput = bHasInputAxis || bHasResidualVel;
const bool bJumpEvent = bPendingJumpPressed || bPendingJumpReleased; const bool bJumpEvent = bPendingJumpPressed || bPendingJumpReleased;
const bool bRateReached = SendAccumulatorSec >= SendInterval; const bool bRateReached = SendAccumulatorSec >= SendInterval;
const bool bHeartbeatReached = TimeSinceLastSendSec >= HeartbeatIntervalSec; const bool bHeartbeatReached = TimeSinceLastSendSec >= HeartbeatIntervalSec;
@@ -494,6 +622,23 @@ void AZeusCharacter::FlushInputAxisToServer(const float DeltaSeconds)
const bool bFallingChanged = (bIsFalling != bPreviousFalling); const bool bFallingChanged = (bIsFalling != bPreviousFalling);
const bool bShouldSend = bJumpEvent || bFallingChanged || (bMovingInput && bRateReached) || (!bMovingInput && bHeartbeatReached); const bool bShouldSend = bJumpEvent || bFallingChanged || (bMovingInput && bRateReached) || (!bMovingInput && bHeartbeatReached);
// DEBUG-INPUT (2026-06-12): log ~1x/s COM o PIE. Remover apos achar o gap.
{
static float DbgInputAcc = 0.0f;
DbgInputAcc += DeltaSeconds;
if (DbgInputAcc >= 1.0f)
{
DbgInputAcc = 0.0f;
const int32 PieId = (GetWorld() && GetWorld()->GetOutermost())
? GetWorld()->GetOutermost()->GetPIEInstanceID() : -1;
UE_LOG(LogZeusPlayer, Warning,
TEXT("[PIE%d][InputDbg] bV1=%d state=%d moving=%d rate=%d should=%d hbReached=%d fwd=%.2f right=%.2f"),
PieId, bV1 ? 1 : 0,
NetClient ? static_cast<int32>(NetClient->GetConnectionState()) : -99,
bMovingInput ? 1 : 0, bRateReached ? 1 : 0, bShouldSend ? 1 : 0,
bHeartbeatReached ? 1 : 0, PendingMoveForward, PendingMoveRight);
}
}
if (!bShouldSend) if (!bShouldSend)
{ {
return; return;
@@ -501,18 +646,55 @@ void AZeusCharacter::FlushInputAxisToServer(const float DeltaSeconds)
++InputSequence; ++InputSequence;
const int32 ClientTimeMs = static_cast<int32>(FPlatformTime::Seconds() * 1000.0); const int32 ClientTimeMs = static_cast<int32>(FPlatformTime::Seconds() * 1000.0);
// ADR 0040: yaw da camara/controller para que o servidor consiga rotar o // YAW DO CORPO (nao da mira): GetActorRotation().Yaw reflete o orient-to-movement
// input (referencial-camara) para velocidade mundial. Fallback para o yaw // do CMC (bOrientRotationToMovement=true, RotationRate=(0,500,0)) -> o corpo vira
// do actor se nao houver controller (e.g. tela de loading). // pra direcao do MOVIMENTO (WASD), suave a 500 deg/s, e o MOUSE nao gira o corpo
const float ViewYawDeg = (GetController() // (bUseControllerRotationYaw=false). O servidor usa este yaw direto e o proxy nos
? static_cast<float>(GetController()->GetControlRotation().Yaw) // outros clientes o replica -> o proxy vira IDENTICO ao corpo do dono.
: static_cast<float>(GetActorRotation().Yaw)); //
// (Historico bug 2026-06-13: antes mandava GetControlRotation().Yaw -- a MIRA --
// e o servidor fazia atan2(vel) INSTANTANEO quando movia (proxy "virava demais"
// 45 graus) e usava a mira quando parado (proxy seguia o mouse). ADR 0040: o
// servidor NAO rotaciona mais o input por este yaw (ADR 0041 manda pos+vel
// mundiais), entao trocar a mira pelo corpo e' seguro. Quando precisar da MIRA
// real (aim/combate), adicionar um campo aimYawDeg dedicado -- nao reusar este.)
const float BodyYawDeg = static_cast<float>(GetActorRotation().Yaw);
// ADR 0041: cliente autoritativo da posicao XYZ + velocidade XY. O CMC ja // ADR 0041: cliente autoritativo da posicao XYZ + velocidade XY. O CMC ja
// integrou o input local antes deste flush, portanto `GetActorLocation` e // integrou o input local antes deste flush, portanto `GetActorLocation` e
// `GetVelocity` reflectem o estado real que o servidor deve replicar para // `GetVelocity` reflectem o estado real que o servidor deve replicar para
// os outros clientes (sujeito ao clamp anti-cheat). // os outros clientes (sujeito ao clamp anti-cheat).
const FVector PosCm = GetActorLocation(); const FVector PosCm = GetActorLocation();
const FVector Vel = GetVelocity(); const FVector Vel = GetVelocity();
if (bV1)
{
// Sistema novo: INPUT 6077 com a pose absoluta (ADR 0040/0041).
const bool bSent = NetClient->EmitInput(
PendingMoveForward,
PendingMoveRight,
bPendingJumpPressed,
bPendingJumpReleased,
InputSequence,
ClientTimeMs,
BodyYawDeg,
PosCm,
FVector2D(Vel.X, Vel.Y),
bIsFalling,
static_cast<float>(Vel.Z));
// DEBUG-INPUT (2026-06-12): confirma chamada + retorno do SendPacket.
static float DbgEmitAcc = 0.0f;
DbgEmitAcc += DeltaSeconds;
if (DbgEmitAcc >= 1.0f)
{
DbgEmitAcc = 0.0f;
const int32 PieId = (GetWorld() && GetWorld()->GetOutermost())
? GetWorld()->GetOutermost()->GetPIEInstanceID() : -1;
UE_LOG(LogZeusPlayer, Warning,
TEXT("[PIE%d][InputDbg] EmitInput seq=%d sent=%d pos=%s"),
PieId, InputSequence, bSent ? 1 : 0, *PosCm.ToString());
}
}
else if (ZeusNetwork)
{
ZeusNetwork->SendInputAxis( ZeusNetwork->SendInputAxis(
PendingMoveForward, PendingMoveForward,
PendingMoveRight, PendingMoveRight,
@@ -520,11 +702,12 @@ void AZeusCharacter::FlushInputAxisToServer(const float DeltaSeconds)
bPendingJumpReleased, bPendingJumpReleased,
InputSequence, InputSequence,
ClientTimeMs, ClientTimeMs,
ViewYawDeg, BodyYawDeg,
PosCm, PosCm,
FVector2D(Vel.X, Vel.Y), FVector2D(Vel.X, Vel.Y),
bIsFalling, bIsFalling,
static_cast<float>(Vel.Z)); static_cast<float>(Vel.Z));
}
SendAccumulatorSec = 0.0f; SendAccumulatorSec = 0.0f;
TimeSinceLastSendSec = 0.0f; TimeSinceLastSendSec = 0.0f;

View File

@@ -11,6 +11,9 @@ class USpringArmComponent;
class UCameraComponent; class UCameraComponent;
class UInputAction; class UInputAction;
class UZeusNetworkSubsystem; class UZeusNetworkSubsystem;
class UZeusNetworkingClientSubsystem;
class UZeusAOIComponent;
class UUserWidget;
struct FInputActionValue; struct FInputActionValue;
DECLARE_LOG_CATEGORY_EXTERN(LogZeusPlayer, Log, All); DECLARE_LOG_CATEGORY_EXTERN(LogZeusPlayer, Log, All);
@@ -46,6 +49,10 @@ class ZMMO_API AZeusCharacter : public ACharacter, public IZeusEntityInterface
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components", meta = (AllowPrivateAccess = "true")) UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components", meta = (AllowPrivateAccess = "true"))
UCameraComponent* FollowCamera; UCameraComponent* FollowCamera;
/** AOI do cliente (+ overlays de debug dirigidos pelo Zeus Admin Tools). */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Zeus|AOI", meta = (AllowPrivateAccess = "true"))
TObjectPtr<UZeusAOIComponent> AOIComponent;
// GAS Component vive no PlayerState (AZeusPlayerState + Component Registry). // GAS Component vive no PlayerState (AZeusPlayerState + Component Registry).
// Acesso: GetPlayerState<AZeusPlayerState>()->GetZeusComponent<UZeusGASComponent>(). // Acesso: GetPlayerState<AZeusPlayerState>()->GetZeusComponent<UZeusGASComponent>().
@@ -67,6 +74,11 @@ protected:
UPROPERTY(EditAnywhere, Category = "Input") UPROPERTY(EditAnywhere, Category = "Input")
UInputAction* DashAction; UInputAction* DashAction;
/** Painel Zeus Admin Tools (AIO Debug), aberto/fechado por F8. Default =
* WBP_AdminToolsAIO (resolvido no construtor). */
UPROPERTY(EditAnywhere, Category = "Zeus|Admin")
TSubclassOf<UUserWidget> AdminPanelClass;
/** Cadencia de envio dos pacotes `C_INPUT_AXIS` (Hz). */ /** Cadencia de envio dos pacotes `C_INPUT_AXIS` (Hz). */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Zeus|Networking", meta = (ClampMin = "1", ClampMax = "120")) UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Zeus|Networking", meta = (ClampMin = "1", ClampMax = "120"))
int32 InputSendRateHz = 30; int32 InputSendRateHz = 30;
@@ -114,6 +126,9 @@ protected:
/// + responde S_ABILITY_ACTIVATED (HP/SP atualizado, montage roda local). /// + responde S_ABILITY_ACTIVATED (HP/SP atualizado, montage roda local).
void OnDashTriggered(); void OnDashTriggered();
/** F8: cria/mostra ou esconde o painel Zeus Admin Tools (input mode UI + cursor). */
void ToggleAdminPanel();
public: public:
/** Wrappers expostos a Blueprint (UI / mobile / automation). */ /** Wrappers expostos a Blueprint (UI / mobile / automation). */
UFUNCTION(BlueprintCallable, Category = "Input") UFUNCTION(BlueprintCallable, Category = "Input")
@@ -150,6 +165,11 @@ private:
UFUNCTION() UFUNCTION()
void HandleZeusPlayerSpawned(int64 InEntityId, bool bIsLocal, FVector PosCm, float YawDeg, int64 ServerTimeMs); void HandleZeusPlayerSpawned(int64 InEntityId, bool bIsLocal, FVector PosCm, float YawDeg, int64 ServerTimeMs);
// Self-entity do sistema de rede novo (ENT_SELF): o cliente aprende o
// proprio entityId. Substitui o caminho legacy OnPlayerSpawned(bIsLocal=true).
UFUNCTION()
void HandleSelfEntityAssigned(int64 InEntityId);
UFUNCTION() UFUNCTION()
void HandleZeusCharInfo(int64 InEntityId, const FString& CharName, const FString& GuildName); void HandleZeusCharInfo(int64 InEntityId, const FString& CharName, const FString& GuildName);
@@ -158,6 +178,14 @@ private:
UPROPERTY() UPROPERTY()
TObjectPtr<UZeusNetworkSubsystem> ZeusNetwork; TObjectPtr<UZeusNetworkSubsystem> ZeusNetwork;
// Sistema de rede novo (canonico): fonte do self-entity (ENT_SELF).
UPROPERTY()
TObjectPtr<UZeusNetworkingClientSubsystem> NetClient;
/** Instancia viva do painel admin (criada lazy no primeiro F8). */
UPROPERTY(Transient)
TObjectPtr<UUserWidget> AdminPanelInstance;
bool bSpawnDelegateBound = false; bool bSpawnDelegateBound = false;
/** Identidade autoritativa atribuida quando o servidor envia S_SPAWN_PLAYER local. */ /** Identidade autoritativa atribuida quando o servidor envia S_SPAWN_PLAYER local. */

View File

@@ -132,11 +132,18 @@ void AZeusPlayerProxy::Tick(const float DeltaSeconds)
// conhecida, clampada por `MaxExtrapolationSeconds` para nao deixar // conhecida, clampada por `MaxExtrapolationSeconds` para nao deixar
// o proxy fugir se a rede ficar muda. // o proxy fugir se a rede ficar muda.
const FZeusProxySnapshot& Last = SnapshotBuffer.Last(); const FZeusProxySnapshot& Last = SnapshotBuffer.Last();
const float ExtrapSec = FMath::Clamp( const float RawExtrapSec =
static_cast<float>(RenderMs - Last.ServerTimeMs) / 1000.0f, static_cast<float>(RenderMs - Last.ServerTimeMs) / 1000.0f;
0.0f, MaxExtrapolationSeconds); const float ExtrapSec = FMath::Clamp(RawExtrapSec, 0.0f, MaxExtrapolationSeconds);
NewPos = Last.PosCm + Last.VelCmS * ExtrapSec; NewPos = Last.PosCm + Last.VelCmS * ExtrapSec;
NewVel = Last.VelCmS; // STARVATION (buffer=1 por muito tempo -- ex: 1o login sem DELTA_BATCH por
// segundos): a extrapolacao satura no clamp e a POSE congela, mas manter a
// velocidade faz o AnimBP rodar locomocao PARADO ("andar no lugar"/moonwalk
// = o "jitter" reportado). Se ja passamos bem do clamp, zera a vel visual
// pra o proxy ir pra Idle ate' os deltas voltarem (em vez de animar
// andando travado). Quando os deltas chegam, o ramo de interpolacao retoma.
const bool bStarved = RawExtrapSec > (MaxExtrapolationSeconds + 0.15f);
NewVel = bStarved ? FVector::ZeroVector : Last.VelCmS;
bExtrapolating = ExtrapSec > KINDA_SMALL_NUMBER; bExtrapolating = ExtrapSec > KINDA_SMALL_NUMBER;
} }
else else
@@ -150,12 +157,32 @@ void AZeusPlayerProxy::Tick(const float DeltaSeconds)
NewVel = FMath::Lerp(A.VelCmS, B.VelCmS, InterpAlpha); NewVel = FMath::Lerp(A.VelCmS, B.VelCmS, InterpAlpha);
} }
// Yaw deriva da velocidade interpolada: locomotion mantem face na direcao // ORIENTACAO DO CORPO = REPLICA do yaw autoritativo do servidor (Snapshot.YawDeg),
// do movimento; em idle preserva o yaw atual para nao "snappar". // interpolado shortest-path entre os MESMOS A/B da posicao. Esse yaw agora carrega
// o yaw do CORPO do dono (orient-to-movement do CMC: vira pra direcao do MOVIMENTO
// /WASD, suave a 500 deg/s; parado mantem; o MOUSE nao gira o corpo) -- o cliente
// dono manda GetActorRotation().Yaw e o servidor repassa direto. Por isso o proxy
// so' REPLICA: vira identico ao corpo do dono, sem re-derivar nada.
//
// NAO derivamos de Atan2(velocidade): isso era a fonte do bug 2026-06-13 ("virava
// demais 45 graus") -- a direcao da velocidade salta, o yaw do corpo nao. A
// suavizacao correta ja' aconteceu no CMC do dono; aqui so' interpolamos amostras.
//
// FUTURO -- Motion Matching/aim: quando "a mira dita a locomocao" (strafe/aim
// offset), o servidor passara a mandar um aimYawDeg separado e o AnimBP usara a
// velocidade relativa a este yaw pra escolher fwd/back/left/right. Por ORA, 1 yaw.
FRotator NewRot = GetActorRotation(); FRotator NewRot = GetActorRotation();
if (FVector(NewVel.X, NewVel.Y, 0.0f).SizeSquared() > 1.0f) if (IdxB == INDEX_NONE)
{ {
NewRot.Yaw = FMath::RadiansToDegrees(FMath::Atan2(NewVel.Y, NewVel.X)); NewRot.Yaw = SnapshotBuffer.Last().YawDeg; // extrapolando: segura o ultimo yaw
}
else
{
const float YawA = SnapshotBuffer[IdxB - 1].YawDeg;
const float YawB = SnapshotBuffer[IdxB].YawDeg;
// FindDeltaAngleDegrees -> menor arco (-180..180); evita girar "o lado longo".
const float DeltaYaw = FMath::FindDeltaAngleDegrees(YawA, YawB);
NewRot.Yaw = YawA + DeltaYaw * InterpAlpha;
} }
NewRot.Pitch = 0.0f; NewRot.Pitch = 0.0f;
NewRot.Roll = 0.0f; NewRot.Roll = 0.0f;
@@ -207,10 +234,26 @@ void AZeusPlayerProxy::Tick(const float DeltaSeconds)
LastDiagLogSec = NowSec; LastDiagLogSec = NowSec;
const int64 NewestMs = SnapshotBuffer.Last().ServerTimeMs; const int64 NewestMs = SnapshotBuffer.Last().ServerTimeMs;
const int64 RenderLagMs = ServerNowMs - NewestMs; const int64 RenderLagMs = ServerNowMs - NewestMs;
UE_LOG(LogZMMO, Verbose, // === DIAG PERMANENTE -- jitter por FOME DE DELTAS ========================
TEXT("ZeusPlayerProxy[%lld] buffer=%d renderLagMs=%lld interpAlpha=%.2f extrap=%d delayMs=%.0f"), // Bug investigado/resolvido 2026-06-13 (ver memoria
EntityId, SnapshotBuffer.Num(), RenderLagMs, InterpAlpha, // project_proxy_delta_starvation_jitter). Este Warning FICA DE PROPOSITO
bExtrapolating ? 1 : 0, InterpolationDelayMs); // (decisao do dono: NAO rebaixar p/ Verbose nem remover) -- e' a sonda pra
// cacar regressao do jitter de proxy. Como ler:
// secsSinceSnap = ha quanto tempo o proxy NAO recebe snapshot. Sobe = FOME
// (player remoto parado -> ComputeDelta suprime no server -> sem DELTA_BATCH).
// newestMs = serverTimeMs do ultimo snapshot; CONGELA na fome (deveria avancar).
// renderLagMs = ServerNowMs - newestMs; cresce 1:1 com o wall-clock na fome.
// speed=0 na fome = a mitigacao (zera a vel visual -> Idle, sem "moonwalk") agindo.
// Ao o player voltar a se mover sai "buffer STALE ... reset+reseed" (anti-stale)
// e renderLagMs/secsSinceSnap voltam a ~0 (snap limpo, sem salto/jitter).
// =========================================================================
const double SecsSinceSnap =
(LastSnapshotRecvSec > 0.0) ? (NowSec - LastSnapshotRecvSec) : -1.0;
UE_LOG(LogZMMO, Warning,
TEXT("ZeusPlayerProxy[%lld] buffer=%d renderLagMs=%lld newestMs=%lld secsSinceSnap=%.1f interpAlpha=%.2f extrap=%d speed=%.0f"),
EntityId, SnapshotBuffer.Num(), RenderLagMs, NewestMs, SecsSinceSnap, InterpAlpha,
bExtrapolating ? 1 : 0,
FVector(VisualVelocity.X, VisualVelocity.Y, 0.0f).Size());
} }
} }
@@ -222,18 +265,65 @@ void AZeusPlayerProxy::SetZeusIdentity(const int64 InEntityId, const EZeusEntity
void AZeusPlayerProxy::ApplyEntitySnapshot(const FZeusEntitySnapshot& Snapshot) void AZeusPlayerProxy::ApplyEntitySnapshot(const FZeusEntitySnapshot& Snapshot)
{ {
// Estabiliza o offset de relogio na primeira amostra. Subsequente nao LastSnapshotRecvSec = FPlatformTime::Seconds(); // DIAG: marca chegada (mede a fome no Tick)
// re-sincroniza para evitar saltos visuais; um esquema mais sofisticado
// (ema do offset) cabe quando tivermos ping/RTT estabilizado por sessao. // 2026-06-06 H3 fix — disciplina o ServerClockOffsetMs via EMA com clamp.
// Antes: offset congelado na 1a amostra. Em cross-server handoff a fonte
// do ServerTimeMs muda (publisher antigo -> novo) e o offset fica
// permanentemente skewed, alimentando tremor pos-handoff e drift cumulativo
// de clock cliente vs server. Agora cada amostra recalcula candidato e
// suaviza (alpha=0.02, ~50 amostras pra convergir) com clamp max 2ms/snapshot
// pra evitar salto visual durante transicao.
const int64 LocalNowMs = static_cast<int64>(FPlatformTime::Seconds() * 1000.0);
const int64 OffsetCandidate = LocalNowMs - Snapshot.ServerTimeMs;
// Anti-stale (2026-06-13): se este snapshot esta MUITO a frente do mais novo
// do buffer, o proxy ficou "mudo" por segundos -- player remoto parado ->
// ComputeDelta suprime -> sem DELTA_BATCH novo -> renderLagMs disparou pra
// dezenas de segundos. Manter os snapshots velhos faz a interpolacao usar um
// span gigante (pose de ~100s atras -> agora) quando o player volta a se mover,
// causando rasteio/salto (o jitter reportado). Descarta o buffer obsoleto +
// re-bootstrapa o relogio pra a interpolacao recomecar limpa neste keyframe.
if (SnapshotBuffer.Num() > 0
&& (Snapshot.ServerTimeMs - SnapshotBuffer.Last().ServerTimeMs) > 1000)
{
UE_LOG(LogZMMO, Warning,
TEXT("ZeusPlayerProxy[%lld] buffer STALE gap=%lldms -> reset+reseed"),
EntityId, Snapshot.ServerTimeMs - SnapshotBuffer.Last().ServerTimeMs);
SnapshotBuffer.Reset();
ServerClockOffsetMs = OffsetCandidate;
}
if (SnapshotBuffer.Num() == 0) if (SnapshotBuffer.Num() == 0)
{ {
const int64 LocalNowMs = static_cast<int64>(FPlatformTime::Seconds() * 1000.0); // Primeira amostra: bootstrap direto (nao tem baseline pra suavizar).
ServerClockOffsetMs = LocalNowMs - Snapshot.ServerTimeMs; ServerClockOffsetMs = OffsetCandidate;
// DIAG (jitter 1o login): confirma o seed do relogio. serverTimeMs deve ser
// != 0 e o offset razoavel; se vier 0 aqui, o keyframe/catch-up corrompeu o
// bootstrap (hipotese descartada, mas o log fecha a questao em campo).
UE_LOG(LogZMMO, Warning,
TEXT("ZeusPlayerProxy[%lld] SEED clock serverTimeMs=%lld localNowMs=%lld offset=%lld"),
EntityId, Snapshot.ServerTimeMs, LocalNowMs, ServerClockOffsetMs);
}
else
{
const double Alpha = 0.02;
const int64 RawTarget = static_cast<int64>(
static_cast<double>(ServerClockOffsetMs) * (1.0 - Alpha)
+ static_cast<double>(OffsetCandidate) * Alpha);
// Clamp max 2ms por amostra — em 30Hz isso converge a 60ms/s, mais que
// o suficiente pra absorver drift normal sem causar tremor visual.
const int64 Delta = RawTarget - ServerClockOffsetMs;
const int64 ClampedDelta = (Delta > 2) ? 2
: (Delta < -2) ? -2
: Delta;
ServerClockOffsetMs += ClampedDelta;
} }
FZeusProxySnapshot S; FZeusProxySnapshot S;
S.PosCm = Snapshot.PositionCm; S.PosCm = Snapshot.PositionCm;
S.VelCmS = Snapshot.VelocityCmS; S.VelCmS = Snapshot.VelocityCmS;
S.YawDeg = Snapshot.YawDeg; // yaw do CORPO do dono (orient-to-movement; mouse nao gira)
S.bGrounded = Snapshot.bGrounded; S.bGrounded = Snapshot.bGrounded;
S.ServerTimeMs = Snapshot.ServerTimeMs; S.ServerTimeMs = Snapshot.ServerTimeMs;

View File

@@ -42,6 +42,15 @@ struct FZeusProxySnapshot
UPROPERTY() UPROPERTY()
FVector VelCmS = FVector::ZeroVector; FVector VelCmS = FVector::ZeroVector;
/** Yaw autoritativo do servidor (graus) = o MESMO yaw que o player LOCAL aplicou
* ao virar. O proxy replica EXATAMENTE a viradinha do local (apertou D = mesma
* virada nos dois). Sem armazenar isto, o proxy derivava o yaw da velocidade
* (Atan2) e "virava totalmente pra direcao do movimento" (bug 2026-06-13).
* NAO confundir com "mira dita a locomocao" (strafe / Motion Matching) -- isso
* esta' DESABILITADO por ora (ver AVISO em ZeusPlayerProxy.cpp::Tick). */
UPROPERTY()
float YawDeg = 0.0f;
UPROPERTY() UPROPERTY()
bool bGrounded = true; bool bGrounded = true;
@@ -140,6 +149,12 @@ protected:
*/ */
int64 ServerClockOffsetMs = 0; int64 ServerClockOffsetMs = 0;
/** DIAG (2026-06-13): instante (s, monotonic) do ultimo snapshot recebido em
* ApplyEntitySnapshot. Usado no log do Tick pra medir "secsSinceSnap" = quanto
* tempo o proxy ficou MUDO (sem delta). secsSinceSnap grande == fome (player
* remoto parado -> ComputeDelta suprime). 0 = nenhum snapshot ainda. */
double LastSnapshotRecvSec = 0.0;
/** Aceleracao derivada (com smoothing) escrita no CMC custom para alimentar /** Aceleracao derivada (com smoothing) escrita no CMC custom para alimentar
* o AnimBP (`ShouldMove`/`Jump_Start` exigem Acceleration!=0). */ * o AnimBP (`ShouldMove`/`Jump_Start` exigem Acceleration!=0). */
FVector LastDerivedAccelerationCmS2 = FVector::ZeroVector; FVector LastDerivedAccelerationCmS2 = FVector::ZeroVector;

View File

@@ -5,10 +5,47 @@
#include "ZeusPlayerController.h" #include "ZeusPlayerController.h"
#include "ZeusPlayerState.h" #include "ZeusPlayerState.h"
#include "UObject/ConstructorHelpers.h"
AZeusGameMode::AZeusGameMode() AZeusGameMode::AZeusGameMode()
{
// DefaultPawnClass: usar o BP filho (BP_ThirdPersonCharacter) em vez do
// C++ puro. BPs filhos so' sao instanciados se o pawn possessed for da
// classe BP -- caso contrario o engine cria a classe C++ direta e perde
// TUDO que o designer fez no BP (EnhancedInputAction events, AnimBP wire,
// hooks de ability, componentes adicionados via editor).
//
// Fallback pra AZeusCharacter pura se o asset BP nao existir (asset
// renomeado/deletado por engano -- nao deve acontecer em prod).
static ConstructorHelpers::FClassFinder<APawn> DefaultPawnBP(
TEXT("/Game/ZMMO/Core/Player/BP_ZeusPlayerCharacter"));
if (DefaultPawnBP.Succeeded())
{
DefaultPawnClass = DefaultPawnBP.Class;
}
else
{ {
DefaultPawnClass = AZeusCharacter::StaticClass(); DefaultPawnClass = AZeusCharacter::StaticClass();
}
PlayerControllerClass = AZeusPlayerController::StaticClass(); PlayerControllerClass = AZeusPlayerController::StaticClass();
// PlayerStateClass: usar BP filho pelo mesmo motivo do DefaultPawnClass --
// componentes BP-added (ex: UZeusGASComponent listado em DefaultGame.ini
// ComponentClasses), eventos no Event Graph e overrides do designer so'
// existem na classe BP. C++ direto (AZeusPlayerState) perde tudo isso.
// TODO: renomear o asset pra BP_ZeusPlayerState pra alinhar com a convencao
// (BP_ZeusPlayerCharacter) -- quando renomear, atualizar o path aqui.
static ConstructorHelpers::FClassFinder<APlayerState> PlayerStateBP(
TEXT("/Game/ZMMO/Core/Player/BP_PlayerState"));
if (PlayerStateBP.Succeeded())
{
PlayerStateClass = PlayerStateBP.Class;
}
else
{
PlayerStateClass = AZeusPlayerState::StaticClass(); PlayerStateClass = AZeusPlayerState::StaticClass();
}
HUDClass = AZeusHUD::StaticClass(); HUDClass = AZeusHUD::StaticClass();
} }

View File

@@ -0,0 +1,419 @@
// Copyright Zeus Server Engine. All rights reserved.
#include "ZeusAOIComponent.h"
#include "ZeusNetworkingClientSubsystem.h" // V1 canonico: OnDebugAoiInfo (6161) + EmitDebugAoiRequest (6160)
#include "ZeusFrontierOverlayActor.h" // ator que desenha as paredes de fronteira
#include "ZMMONetLog.h" // Batch 2.5: LogZeusAOI (categoria do modulo ZMMO)
#include "DrawDebugHelpers.h"
#include "Engine/GameInstance.h"
#include "Engine/World.h"
#include "GameFramework/Actor.h"
// Console vars — espelham as flags do painel (overlay liga se flag OU CVar).
static TAutoConsoleVariable<int32> CVarShowAOI(
TEXT("zeus.debug.aoi"), 0, TEXT("Desenha a Zona de Interesse (esfera do raio AOI)."), ECVF_Default);
static TAutoConsoleVariable<int32> CVarShowDespawn(
TEXT("zeus.debug.despawn"), 0, TEXT("Desenha a Zona de Despawn (esfera externa do AOI)."), ECVF_Default);
static TAutoConsoleVariable<int32> CVarShowCell(
TEXT("zeus.debug.cell"), 0, TEXT("Desenha os bounds da celula (placeholder)."), ECVF_Default);
static TAutoConsoleVariable<int32> CVarShowProxy(
TEXT("zeus.debug.proxy"), 0, TEXT("Desenha labels de proxy (TODO)."), ECVF_Default);
static TAutoConsoleVariable<int32> CVarShowHandoff(
TEXT("zeus.debug.handoff"), 0, TEXT("Desenha a linha de handoff (placeholder)."), ECVF_Default);
static TAutoConsoleVariable<int32> CVarShowDrift(
TEXT("zeus.debug.drift"), 0, TEXT("Desenha o drift autoritativo (TODO)."), ECVF_Default);
static TAutoConsoleVariable<int32> CVarShowFrontier(
TEXT("zeus.debug.frontier"), 0, TEXT("Desenha as paredes das fronteiras do mesh (own<->neighbor)."), ECVF_Default);
namespace
{
const FColor kColorInterest(93, 213, 255); // cyan — Zona de Interesse
const FColor kColorDespawn(255, 140, 90); // laranja — Zona de Despawn (externa)
const FColor kColorCell(232, 192, 96);
const FColor kColorHandoff(255, 155, 190);
// Resolucao das esferas de overlay. 32 segments deixavam a "bolha" facetada
// (poucas linhas) num raio de 60-80m; 48 deixa o circulo bem mais denso/legivel.
constexpr int32 kSphereSegments = 48;
// Clamp visual defensivo: se o raio vier absurdo (config corrompida), nao desenha
// uma esfera gigante que engole o mapa. 500m cobre qualquer AOI real (max=800m,
// mas overlay > 500m ja' nao ajuda a leitura). So' afeta o desenho, nao o gameplay.
constexpr float kMaxOverlayRadiusCm = 50000.0f;
}
UZeusAOIComponent::UZeusAOIComponent()
{
PrimaryComponentTick.bCanEverTick = true;
PrimaryComponentTick.bStartWithTickEnabled = true;
SetIsReplicatedByDefault(false); // debug/cliente; gameplay AOI real entra depois
}
void UZeusAOIComponent::BeginPlay()
{
Super::BeginPlay();
// Bind do feed de config de AOI (servidor -> cliente) pelo transporte V1
// canonico. O legacy (UZeusNetworkSubsystem) esta desativado no V1 -> usava
// SendDebugAoiRequest que logava "ignorado: nao conectado" e o overlay nunca
// recebia a config. Agora via UZeusNetworkingClientSubsystem (OnDebugAoiInfo 6161).
if (UWorld* World = GetWorld())
{
if (UGameInstance* GI = World->GetGameInstance())
{
if (UZeusNetworkingClientSubsystem* NetC = GI->GetSubsystem<UZeusNetworkingClientSubsystem>())
{
AoiConfigHandle_ = NetC->OnDebugAoiInfo.AddUObject(this, &UZeusAOIComponent::HandleAoiConfig);
FrontierConfigHandle_ = NetC->OnDebugFrontierInfo.AddUObject(this, &UZeusAOIComponent::HandleFrontierConfig);
// 1.4 -- visibility tracking. Delegates DYNAMIC do subsystem V1
// precisam AddDynamic + UFUNCTION nas handlers (assim como no
// ZeusWorldSubsystem que ja faz spawn/despawn de proxies).
NetC->OnEntitySpawned.AddDynamic(this, &UZeusAOIComponent::HandleEntitySpawnedV1);
NetC->OnEntityDespawned.AddDynamic(this, &UZeusAOIComponent::HandleEntityDespawnedV1);
NetC->OnSelfEntityAssigned.AddDynamic(this, &UZeusAOIComponent::HandleSelfEntityAssignedV1);
}
}
}
// Best-effort: ja' pede a config (no-op se ainda nao conectado — o request
// e' re-disparado ao ligar um overlay em SetOverlayEnabled).
RequestAoiConfig();
}
void UZeusAOIComponent::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
if (AoiConfigHandle_.IsValid() || FrontierConfigHandle_.IsValid())
{
if (UWorld* World = GetWorld())
{
if (UGameInstance* GI = World->GetGameInstance())
{
if (UZeusNetworkingClientSubsystem* NetC = GI->GetSubsystem<UZeusNetworkingClientSubsystem>())
{
if (AoiConfigHandle_.IsValid()) { NetC->OnDebugAoiInfo.Remove(AoiConfigHandle_); }
if (FrontierConfigHandle_.IsValid()) { NetC->OnDebugFrontierInfo.Remove(FrontierConfigHandle_); }
// 1.4 -- unbind dos delegates DYNAMIC de tracking.
NetC->OnEntitySpawned.RemoveDynamic(this, &UZeusAOIComponent::HandleEntitySpawnedV1);
NetC->OnEntityDespawned.RemoveDynamic(this, &UZeusAOIComponent::HandleEntityDespawnedV1);
NetC->OnSelfEntityAssigned.RemoveDynamic(this, &UZeusAOIComponent::HandleSelfEntityAssignedV1);
}
}
}
AoiConfigHandle_.Reset();
FrontierConfigHandle_.Reset();
}
VisibleEntityIds_.Empty();
if (FrontierActor_)
{
FrontierActor_->Destroy();
FrontierActor_ = nullptr;
}
Super::EndPlay(EndPlayReason);
}
void UZeusAOIComponent::TickComponent(float DeltaTime, ELevelTick TickType,
FActorComponentTickFunction* ThisTickFunction)
{
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
// Frontier: sincroniza o overlay com o estado atual. Pega o liga/desliga via
// CVar zeus.debug.frontier (que nao passa por SetOverlayEnabled) e garante que
// ao desligar as paredes sejam REMOVIDAS (UpdateFrontierOverlay -> ClearOverlay).
if (ResolveOverlay(EZeusAOIOverlay::FrontierCells) != bFrontierApplied_)
{
UpdateFrontierOverlay();
}
// Re-pede o frontier periodicamente enquanto ligado -> o overlay reflete
// split/merge da topologia dinamica (debug client-side, baixa freq, so' ON).
if (ResolveOverlay(EZeusAOIOverlay::FrontierCells))
{
FrontierRepollAccumSec_ += DeltaTime;
if (FrontierRepollAccumSec_ >= FrontierRepollIntervalSec)
{
FrontierRepollAccumSec_ = 0.0f;
RequestFrontierConfig();
}
}
if (AnyOverlayActive())
{
DrawOverlays();
}
}
void UZeusAOIComponent::RequestAoiConfig()
{
if (UWorld* World = GetWorld())
{
if (UGameInstance* GI = World->GetGameInstance())
{
if (UZeusNetworkingClientSubsystem* NetC = GI->GetSubsystem<UZeusNetworkingClientSubsystem>())
{
NetC->EmitDebugAoiRequest(); // V1: no-op se ainda nao Accepted
}
}
}
}
void UZeusAOIComponent::HandleAoiConfig(float InterestCm, float DespawnCm)
{
InterestRadiusCm = InterestCm;
DespawnRadiusCm = DespawnCm;
// K4: log AOI cfg recebido do server (paridade com Net.AOI server-side
// "config_applied ZI=... ZD=..."). Cold path (1 por sessao).
UE_LOG(LogZeusAOI, Display,
TEXT("AOI cfg recv interestCm=%.0f (%.0fm) despawnCm=%.0f (%.0fm)"),
InterestCm, InterestCm / 100.0f,
DespawnCm, DespawnCm / 100.0f);
}
void UZeusAOIComponent::RequestFrontierConfig()
{
if (UWorld* World = GetWorld())
{
if (UGameInstance* GI = World->GetGameInstance())
{
if (UZeusNetworkingClientSubsystem* NetC = GI->GetSubsystem<UZeusNetworkingClientSubsystem>())
{
NetC->EmitDebugFrontierRequest(); // V1: no-op se ainda nao Accepted
}
}
}
}
void UZeusAOIComponent::HandleFrontierConfig(const ZeusV1::FDebugFrontierInfo& Info)
{
FrontierInfo_ = Info;
bFrontierReceived_ = true;
UE_LOG(LogZeusAOI, Display,
TEXT("Frontier cfg recv own=%d neighbors=%d"),
Info.OwnedCells.Num(), Info.NeighborCells.Num());
UpdateFrontierOverlay();
}
AZeusFrontierOverlayActor* UZeusAOIComponent::EnsureFrontierActor()
{
if (FrontierActor_) { return FrontierActor_; }
UWorld* World = GetWorld();
if (!World) { return nullptr; }
FActorSpawnParameters Params;
Params.Owner = GetOwner();
Params.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
FrontierActor_ = World->SpawnActor<AZeusFrontierOverlayActor>(
AZeusFrontierOverlayActor::StaticClass(), FVector::ZeroVector, FRotator::ZeroRotator, Params);
return FrontierActor_;
}
void UZeusAOIComponent::UpdateFrontierOverlay()
{
const bool bOn = ResolveOverlay(EZeusAOIOverlay::FrontierCells);
bFrontierApplied_ = bOn;
if (!bOn)
{
if (FrontierActor_) { FrontierActor_->ClearOverlay(); }
return;
}
if (!bFrontierReceived_)
{
// Ainda nao temos as cells; pede e espera o 6163 (HandleFrontierConfig re-chama).
RequestFrontierConfig();
return;
}
AZeusFrontierOverlayActor* Actor = EnsureFrontierActor();
if (!Actor) { return; }
const float AnchorZ = GetOwner() ? GetOwner()->GetActorLocation().Z : 0.0f;
Actor->RebuildFromFrontier(FrontierInfo_, AnchorZ);
}
bool UZeusAOIComponent::ResolveOverlay(EZeusAOIOverlay Overlay) const
{
switch (Overlay)
{
case EZeusAOIOverlay::AOIRadius: return bShowAOIRadius || CVarShowAOI.GetValueOnGameThread() > 0;
case EZeusAOIOverlay::DespawnZone: return bShowDespawnZone || CVarShowDespawn.GetValueOnGameThread() > 0;
case EZeusAOIOverlay::CellBounds: return bShowCellBounds || CVarShowCell.GetValueOnGameThread() > 0;
case EZeusAOIOverlay::ProxyLabels: return bShowProxyLabels || CVarShowProxy.GetValueOnGameThread() > 0;
case EZeusAOIOverlay::HandoffLine: return bShowHandoffLine || CVarShowHandoff.GetValueOnGameThread() > 0;
case EZeusAOIOverlay::AuthDrift: return bShowAuthDrift || CVarShowDrift.GetValueOnGameThread() > 0;
case EZeusAOIOverlay::FrontierCells: return bShowFrontier || CVarShowFrontier.GetValueOnGameThread() > 0;
default: return false;
}
}
bool UZeusAOIComponent::AnyOverlayActive() const
{
return ResolveOverlay(EZeusAOIOverlay::AOIRadius)
|| ResolveOverlay(EZeusAOIOverlay::DespawnZone)
|| ResolveOverlay(EZeusAOIOverlay::CellBounds)
|| ResolveOverlay(EZeusAOIOverlay::ProxyLabels)
|| ResolveOverlay(EZeusAOIOverlay::HandoffLine)
|| ResolveOverlay(EZeusAOIOverlay::AuthDrift)
|| ResolveOverlay(EZeusAOIOverlay::FrontierCells);
}
void UZeusAOIComponent::DrawOverlays()
{
const AActor* Owner = GetOwner();
UWorld* World = GetWorld();
if (!Owner || !World) { return; }
const FVector Loc = Owner->GetActorLocation();
// Zona de Interesse (interna) — raio real do servidor. Sem o raio (==0),
// ainda nao chegou a config: nao desenha (loading).
if (ResolveOverlay(EZeusAOIOverlay::AOIRadius) && InterestRadiusCm > 0.0f)
{
const float R = FMath::Min(InterestRadiusCm, kMaxOverlayRadiusCm);
DrawDebugSphere(World, Loc, R, kSphereSegments, kColorInterest, false, -1.0f, 0, LineThickness);
}
// Zona de Despawn (externa, > interesse = histerese) — raio real do servidor.
if (ResolveOverlay(EZeusAOIOverlay::DespawnZone) && DespawnRadiusCm > 0.0f)
{
const float R = FMath::Min(DespawnRadiusCm, kMaxOverlayRadiusCm);
DrawDebugSphere(World, Loc, R, kSphereSegments, kColorDespawn, false, -1.0f, 0, LineThickness);
}
if (ResolveOverlay(EZeusAOIOverlay::CellBounds))
{
// PLACEHOLDER: box grande centrada no player. TODO bounds reais da cell.
DrawDebugBox(World, Loc, FVector(CellBoundsExtentCm, CellBoundsExtentCm, 2000.0f),
kColorCell, false, -1.0f, 0, LineThickness);
}
if (ResolveOverlay(EZeusAOIOverlay::HandoffLine))
{
// PLACEHOLDER: vetor pra frente. TODO apontar pro centro da cell vizinha.
const float Len = InterestRadiusCm > 0.0f ? InterestRadiusCm : 3000.0f;
const FVector End = Loc + Owner->GetActorForwardVector() * Len;
DrawDebugDirectionalArrow(World, Loc, End, 200.0f, kColorHandoff, false, -1.0f, 0, LineThickness);
}
// ProxyLabels / AuthDrift: TODO (precisam do registry de proxies/snapshots).
}
void UZeusAOIComponent::SetOverlayEnabled_Implementation(EZeusAOIOverlay Overlay, bool bEnabled)
{
switch (Overlay)
{
case EZeusAOIOverlay::AOIRadius: bShowAOIRadius = bEnabled; break;
case EZeusAOIOverlay::DespawnZone: bShowDespawnZone = bEnabled; break;
case EZeusAOIOverlay::CellBounds: bShowCellBounds = bEnabled; break;
case EZeusAOIOverlay::ProxyLabels: bShowProxyLabels = bEnabled; break;
case EZeusAOIOverlay::HandoffLine: bShowHandoffLine = bEnabled; break;
case EZeusAOIOverlay::AuthDrift: bShowAuthDrift = bEnabled; break;
case EZeusAOIOverlay::FrontierCells: bShowFrontier = bEnabled; break;
default: break;
}
// Ao ligar uma zona de AOI sem ter os raios ainda, pede a config (loading).
const bool bAoiZone = (Overlay == EZeusAOIOverlay::AOIRadius || Overlay == EZeusAOIOverlay::DespawnZone);
if (bEnabled && bAoiZone && InterestRadiusCm <= 0.0f)
{
RequestAoiConfig();
}
// Frontier: ao ligar pede as cells (6162) e reconstroi; ao desligar limpa as paredes.
if (Overlay == EZeusAOIOverlay::FrontierCells)
{
if (bEnabled) { RequestFrontierConfig(); }
UpdateFrontierOverlay();
}
}
void UZeusAOIComponent::SetAllOverlays_Implementation(bool bEnabled)
{
bShowAOIRadius = bShowDespawnZone = bShowCellBounds = bEnabled;
bShowProxyLabels = bShowHandoffLine = bShowAuthDrift = bEnabled;
bShowFrontier = bEnabled;
if (bEnabled && InterestRadiusCm <= 0.0f)
{
RequestAoiConfig();
}
if (bEnabled) { RequestFrontierConfig(); }
UpdateFrontierOverlay();
}
bool UZeusAOIComponent::IsOverlayEnabled_Implementation(EZeusAOIOverlay Overlay) const
{
// Reflete o estado real (flag OU CVar) p/ a UI mostrar o que esta' desenhando.
return ResolveOverlay(Overlay);
}
float UZeusAOIComponent::GetInterestRadiusCm_Implementation() const
{
return InterestRadiusCm; // 0 = config do servidor ainda nao chegou (loading)
}
float UZeusAOIComponent::GetDespawnRadiusCm_Implementation() const
{
return DespawnRadiusCm;
}
int32 UZeusAOIComponent::GetEnabledOverlayCount() const
{
return (bShowAOIRadius ? 1 : 0) + (bShowDespawnZone ? 1 : 0) + (bShowCellBounds ? 1 : 0)
+ (bShowProxyLabels ? 1 : 0) + (bShowHandoffLine ? 1 : 0) + (bShowAuthDrift ? 1 : 0)
+ (bShowFrontier ? 1 : 0);
}
// ===========================================================================
// Gameplay AOI: visibility tracking (1.4 do roteiro PvP)
// ===========================================================================
//
// Espelha ENT_SPAWN/ENT_DESPAWN do subsystem V1 num TSet local. Fires os
// delegates BlueprintAssignable pra game systems escutarem. Self-entity
// filtrada (o proprio char nao entra no tracking nem fires).
void UZeusAOIComponent::HandleSelfEntityAssignedV1(int64 EntityId)
{
SelfEntityId_ = EntityId;
// Se ja' havia "spawnado" antes do SELF chegar (race possivel), remove agora.
if (EntityId != 0 && VisibleEntityIds_.Contains(EntityId))
{
VisibleEntityIds_.Remove(EntityId);
OnEntityExitedAOI.Broadcast(EntityId, /*Reason=*/0);
}
}
void UZeusAOIComponent::HandleEntitySpawnedV1(int64 EntityId, int32 /*Kind*/,
FVector /*PosCm*/, float /*YawDeg*/,
FVector /*VelCmS*/, bool /*bGrounded*/,
int64 /*ServerTimeMs*/)
{
if (EntityId == 0) { return; }
if (EntityId == SelfEntityId_) { return; } // nunca trackear self
bool bAdded = false;
VisibleEntityIds_.Add(EntityId, &bAdded);
if (bAdded)
{
OnEntityEnteredAOI.Broadcast(EntityId);
}
// Re-spawn (entity ja' visivel) = no-op silencioso. Pode acontecer em
// keyframe pos-handoff cross-server.
}
void UZeusAOIComponent::HandleEntityDespawnedV1(int64 EntityId, int32 Reason)
{
if (EntityId == 0) { return; }
if (VisibleEntityIds_.Remove(EntityId) > 0)
{
OnEntityExitedAOI.Broadcast(EntityId, Reason);
}
}
int32 UZeusAOIComponent::GetVisibleEntityCount() const
{
return VisibleEntityIds_.Num();
}
TArray<int64> UZeusAOIComponent::GetVisibleEntityIds() const
{
return VisibleEntityIds_.Array();
}
bool UZeusAOIComponent::IsEntityVisible(int64 EntityId) const
{
return VisibleEntityIds_.Contains(EntityId);
}

View File

@@ -0,0 +1,149 @@
// Copyright Zeus Server Engine. All rights reserved.
#pragma once
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "ZeusAOIDebugTarget.h" // plugin ZeusAdminToolsRuntime: interface + EZeusAOIOverlay
#include "ZeusV1Protocol.h" // ZeusV1::FDebugFrontierInfo (overlay de fronteira)
#include "ZeusAOIComponent.generated.h"
class AZeusFrontierOverlayActor;
// Delegates BlueprintAssignable pra game systems reagirem a entrada/saida de
// outras entidades na AOI do player local. Disparados quando ENT_SPAWN /
// ENT_DESPAWN chega do servidor (V1 canonico). Self-entity filtrada -- o
// proprio char NAO fires aqui.
// - Entered: 1 param (EntityId)
// - Exited: 2 params (EntityId, Reason). Reason vem do payload ENT_DESPAWN.
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FZeusAOIEntityEnter, int64, EntityId);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FZeusAOIEntityExit, int64, EntityId, int32, Reason);
/**
* UZeusAOIComponent (cliente / jogo)
*
* Componente de gameplay do player local. Dominio principal: Area of Interest
* do cliente (proxies, despawn coordenado, interpolacao — ver
* [[project_aoi_client_component]]). AQUI, alem disso, expoe um "plus" de
* DEBUG: overlays via DrawDebug* dirigidos pelo painel admin (plugin) atraves
* de IZeusAOIDebugTarget, e/ou por console vars (zeus.debug.aoi etc.).
*
* As DUAS zonas do AOI (Interesse interna + Despawn externa = histerese) vem do
* SERVIDOR: ao ligar um overlay (ou no BeginPlay) o componente pede a config via
* C_DEBUG_AOI_REQUEST e recebe S_DEBUG_AOI_INFO (delegate OnDebugAoiInfo do
* UZeusNetworkSubsystem) com interestRadiusCm + despawnRadiusCm. Enquanto nao
* chega (raio == 0), nao desenha (loading).
*
* Vive na src do ZMMO (nao no plugin): e' um componente do JOGO. O plugin
* ZeusAdminTools apenas COMUNICA com ele via a interface — direcao da
* dependencia: ZMMO -> plugin (implementa a interface).
*/
UCLASS(ClassGroup = (Zeus), meta = (BlueprintSpawnableComponent), DisplayName = "Zeus AOI Component")
class ZMMO_API UZeusAOIComponent : public UActorComponent, public IZeusAOIDebugTarget
{
GENERATED_BODY()
public:
UZeusAOIComponent();
virtual void BeginPlay() override;
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
virtual void TickComponent(float DeltaTime, ELevelTick TickType,
FActorComponentTickFunction* ThisTickFunction) override;
// === IZeusAOIDebugTarget (dirigido pelo painel admin) ===
virtual void SetOverlayEnabled_Implementation(EZeusAOIOverlay Overlay, bool bEnabled) override;
virtual void SetAllOverlays_Implementation(bool bEnabled) override;
virtual bool IsOverlayEnabled_Implementation(EZeusAOIOverlay Overlay) const override;
virtual float GetInterestRadiusCm_Implementation() const override;
virtual float GetDespawnRadiusCm_Implementation() const override;
// === Flags de debug por overlay ===
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Zeus|AOI Debug|Overlays") bool bShowAOIRadius = false; // Zona de Interesse
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Zeus|AOI Debug|Overlays") bool bShowDespawnZone = false; // Zona de Despawn
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Zeus|AOI Debug|Overlays") bool bShowCellBounds = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Zeus|AOI Debug|Overlays") bool bShowProxyLabels = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Zeus|AOI Debug|Overlays") bool bShowHandoffLine = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Zeus|AOI Debug|Overlays") bool bShowAuthDrift = false;
// Fronteiras do mesh: paredes own<->neighbor com label "Cell <id>", renderizadas
// por AZeusFrontierOverlayActor a partir do DEBUG_FRONTIER_INFO (6163).
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Zeus|AOI Debug|Overlays") bool bShowFrontier = false;
// === Parametros ===
// Raios reais vem do servidor (S_DEBUG_AOI_INFO). 0 = ainda nao recebido (loading).
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Zeus|AOI Debug|Params") float InterestRadiusCm = 0.0f;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Zeus|AOI Debug|Params") float DespawnRadiusCm = 0.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Zeus|AOI Debug|Params") float CellBoundsExtentCm = 50000.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Zeus|AOI Debug|Params") float LineThickness = 2.0f;
UFUNCTION(BlueprintPure, Category = "Zeus|AOI Debug")
int32 GetEnabledOverlayCount() const;
// === Gameplay AOI: visibility tracking (1.4 do roteiro PvP) ============
//
// Espelha quem o servidor mandou ENT_SPAWN/ENT_DESPAWN pro player local.
// Cliente NAO faz histérese própria -- server já filtra com
// margem de ~2048cm ([[project_aoi_crossserver_hysteresis]]). Game systems
// (HUD, audio, ability target filtering) escutam estes delegates pra
// reagir a entrada/saída. Self-entity filtrada (o próprio char
// não fires nem entra no tracking).
UPROPERTY(BlueprintAssignable, Category = "Zeus|AOI") FZeusAOIEntityEnter OnEntityEnteredAOI;
UPROPERTY(BlueprintAssignable, Category = "Zeus|AOI") FZeusAOIEntityExit OnEntityExitedAOI;
UFUNCTION(BlueprintPure, Category = "Zeus|AOI") int32 GetVisibleEntityCount() const;
UFUNCTION(BlueprintPure, Category = "Zeus|AOI") TArray<int64> GetVisibleEntityIds() const;
UFUNCTION(BlueprintPure, Category = "Zeus|AOI") bool IsEntityVisible(int64 EntityId) const;
UFUNCTION(BlueprintPure, Category = "Zeus|AOI") int64 GetSelfEntityId() const { return SelfEntityId_; }
private:
void DrawOverlays();
bool ResolveOverlay(EZeusAOIOverlay Overlay) const;
bool AnyOverlayActive() const;
/** Pede a config de AOI ao servidor (C_DEBUG_AOI_REQUEST). No-op se ja' temos
* os raios ou se nao houver UZeusNetworkSubsystem conectado. */
void RequestAoiConfig();
/** Bind de UZeusNetworkSubsystem::OnDebugAoiInfo: armazena os raios reais. */
void HandleAoiConfig(float InterestCm, float DespawnCm);
/** Pede o frontier do mesh ao servidor (DEBUG_FRONTIER_REQUEST 6162). */
void RequestFrontierConfig();
/** Bind de OnDebugFrontierInfo: guarda as cells e (se overlay on) reconstroi as paredes. */
void HandleFrontierConfig(const ZeusV1::FDebugFrontierInfo& Info);
/** Spawna (lazy) o ator que desenha as paredes + labels. */
AZeusFrontierOverlayActor* EnsureFrontierActor();
/** Liga / reconstroi / limpa o overlay de fronteira conforme bShowFrontier + dados. */
void UpdateFrontierOverlay();
/** Handle do bind do OnDebugAoiInfo (pra remover no EndPlay). */
FDelegateHandle AoiConfigHandle_;
/** Handle do bind do OnDebugFrontierInfo (frontier overlay). */
FDelegateHandle FrontierConfigHandle_;
/** Ultimo frontier recebido do server (own cells + neighbors). */
ZeusV1::FDebugFrontierInfo FrontierInfo_;
bool bFrontierReceived_ = false;
/** Estado do overlay JA aplicado (paredes desenhadas). O TickComponent compara
* com ResolveOverlay p/ pegar liga/desliga via CVar tambem (nao so' o toggle). */
bool bFrontierApplied_ = false;
/** Re-poll periodico do frontier enquanto ligado -> overlay reflete split/merge
* da topologia dinamica (o 6163 traz o estado atual do topology:current). */
float FrontierRepollIntervalSec = 2.0f;
float FrontierRepollAccumSec_ = 0.0f;
/** Ator que desenha as paredes + labels (spawn lazy ao ligar o overlay). */
UPROPERTY() TObjectPtr<AZeusFrontierOverlayActor> FrontierActor_ = nullptr;
// === Visibility tracking (1.4) ==========================================
UFUNCTION() void HandleEntitySpawnedV1(int64 EntityId, int32 Kind, FVector PosCm,
float YawDeg, FVector VelCmS, bool bGrounded,
int64 ServerTimeMs);
UFUNCTION() void HandleEntityDespawnedV1(int64 EntityId, int32 Reason);
UFUNCTION() void HandleSelfEntityAssignedV1(int64 EntityId);
/** EntityIds atualmente visíveis (espelho do server). TSet pra O(1) check.
* Não inclui self. Limpo no EndPlay. */
TSet<int64> VisibleEntityIds_;
/** EntityId do próprio char (recebido via ENT_SELF). 0 = ainda não atribuído. */
int64 SelfEntityId_ = 0;
};

View File

@@ -0,0 +1,218 @@
// Copyright Zeus Server Engine. All rights reserved.
#include "ZeusFrontierOverlayActor.h"
#include "ZMMONetLog.h" // LogZeusAOI (categoria do modulo ZMMO)
#include "ProceduralMeshComponent.h"
#include "Materials/MaterialInterface.h"
#include "Engine/World.h"
#include "DrawDebugHelpers.h"
#include "UObject/ConstructorHelpers.h"
namespace
{
const TCHAR* kWallMaterialPath =
TEXT("/Game/ZMMO/Materials/VFX/Portal/GridBoundary/Instancia/MI_VFX_Portal_GridBoundary_01.MI_VFX_Portal_GridBoundary_01");
const FColor kLabelAlive(93, 213, 255); // ciano -- server online
const FColor kLabelDead(255, 90, 90); // vermelho -- server offline / cell vazia
// Decodifica o cellId na notacao "raiz fora, splits no array" da k-d tree
// binaria: raiz pura -> "0"; derivada -> "0,[0,1]". Espelha o layout
// bit-packed de Server/ZeusServices/Shared/types/src/cell-id.ts:
// bit 30 = flag (1 = cell derivada de split)
// bits 29..26 = depth (profundidade de split)
// bits 25..15 = rootId (cell raiz do baseGrid)
// bits 14..0 = path (1 bit por nivel, MSB-first)
FString DecodeCellPath(uint32 CellId)
{
constexpr uint32 kFlagBit = 1u << 30;
if ((CellId & kFlagBit) == 0u)
{
return FString::Printf(TEXT("%u"), CellId); // raiz pura
}
const uint32 Depth = (CellId >> 26) & 0xFu;
const uint32 Root = (CellId >> 15) & 0x7FFu;
const uint32 Packed = CellId & 0x7FFFu;
FString Path;
for (uint32 i = 0; i < Depth; ++i)
{
if (i > 0)
{
Path += TEXT(",");
}
Path += ((Packed >> (14u - i)) & 1u) ? TEXT("1") : TEXT("0");
}
return FString::Printf(TEXT("%u,[%s]"), Root, *Path);
}
// Aresta compartilhada entre dois retangulos axis-aligned (no plano XY).
bool SharedEdge(float aMinX, float aMinY, float aMaxX, float aMaxY,
float bMinX, float bMinY, float bMaxX, float bMaxY,
float Eps, FVector2D& OutA, FVector2D& OutB)
{
const float yLo = FMath::Max(aMinY, bMinY), yHi = FMath::Min(aMaxY, bMaxY);
const float xLo = FMath::Max(aMinX, bMinX), xHi = FMath::Min(aMaxX, bMaxX);
if (FMath::Abs(aMaxX - bMinX) <= Eps && yHi > yLo) { OutA = {aMaxX, yLo}; OutB = {aMaxX, yHi}; return true; }
if (FMath::Abs(aMinX - bMaxX) <= Eps && yHi > yLo) { OutA = {aMinX, yLo}; OutB = {aMinX, yHi}; return true; }
if (FMath::Abs(aMaxY - bMinY) <= Eps && xHi > xLo) { OutA = {xLo, aMaxY}; OutB = {xHi, aMaxY}; return true; }
if (FMath::Abs(aMinY - bMaxY) <= Eps && xHi > xLo) { OutA = {xLo, aMinY}; OutB = {xHi, aMinY}; return true; }
return false;
}
}
AZeusFrontierOverlayActor::AZeusFrontierOverlayActor()
{
PrimaryActorTick.bCanEverTick = true;
PrimaryActorTick.bStartWithTickEnabled = true;
WallMesh = CreateDefaultSubobject<UProceduralMeshComponent>(TEXT("WallMesh"));
SetRootComponent(WallMesh);
WallMesh->bUseAsyncCooking = false;
WallMesh->SetCollisionEnabled(ECollisionEnabled::NoCollision);
WallMesh->SetCastShadow(false);
static ConstructorHelpers::FObjectFinder<UMaterialInterface> MatFinder(kWallMaterialPath);
if (MatFinder.Succeeded())
{
WallMaterial = MatFinder.Object;
}
}
void AZeusFrontierOverlayActor::RebuildFromFrontier(const ZeusV1::FDebugFrontierInfo& Info, float AnchorZCm)
{
if (!WallMesh) { return; }
AnchorZ_ = AnchorZCm;
// 1) Junta own + neighbors numa lista unica de cells (grid inteiro). Dedup por cellId.
Cells_.Reset();
auto AddCell = [this](uint32 CellId, int32 MinX, int32 MinY, int32 MaxX, int32 MaxY,
const FString& InOwner, bool bIsDead)
{
for (const FCellView& Ex : Cells_) { if (Ex.CellId == CellId) { return; } }
FCellView V;
V.CellId = CellId;
V.MinX = static_cast<float>(MinX); V.MinY = static_cast<float>(MinY);
V.MaxX = static_cast<float>(MaxX); V.MaxY = static_cast<float>(MaxY);
V.Owner = InOwner; V.bDead = bIsDead;
Cells_.Add(MoveTemp(V));
};
for (const ZeusV1::FFrontierOwnCell& C : Info.OwnedCells)
{
AddCell(C.CellId, C.MinXCm, C.MinYCm, C.MaxXCm, C.MaxYCm, C.OwnerName, false);
}
for (const ZeusV1::FFrontierNeighborCell& Nb : Info.NeighborCells)
{
AddCell(Nb.CellId, Nb.MinXCm, Nb.MinYCm, Nb.MaxXCm, Nb.MaxYCm, Nb.OwnerName, Nb.bDead);
}
// 2) Paredes: 1 quad por aresta compartilhada entre QUALQUER par (i<j -> dedup).
const float BaseZ = AnchorZCm - WallHeightCm * 0.5f;
const float TopZ = AnchorZCm + WallHeightCm * 0.5f;
TArray<FVector> Verts;
TArray<int32> Tris;
TArray<FVector> Normals;
TArray<FVector2D> UVs;
TArray<FProcMeshTangent> Tangents;
TArray<FLinearColor> Colors;
for (int32 i = 0; i < Cells_.Num(); ++i)
{
for (int32 j = i + 1; j < Cells_.Num(); ++j)
{
const FCellView& A = Cells_[i];
const FCellView& B = Cells_[j];
FVector2D P0, P1;
if (!SharedEdge(A.MinX, A.MinY, A.MaxX, A.MaxY,
B.MinX, B.MinY, B.MaxX, B.MaxY, EdgeEpsilonCm, P0, P1))
{
continue;
}
const int32 Base = Verts.Num();
Verts.Add(FVector(P0.X, P0.Y, BaseZ));
Verts.Add(FVector(P1.X, P1.Y, BaseZ));
Verts.Add(FVector(P1.X, P1.Y, TopZ));
Verts.Add(FVector(P0.X, P0.Y, TopZ));
const FVector2D Dir2D = (P1 - P0).GetSafeNormal();
const FVector N(-Dir2D.Y, Dir2D.X, 0.f);
Normals.Add(N); Normals.Add(N); Normals.Add(N); Normals.Add(N);
UVs.Add(FVector2D(0.f, 1.f));
UVs.Add(FVector2D(1.f, 1.f));
UVs.Add(FVector2D(1.f, 0.f));
UVs.Add(FVector2D(0.f, 0.f));
const FProcMeshTangent Tan(FVector(Dir2D.X, Dir2D.Y, 0.f), false);
Tangents.Add(Tan); Tangents.Add(Tan); Tangents.Add(Tan); Tangents.Add(Tan);
const FLinearColor C = (A.bDead || B.bDead) ? FLinearColor(kLabelDead) : FLinearColor(kLabelAlive);
Colors.Add(C); Colors.Add(C); Colors.Add(C); Colors.Add(C);
Tris.Add(Base + 0); Tris.Add(Base + 1); Tris.Add(Base + 2);
Tris.Add(Base + 0); Tris.Add(Base + 2); Tris.Add(Base + 3);
Tris.Add(Base + 0); Tris.Add(Base + 2); Tris.Add(Base + 1);
Tris.Add(Base + 0); Tris.Add(Base + 3); Tris.Add(Base + 2);
}
}
WallMesh->ClearAllMeshSections();
WallMesh->SetVisibility(true);
if (Verts.Num() > 0)
{
WallMesh->CreateMeshSection_LinearColor(
/*SectionIndex=*/0, Verts, Tris, Normals, UVs, Colors, Tangents, /*bCreateCollision=*/false);
if (WallMaterial) { WallMesh->SetMaterial(0, WallMaterial); }
}
bHasOverlay_ = (Cells_.Num() > 0);
UE_LOG(LogZeusAOI, Display,
TEXT("Frontier overlay rebuilt: cells=%d walls=%d anchorZ=%.0f%s"),
Cells_.Num(), Verts.Num() / 4, AnchorZCm,
WallMaterial ? TEXT("") : TEXT(" [WARN: material nula]"));
}
void AZeusFrontierOverlayActor::ClearOverlay()
{
if (WallMesh)
{
WallMesh->ClearAllMeshSections();
WallMesh->SetVisibility(false);
}
Cells_.Reset();
bHasOverlay_ = false;
}
void AZeusFrontierOverlayActor::DrawCellLabels()
{
if (!bHasOverlay_ || Cells_.Num() == 0) { return; }
UWorld* World = GetWorld();
if (!World) { return; }
for (const FCellView& C : Cells_)
{
const float Cx = (C.MinX + C.MaxX) * 0.5f;
const float Cy = (C.MinY + C.MaxY) * 0.5f;
const FVector Pos(Cx, Cy, AnchorZ_ + LabelHeightCm);
// "Cell 0" / "Cell 0,2" (notacao da quadtree) + nome do server dono.
FString Txt = FString::Printf(TEXT("Cell %s"), *DecodeCellPath(C.CellId));
if (!C.Owner.IsEmpty()) { Txt += TEXT("\n") + C.Owner; }
const FColor Col = C.bDead ? kLabelDead : kLabelAlive;
// DrawDebugString: billboard de tela (visivel de qualquer lado, nao gira,
// tamanho de tela constante). Re-desenhado a cada frame (Duration 0).
DrawDebugString(World, Pos, Txt, /*TestBaseActor=*/nullptr, Col,
/*Duration=*/0.0f, /*bDrawShadow=*/true, LabelFontScale);
}
}
void AZeusFrontierOverlayActor::Tick(float DeltaSeconds)
{
Super::Tick(DeltaSeconds);
DrawCellLabels();
}

Some files were not shown because too many files have changed in this diff Show More