feat(char-spawn): Fase 4 — DT_Maps + pawn reposicionado na pos do DB

Cliente UE (foco da frente):
- FZMMOMapDef + FZMMOMapSpawn (Source/ZMMO/Data/World/MapDef.h) — row do
  DT_Maps com MapId/DisplayName/ClientLevel/Spawns[] taggeados
- DT_Maps.uasset (Content/ZMMO/Data/World/) — 1 row "TestWorld" mapId=1
  apontando pra L_TestWorld + spawn "default" em (0,0,200)
- L_TestWorld movido: /Game/ThirdPerson/TestWorld -> /Game/ZMMO/Maps/World/L_TestWorld
  (75 ext actors WP duplicados, original deletado)
- ZMMOCharSummary.MapName (FString) -> MapId (int32)
- UIUserLobbyScreen_Base parseia uint16 mapId no S_CHAR_SELECT_OK + chama
  Flow->TravelToMapById(mapId) + Flow->SetPendingSpawnPose(pos, yaw)
- UIFrontEndFlowSubsystem: MapsTableAsset (config), FindMapDef,
  ResolveLevelPathByMapId, TravelToMapById, SetPendingSpawnPose/Consume
- AZMMOPlayerCharacter::BeginPlay: consome PendingSpawnPose do Flow e
  SetActorLocationAndRotation no pawn local (substitui PlayerStart default)
- DefaultGame.ini: MapsTableAsset=/Game/ZMMO/Data/World/DT_Maps.DT_Maps
- .gitignore: + .vscode/

Outras mudancas no working tree (paralelas a esta frente):
- Content/AutoCreated/MMOWidget/Widgets/WBP_* (boot/login/charselect/etc.)
- Content/AutoCreated/MMOWidget/Fonts/Font_* + font_*_Face
- Refatoracao de fontes ZMMO/UI/Fonts (FF_* deletadas, F_* + Face)
- Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/* (75 actors)
- Content/Editor/ZeusUMGForge/FontMap.uasset
- Content/ZMMO/UI/Shared/UI_CheckBox_Master.uasset
- Content/ExternalContent/{Fab,Marketplace,Megascans}/.gitkeep
- Content/ThirdPerson/Menu.umap
This commit is contained in:
2026-05-23 02:00:48 -03:00
parent 6968ac4656
commit 34e9dbe454
250 changed files with 918 additions and 8 deletions

View File

@@ -0,0 +1,92 @@
#pragma once
#include "CoreMinimal.h"
#include "Engine/DataTable.h"
#include "Engine/World.h"
#include "ZoneRow.h"
#include "MapDef.generated.h"
/**
* FZMMOMapSpawn
*
* Ponto de spawn dentro de um mapa, identificado por `Tag`. Usado pelo
* server pra decidir onde materializar um char quando ele entra:
* - Char sem posicao salva: usa spawn com tag="default"
* - Char saindo de raid: pode pedir tag="raid_return"
* - Char morto: pode pedir tag="graveyard"
*
* ZeusEditorTools pode auto-popular o `Spawns[]` do FZMMOMapDef varrendo
* todos APlayerStart (ou ator custom AZMMOMapSpawnPoint) presentes no
* `.umap` do `ClientLevel` — botao "Sync from level".
*/
USTRUCT(BlueprintType)
struct ZMMO_API FZMMOMapSpawn
{
GENERATED_BODY()
/** Identificador do spawn (ex.: "default", "raid_return"). "default" e obrigatorio. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Spawn")
FName Tag = TEXT("default");
/** Posicao em cm (espaco do mundo). */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Spawn")
FVector PositionCm = FVector::ZeroVector;
/** Yaw em graus. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Spawn")
float YawDeg = 0.f;
};
/**
* FZMMOMapDef
*
* Definicao canonica de um mapa do MMO. Linha da DT_Maps no cliente.
* Exportada para `Server/ZeusServerEngine/Config/DataTables/maps_config.json`
* pelo plugin ZeusEditorTools — o JSON e a fonte que o WorldServer consome.
*
* Identidade:
* - `MapId` (uint16) e a chave estavel cross-cliente/server. 0 = invalido
* (detecta bug de leitura de DB / migration esquecida).
* - RowName na DataTable e o nome interno (ex.: "TestWorld") — usado no
* exporter como `name` do JSON. Nao duplicamos em campo `Name`.
*
* Wire economy:
* - `S_CHAR_SELECT_OK` envia `mapId` como uint16 (2 bytes) em vez de
* `mapName` (string), evitando ~10-30 bytes por handoff.
*/
USTRUCT(BlueprintType)
struct ZMMO_API FZMMOMapDef : public FTableRowBase
{
GENERATED_BODY()
/** ID estavel uint16. 0 = invalido. int32 aqui pois UE nao expoe uint16 em UPROPERTY. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Map", meta = (ClampMin = "1", ClampMax = "65535"))
int32 MapId = 0;
/** Nome de exibicao localizavel. Aparece em UI/loading screen. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Map")
FText DisplayName;
/** Asset do level no cliente. Exporter converte pra string canonica no JSON do server. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Map|Level")
TSoftObjectPtr<UWorld> ClientLevel;
/**
* Pontos de spawn taggeados. Editavel inline na DataTable;
* ZeusEditorTools pode auto-popular varrendo PlayerStart do .umap.
* Pelo menos um com Tag="default" e esperado.
*/
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Map|Spawns")
TArray<FZMMOMapSpawn> Spawns;
/** Regras PvP/Safe/Dungeon. Reusa enum de FZoneRow. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Map")
EZoneRules Rules = EZoneRules::PvE;
/** Faixa de level recomendada — display only. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Map|Display")
int32 RecommendedLevelMin = 1;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Map|Display")
int32 RecommendedLevelMax = 99;
};

View File

@@ -18,6 +18,7 @@
#include "ZMMO.h"
#include "ZMMOWorldSubsystem.h"
#include "ZeusNetworkSubsystem.h"
#include "UI/FrontEnd/UIFrontEndFlowSubsystem.h"
DEFINE_LOG_CATEGORY(LogZMMOPlayer);
@@ -108,6 +109,31 @@ void AZMMOPlayerCharacter::BeginPlay()
ResolveZeusNetworkSubsystem();
BindZeusSpawnDelegate();
TryRegisterLocalEntityFromCachedSpawn();
// Fase 4: reposiciona o pawn na pos salva no DB (vinda no S_CHAR_SELECT_OK
// e memorizada no Flow). Substitui o PlayerStart default do level. Se nao
// ha pose pendente (primeiro boot / debug PIE direto no level), mantem o
// PlayerStart.
if (const UGameInstance* GI = GetGameInstance())
{
if (UUIFrontEndFlowSubsystem* Flow = GI->GetSubsystem<UUIFrontEndFlowSubsystem>())
{
FVector PosCm = FVector::ZeroVector;
float YawDeg = 0.0f;
if (Flow->ConsumePendingSpawnPose(PosCm, YawDeg))
{
const FRotator NewRot(0.0f, YawDeg, 0.0f);
SetActorLocationAndRotation(PosCm, NewRot, /*bSweep=*/false, nullptr, ETeleportType::TeleportPhysics);
if (AController* C = GetController())
{
C->SetControlRotation(NewRot);
}
UE_LOG(LogZMMOPlayer, Log,
TEXT("AZMMOPlayerCharacter: pawn reposicionado pra pos do DB (%s) yaw=%.1f"),
*PosCm.ToString(), YawDeg);
}
}
}
}
void AZMMOPlayerCharacter::EndPlay(const EEndPlayReason::Type EndPlayReason)

View File

@@ -1,6 +1,7 @@
#include "UIFrontEndFlowSubsystem.h"
#include "ZMMO.h"
#include "Data/World/MapDef.h"
#include "UIFrontEndScreenSet.h"
#include "UIManagerSubsystem.h"
#include "UIPrimaryGameLayout_Base.h"
@@ -10,6 +11,7 @@
#include "ZeusNetworkSubsystem.h"
#include "ZeusCharServerSubsystem.h"
#include "CommonActivatableWidget.h"
#include "Engine/DataTable.h"
#include "Engine/GameInstance.h"
#include "Engine/LocalPlayer.h"
#include "UObject/UObjectGlobals.h"
@@ -398,3 +400,70 @@ void UUIFrontEndFlowSubsystem::HandleServerHelloTheme(FName ThemeId)
}
}
}
const FZMMOMapDef* UUIFrontEndFlowSubsystem::FindMapDef(int32 MapId) const
{
if (MapId <= 0) return nullptr;
if (MapsTableAsset.IsNull())
{
UE_LOG(LogZMMO, Warning, TEXT("FrontEndFlow: MapsTableAsset nao configurado — adicione DT_Maps em DefaultGame.ini"));
return nullptr;
}
const UDataTable* Table = MapsTableAsset.LoadSynchronous();
if (!Table) return nullptr;
for (const TPair<FName, uint8*>& Row : Table->GetRowMap())
{
const FZMMOMapDef* Def = reinterpret_cast<const FZMMOMapDef*>(Row.Value);
if (Def && Def->MapId == MapId)
{
return Def;
}
}
return nullptr;
}
FString UUIFrontEndFlowSubsystem::ResolveLevelPathByMapId(int32 MapId) const
{
const FZMMOMapDef* Def = FindMapDef(MapId);
if (!Def) return FString();
const FSoftObjectPath ObjPath = Def->ClientLevel.ToSoftObjectPath();
if (!ObjPath.IsValid()) return FString();
return ObjPath.GetLongPackageName();
}
void UUIFrontEndFlowSubsystem::SetPendingSpawnPose(FVector PosCm, float YawDeg)
{
PendingSpawnPosCm = PosCm;
PendingSpawnYawDeg = YawDeg;
bHasPendingSpawnPose = true;
UE_LOG(LogZMMO, Log, TEXT("FrontEndFlow: PendingSpawnPose set pos=(%.1f,%.1f,%.1f) yaw=%.1f"),
PosCm.X, PosCm.Y, PosCm.Z, YawDeg);
}
bool UUIFrontEndFlowSubsystem::ConsumePendingSpawnPose(FVector& OutPosCm, float& OutYawDeg)
{
if (!bHasPendingSpawnPose) return false;
OutPosCm = PendingSpawnPosCm;
OutYawDeg = PendingSpawnYawDeg;
bHasPendingSpawnPose = false;
UE_LOG(LogZMMO, Log, TEXT("FrontEndFlow: PendingSpawnPose consumido pos=(%.1f,%.1f,%.1f) yaw=%.1f"),
OutPosCm.X, OutPosCm.Y, OutPosCm.Z, OutYawDeg);
return true;
}
bool UUIFrontEndFlowSubsystem::TravelToMapById(int32 MapId)
{
const FString MapPath = ResolveLevelPathByMapId(MapId);
if (MapPath.IsEmpty())
{
UE_LOG(LogZMMO, Warning, TEXT("FrontEndFlow: TravelToMapById(%d) falhou — sem entrada no DT_Maps"), MapId);
return false;
}
UE_LOG(LogZMMO, Log, TEXT("FrontEndFlow: TravelToMapById(%d) -> %s"), MapId, *MapPath);
bTravelingToWorld = true;
SetState(EZMMOFrontEndState::EnteringWorld);
const FString Options = TEXT("game=/Script/ZMMO.ZMMOGameMode");
UGameplayStatics::OpenLevel(GetGameInstance(), FName(*MapPath), /*bAbsolute=*/true, Options);
return true;
}

View File

@@ -1,6 +1,7 @@
#pragma once
#include "CoreMinimal.h"
#include "Engine/DataTable.h"
#include "Subsystems/GameInstanceSubsystem.h"
#include "UI/FrontEndTypes.h"
#include "UIFrontEndFlowSubsystem.generated.h"
@@ -9,6 +10,7 @@ class UUIFrontEndScreenSet;
class UUIManagerSubsystem;
class UZeusNetworkSubsystem;
class UZeusCharServerSubsystem;
struct FZMMOMapDef;
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnZMMOFrontEndStateChanged, EZMMOFrontEndState, NewState);
@@ -80,6 +82,42 @@ public:
UFUNCTION(BlueprintCallable, Category = "FrontEnd|Worlds")
void ClearSelectedWorld();
/**
* Resolve `MapId` (uint16 recebido do CharServer) → row do DT_Maps.
* Carrega o asset sync (PIE/dev OK; em prod considerar StreamableManager).
* Retorna nullptr se MapId=0, DT_Maps nao configurado, ou mapId nao
* encontrado.
*/
const FZMMOMapDef* FindMapDef(int32 MapId) const;
/**
* Helper: resolve `MapId` no DT_Maps e retorna `ClientLevel.ToSoftObjectPath().GetLongPackageName()`
* (ex.: `/Game/ThirdPerson/TestWorld`). String vazia se nao encontrado.
*/
UFUNCTION(BlueprintPure, Category = "FrontEnd|Maps")
FString ResolveLevelPathByMapId(int32 MapId) const;
/**
* Inicia transicao pra mundo dado um `MapId`: faz `OpenLevel` com
* `?game=/Script/ZMMO.ZMMOGameMode`. No-op se `MapId` invalido ou
* DT_Maps nao tem entrada.
*/
UFUNCTION(BlueprintCallable, Category = "FrontEnd|Maps")
bool TravelToMapById(int32 MapId);
/**
* Memoriza a pose autoritativa do char vinda no `S_CHAR_SELECT_OK`
* (pos salva no DB + yaw). Sobrevive ao travel porque vivemos no
* GameInstance. Lida pelo `AZMMOPlayerCharacter::BeginPlay` pra
* reposicionar o pawn local em vez de spawnar no PlayerStart default
* do level. Limpa apos consumo (a proxima sessao reenvia).
*/
UFUNCTION(BlueprintCallable, Category = "FrontEnd|Spawn")
void SetPendingSpawnPose(FVector PosCm, float YawDeg);
UFUNCTION(BlueprintCallable, Category = "FrontEnd|Spawn")
bool ConsumePendingSpawnPose(FVector& OutPosCm, float& OutYawDeg);
UPROPERTY(BlueprintAssignable, Category = "FrontEnd")
FOnZMMOFrontEndStateChanged OnStateChanged;
@@ -88,6 +126,15 @@ protected:
UPROPERTY(Config, EditDefaultsOnly, Category = "FrontEnd")
TSoftObjectPtr<UUIFrontEndScreenSet> ScreenSetAsset;
/**
* DataTable com FZMMOMapDef. Source of truth client-side para
* `MapId -> ClientLevel/spawns/displayName`. Configure em DefaultGame.ini:
* [/Script/ZMMO.UIFrontEndFlowSubsystem]
* MapsTableAsset=/Game/ZMMO/Data/World/DT_Maps.DT_Maps
*/
UPROPERTY(Config, EditDefaultsOnly, Category = "FrontEnd|Maps")
TSoftObjectPtr<UDataTable> MapsTableAsset;
private:
void BindNetwork();
void UnbindNetwork();
@@ -127,6 +174,16 @@ private:
UPROPERTY(Transient)
FString SelectedWorldId;
/** Pose autoritativa pendente (do S_CHAR_SELECT_OK). */
UPROPERTY(Transient)
FVector PendingSpawnPosCm = FVector::ZeroVector;
UPROPERTY(Transient)
float PendingSpawnYawDeg = 0.f;
UPROPERTY(Transient)
bool bHasPendingSpawnPose = false;
UPROPERTY(Transient)
TObjectPtr<UUIFrontEndScreenSet> ScreenSet;

View File

@@ -153,6 +153,7 @@ void UUIUserLobbyScreen_Base::ParseCharList(const TArray<uint8>& Payload)
uint32 baseLvl = 0, baseExp = 0, jobLvl = 0, jobExp = 0;
uint16 s = 0, a = 0, v = 0, in_ = 0, d = 0, l = 0;
uint32 hp = 0, maxHp = 0, sp = 0, maxSp = 0, money = 0;
uint16 mapId16 = 0;
float px = 0, py = 0, pz = 0, yaw = 0;
uint32 hair = 0, hairColor = 0, skinColor = 0;
uint8 bodyType = 0;
@@ -172,7 +173,7 @@ void UUIUserLobbyScreen_Base::ParseCharList(const TArray<uint8>& Payload)
|| !ReadU32(Payload, Pos, hp) || !ReadU32(Payload, Pos, maxHp)
|| !ReadU32(Payload, Pos, sp) || !ReadU32(Payload, Pos, maxSp)
|| !ReadU32(Payload, Pos, money)
|| !ReadStringUtf8(Payload, Pos, E.MapName)
|| !ReadU16(Payload, Pos, mapId16)
|| !ReadFloat(Payload, Pos, px) || !ReadFloat(Payload, Pos, py) || !ReadFloat(Payload, Pos, pz)
|| !ReadFloat(Payload, Pos, yaw)
|| !ReadU32(Payload, Pos, hair) || !ReadU32(Payload, Pos, hairColor) || !ReadU32(Payload, Pos, skinColor)
@@ -190,6 +191,7 @@ void UUIUserLobbyScreen_Base::ParseCharList(const TArray<uint8>& Payload)
E.Stats.Int = in_; E.Stats.Dex = d; E.Stats.Luk = l;
E.Hp = hp; E.MaxHp = maxHp; E.Sp = sp; E.MaxSp = maxSp;
E.Money = static_cast<int32>(money);
E.MapId = static_cast<int32>(mapId16);
E.Position = FVector(px, py, pz); E.YawDeg = yaw;
E.Appearance.Hair = hair; E.Appearance.HairColor = hairColor;
E.Appearance.SkinColor = skinColor; E.Appearance.BodyType = bodyType;
@@ -325,13 +327,14 @@ void UUIUserLobbyScreen_Base::HandleCharDeleteCancelAck(const TArray<uint8>& Pay
void UUIUserLobbyScreen_Base::HandleCharSelectOk(const TArray<uint8>& Payload)
{
// Wire: uint64 charId + string mapName + float x/y/z + float yaw +
// Wire: uint64 charId + uint16 mapId + float x/y/z + float yaw +
// string worldHost + uint16 worldPort + string handoffToken + string region
int32 Pos = 0;
uint64 CharId64 = 0; FString MapName, WorldHost, HandoffToken, Region;
uint64 CharId64 = 0; FString WorldHost, HandoffToken, Region;
uint16 MapId16 = 0;
float Px=0, Py=0, Pz=0, Yaw=0; uint16 WorldPort = 0;
if (!ReadU64(Payload, Pos, CharId64)
|| !ReadStringUtf8(Payload, Pos, MapName)
|| !ReadU16(Payload, Pos, MapId16)
|| !ReadFloat(Payload, Pos, Px) || !ReadFloat(Payload, Pos, Py) || !ReadFloat(Payload, Pos, Pz)
|| !ReadFloat(Payload, Pos, Yaw)
|| !ReadStringUtf8(Payload, Pos, WorldHost)
@@ -342,19 +345,25 @@ void UUIUserLobbyScreen_Base::HandleCharSelectOk(const TArray<uint8>& Payload)
UE_LOG(LogZMMO, Warning, TEXT("Lobby: S_CHAR_SELECT_OK malformado"));
return;
}
UE_LOG(LogZMMO, Log, TEXT("Lobby: handoff -> world=%s:%d map=%s token=%s..."),
*WorldHost, WorldPort, *MapName, *HandoffToken.Left(8));
UE_LOG(LogZMMO, Log, TEXT("Lobby: handoff -> world=%s:%d mapId=%u token=%s..."),
*WorldHost, WorldPort, static_cast<uint32>(MapId16), *HandoffToken.Left(8));
UGameInstance* GI = GetGameInstance();
if (!GI) return;
if (UUIFrontEndFlowSubsystem* Flow = GI->GetSubsystem<UUIFrontEndFlowSubsystem>())
UUIFrontEndFlowSubsystem* Flow = GI->GetSubsystem<UUIFrontEndFlowSubsystem>();
if (Flow)
{
Flow->SetState(EZMMOFrontEndState::EnteringWorld);
// Memoriza pose pra `AZMMOPlayerCharacter::BeginPlay` reposicionar
// o pawn local na pos salva no DB (em vez do PlayerStart default).
Flow->SetPendingSpawnPose(FVector(Px, Py, Pz), Yaw);
}
// Fase 3: handoff UDP — apresenta o ticket no `C_CONNECT_REQUEST`. O
// WorldServer valida via GETDEL no Valkey regional. WorldServer envia
// `S_CONNECT_OK` (sucesso) ou `S_CONNECT_REJECT` (ticket invalido/expirado).
// Disparamos PRIMEIRO o connect (nao-bloqueante), depois o OpenLevel —
// ZeusNetworkSubsystem e per-GameInstance e persiste cross-travel.
if (UZeusNetworkSubsystem* ZeusNet = GI->GetSubsystem<UZeusNetworkSubsystem>())
{
ZeusNet->ConnectToZeusServerWithTicket(WorldHost, static_cast<int32>(WorldPort), HandoffToken);
@@ -363,6 +372,18 @@ void UUIUserLobbyScreen_Base::HandleCharSelectOk(const TArray<uint8>& Payload)
{
UE_LOG(LogZMMO, Error, TEXT("Lobby: ZeusNetworkSubsystem ausente — handoff abortado"));
}
// Fase 4: lookup mapId no DT_Maps e dispara OpenLevel pro level certo.
// Se DT_Maps nao tem entrada (ou ainda nao foi criada no editor),
// caimos no fallback do S_TRAVEL_TO_MAP do server (mapName/mapPath string).
if (Flow && MapId16 != 0)
{
if (!Flow->TravelToMapById(static_cast<int32>(MapId16)))
{
UE_LOG(LogZMMO, Warning, TEXT("Lobby: mapId=%u nao resolvido no DT_Maps — esperando S_TRAVEL_TO_MAP do server."),
static_cast<uint32>(MapId16));
}
}
}
void UUIUserLobbyScreen_Base::HandleCharSelectReject(const TArray<uint8>& Payload)

View File

@@ -84,8 +84,9 @@ struct FZMMOCharSummary
UPROPERTY(BlueprintReadOnly, Category = "Zeus|Char")
int32 Money = 0;
/** ID do mapa (uint16). Resolvido via DT_Maps no cliente. 0 = invalido. */
UPROPERTY(BlueprintReadOnly, Category = "Zeus|Char")
FString MapName;
int32 MapId = 0;
UPROPERTY(BlueprintReadOnly, Category = "Zeus|Char")
FVector Position = FVector::ZeroVector;