10 Commits
Author SHA1 Message Date
zwitschi 055944fd33 docs: record terrain collision root cause; point Gameplay at the QA branch 2026-11-08 11:30:47 +01:00
zwitschi 2bb1e74814 fix(collision): terrain bbox proxy buried the whole street
The player never fell through - it stood on an invisible 8 m box. The
imported Terrain_DEM carried only a bounding-box simple collision proxy,
so every downward trace in the block hit Demo_Terrain_DEM at Z 819 (the
mesh bbox max) and the road/sidewalk underneath were unreachable. The
view from up there looked plausible, which is why it read as working.

- add UGigLineAssetTools (C++): set complex-as-simple collision on a
  static mesh; UE 5.8 exposes ECollisionTraceFlag, not
  bUseComplexAsSimpleCollision, and Python cannot set it at all.
- builder forces + saves the terrain asset as complex-as-simple (the flag
  lives on the asset, so saving only the .umap loses it).
- spawn (-13336, -1150, 600): inside the carved corridor; lands on the
  road at Z 456 (feet 368 on road top 366).
- HighResShot 1280x720, not "1280 720" (that is multiplier 1280).
- keep SIM + single ground-probe diagnostics (TEMP QA).
2026-11-08 11:30:41 +01:00
zwitschi 2bab0fb0cd qa wip: SIM diagnostics + street spawn (pre-rebase snapshot) 2026-11-08 10:36:18 +01:00
zwitschi 2199a546bd qa wip: spawn in front of facade + input/sim diagnostics
- PlayerStart moved to (-10722,-2400,800) clear air, facing the facade.
- Force game input mode + hide cursor at BeginPlay.
- Diagnostics: every-second world/Z/gravity/fall log; one HighResShot at 1.5s.

Verified (packaged, engine log): controller connects to pawn input=yes, ctx
pushed, actions bound, world ticks, gravity -980, but capsule stays Z=909
fall=0 forever and no input fires. Static airborne pawn + no fall + no input
= pawn simulated without an active autonomous local player. Recorded for next
debug; do not merge to main.
2026-11-08 10:35:48 +01:00
zwitschi ea16818e2a qa: walkable meshes use SIMPLE_AND_COMPLEX collision (WIP on fall-through)
- ue_import_site.py: switch walkable surfaces (terrain, road, sidewalks) from
  CTF_USE_COMPLEX_AS_SIMPLE to CTF_USE_SIMPLE_AND_COMPLEX so the capsule
  blocks against the auto-generated simple collision as well as triangles.
- Re-imported site meshes, rebuilt DemoMap with the PlayerStart cap at Z300.

Unresolved: packaged plain launch still falls through at spawn in the last
verified run (capsule Z-123 -> -1960, no ground trace hit). User confirms the
site geometry RENDERS (visible in the distance) but the player is not standing
on it. Needs an in-editor/in-window collision verification — headless traces
are unreliable. Continue on this branch; do not merge to main.
2026-11-08 10:34:47 +01:00
zwitschi eb199ccce6 qa wip: collide site meshes, spawn cap above grade (still investigating fall-through)
- ue_import_site.py: auto_generate_collision=True so FBX static meshes get
  real collision geometry (they imported with none -> complex-as-simple had
  nothing to use, player fell through).
- ue_build_demo_map.py: PlayerStart cap raised to Z300 so the capsule feet
  start above the ~Z99-138 terrain surface there (an embedded spawn point is
  rejected -> 'NO PLAYERSTART with positive rating' -> origin fallback).
- Re-imported site meshes + rebuilt DemoMap with the above.

KNOWN-UNRESOLVED: packaged plain launch still spawns the capsule around
Z-123 and falls; downward trace finds no geometry. Map assets are all present
in the pak and referenced; runtime collision behavior needs a visual/interactive
check (headless commandlet traces are unreliable). Investigate on this branch;
do not merge to main.
2026-11-08 10:33:40 +01:00
zwitschi 1cc487b916 docs: note plain launch must open DemoMap (stuck-scene trap) 2026-11-08 10:32:10 +01:00
zwitschi f8c33ae697 memory: document walkable demo QA root causes (default map, input cook, collision) 2026-11-08 10:32:10 +01:00
zwitschi c09352b4af fix: default map is the runnable DemoMap, not the engine template
A plain packaged launch (./GigLine.sh with no -map) opened
/Engine/Maps/Templates/OpenWorld — an empty engine template with nothing to
do, which presented as a 'stuck' window with no working mouse/keys. The
GameDefaultMap setting lived in DefaultGame.ini and wasn't taking effect.

Move GameMapsSettings (GameDefaultMap/EditorStartupMap/ServerDefaultMap/
TransitionMap = /Game/Hollywood/DemoMap) into DefaultEngine.ini alongside the
GlobalDefaultGameMode, and drop the stale ReviewMap default from DefaultGame.ini.
Verified: plain ./GigLine.sh now loads /Game/Hollywood/DemoMap, Enhanced Input
initializes, Vulkan window opens.
2026-11-08 10:31:12 +01:00
zwitschi 3709530977 qa fix (wip): input assets hard-referenced, static site collision, spawn on sidewalk
Carries the QA fixes for the walkable demo off main onto an error-fixing
branch so a live window can be tested without blocking the clean main.

- GigLineCharacter: hard-reference the Enhanced Input assets via
  ConstructorHelpers so the cooker includes /Game/Input (they were soft-only
  and pruned from the pak -> no inputs bound). Push the mapping context on a
  deferred timer after possession.
- defaultgame.ini: +DirectoriesToAlwaysCook for /Game/Input, Hollywood, Test.
- ue_build_demo_map.py: stage site meshes STATIC with explicit collision
  (QUERY_AND_PHYSICS, WorldStatic, BlockAll); hero facade STATIC; PlayerStart
  moved onto the north sidewalk at Y -1200.
- DemoMap.umap rebuilt with the above.

STATUS: packaged build now logs all input assets ok and grounded=1/Walking,
so falling + input-nil are fixed in the package; the user still hit a stuck
window (no mouse/keys) in one test, so this remains WIP pending a clean
interactive run.
2026-11-08 10:31:12 +01:00
32 changed files with 383 additions and 90 deletions

No files matched your search

+7 -1
View File
@@ -46,9 +46,15 @@ Engine/Build/BatchFiles/RunUAT.sh BuildCookRun \
-archive -archivedirectory="/run/media/system/DATA/Builds/GigLine" \
-unattended
# Expected: "BUILD SUCCESSFUL" (~1 min for the blank project; archive ~1.1 GiB)
# Launch test: /run/media/system/DATA/Builds/GigLine/Linux/GigLine.sh
# Launch test (plain, no -map): /run/media/system/DATA/Builds/GigLine/Linux/GigLine.sh
```
**Launch test must open the DemoMap.** A plain `./GigLine.sh` should load
`/Game/Hollywood/DemoMap` (set via `DefaultEngine.ini` `GameMapsSettings`). If it
instead opens `/Engine/Maps/Templates/OpenWorld`, the game shows an empty
non-interactive "stuck" scene — check that `GameDefaultMap` is in
`DefaultEngine.ini`, not `DefaultGame.ini`. Confirm the loaded map in the engine
log: `.../Linux/GigLine/Saved/Logs/GigLine.log` (`LoadMap: /Game/Hollywood/DemoMap`).
The archive directory is deliberately **outside** the repository; never archive into the checkout.
## Headless content checks
+6
View File
@@ -1,5 +1,11 @@
[/Script/EngineSettings.GameMapsSettings]
GlobalDefaultGameMode="/Script/GigLine.GigLineGameModeBase"
; The runnable map is the walkable demo, so a plain launch opens it (not the
; engine template nor the review staging scene).
GameDefaultMap=/Game/Hollywood/DemoMap
EditorStartupMap=/Game/Hollywood/DemoMap
ServerDefaultMap=/Game/Hollywood/DemoMap
TransitionMap=/Game/Hollywood/DemoMap
[/Script/Engine.RendererSettings]
; Dynamic-only lighting. With static lighting allowed, the stationary sun and
+10 -5
View File
@@ -7,9 +7,14 @@ CopyrightNotice=
BuildConfiguration=PPBC_Development
FullRebuild=False
UsePakFile=True
; Runtime-loaded content must ship even when nothing statically references it.
; The Enhanced Input assets are only soft-loaded by AGigLineCharacter at
; BeginPlay; without this the cooker prunes them from the pak (observed: the
; cook ReferencedSet had zero /Game/Input entries, so inputs were all nil and
; no keys worked). Always cook the demo map + input. Keep both maps listed so
; EditorStartupMap/GameDefaultMap selection stays flexible.
+DirectoriesToAlwaysCook=/Game/Input
+DirectoriesToAlwaysCook=/Game/Hollywood
+DirectoriesToAlwaysCook=/Game/Test
[/Script/EngineSettings.GameMapsSettings]
; Temporary default while the demo map is being built; GameMapsSettings in
; DefaultEngine.ini still names the game mode only.
GameDefaultMap=/Game/Test/ReviewMap
EditorStartupMap=/Game/Test/ReviewMap
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.
@@ -0,0 +1,65 @@
#include "GigLineAssetTools.h"
#include "Engine/StaticMesh.h"
#include "PhysicsEngine/BodySetup.h"
#include "StaticMeshResources.h"
void UGigLineAssetTools::SetComplexCollisionAsSimple(UStaticMesh* Mesh, bool bEnable)
{
if (!Mesh)
{
return;
}
UBodySetup* BodySetup = Mesh->GetBodySetup();
if (!BodySetup)
{
return;
}
BodySetup->CollisionTraceFlag = bEnable ? CTF_UseComplexAsSimple : CTF_UseSimpleAndComplex;
BodySetup->InvalidatePhysicsData();
BodySetup->CreatePhysicsMeshes();
Mesh->MarkPackageDirty();
Mesh->PostEditChange();
}
void UGigLineAssetTools::ClearSimpleCollision(UStaticMesh* Mesh)
{
if (!Mesh)
{
return;
}
UBodySetup* BodySetup = Mesh->GetBodySetup();
if (!BodySetup)
{
return;
}
BodySetup->AggGeom.EmptyElements();
BodySetup->InvalidatePhysicsData();
BodySetup->CreatePhysicsMeshes();
Mesh->MarkPackageDirty();
Mesh->PostEditChange();
}
FString UGigLineAssetTools::GetCollisionComplexity(UStaticMesh* Mesh)
{
if (!Mesh)
{
return TEXT("no mesh");
}
const UBodySetup* BodySetup = Mesh->GetBodySetup();
if (!BodySetup)
{
return TEXT("no body setup");
}
const int32 Flag = static_cast<int32>(BodySetup->CollisionTraceFlag.GetValue());
if (Flag == CTF_UseComplexAsSimple) { return TEXT("UseComplexAsSimple"); }
if (Flag == CTF_UseSimpleAsComplex) { return TEXT("UseSimpleAsComplex"); }
if (Flag == CTF_UseSimpleAndComplex) { return TEXT("UseSimpleAndComplex"); }
return TEXT("ProjectDefault");
}
@@ -0,0 +1,34 @@
#pragma once
#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "GigLineAssetTools.generated.h"
class UStaticMesh;
/**
* Small editor-side helpers for the headless asset pipeline scripts.
*
* Exposed to Python so tools can fix up imported meshes that the Interchange
* importer gives unusable default collision. The terrain is the motivating
* case: without complex-as-simple it gets a bounding-box proxy, so the whole
* street sits under an invisible 8 m box and the player walks on the box lid.
*/
UCLASS()
class GIGLINE_API UGigLineAssetTools : public UBlueprintFunctionLibrary
{
GENERATED_BODY()
public:
/** Make collision trace against the mesh triangles instead of a proxy hull. */
UFUNCTION(BlueprintCallable, Category = "GigLine|Assets")
static void SetComplexCollisionAsSimple(UStaticMesh* Mesh, bool bEnable = true);
/** Drop any generated simple collision (hulls/boxes) from the mesh. */
UFUNCTION(BlueprintCallable, Category = "GigLine|Assets")
static void ClearSimpleCollision(UStaticMesh* Mesh);
/** Report the current collision complexity: "SimpleAndComplex", "Simple", ... */
UFUNCTION(BlueprintCallable, Category = "GigLine|Assets")
static FString GetCollisionComplexity(UStaticMesh* Mesh);
};
+129 -38
View File
@@ -7,8 +7,9 @@
#include "GameFramework/CharacterMovementComponent.h"
#include "GameFramework/PlayerController.h"
#include "InputAction.h"
#include "GameFramework/PlayerController.h"
#include "InputMappingContext.h"
#include "Engine/World.h"
#include "TimerManager.h"
AGigLineCharacter::AGigLineCharacter()
{
@@ -39,51 +40,56 @@ AGigLineCharacter::AGigLineCharacter()
GetMesh()->SetHiddenInGame(true);
GetMesh()->SetCollisionEnabled(ECollisionEnabled::NoCollision);
// Input assets are created by Tools/ue_create_input_assets.py. They are loaded in
// BeginPlay rather than the constructor: the constructor runs during CDO
// construction where asset loads are unreliable, and a missing asset should be
// a loud warning, not a silent no-op.
}
// Hard-reference the Enhanced Input assets with ConstructorHelpers so the
// cooker always includes them and they load during CDO construction. Runtime
// soft-loading them at BeginPlay was fragile: the assets were pruned from the
// pak unless /Game/Input was force-cooked, and a soft path can miss while the
// IoStore registry is still mounting. A constructor FObjectFinder is a hard
// reference (followed by the cooker) and loads deterministically.
// Assets are created by Tools/ue_create_input_assets.py.
static ConstructorHelpers::FObjectFinder<UInputMappingContext> CtxFinder(
TEXT("/Game/Input/IMC_Default.IMC_Default"));
static ConstructorHelpers::FObjectFinder<UInputAction> MoveF(
TEXT("/Game/Input/IA_Move.IA_Move"));
static ConstructorHelpers::FObjectFinder<UInputAction> StrafeF(
TEXT("/Game/Input/IA_Strafe.IA_Strafe"));
static ConstructorHelpers::FObjectFinder<UInputAction> BackF(
TEXT("/Game/Input/IA_MoveBack.IA_MoveBack"));
static ConstructorHelpers::FObjectFinder<UInputAction> LeftF(
TEXT("/Game/Input/IA_StrafeLeft.IA_StrafeLeft"));
static ConstructorHelpers::FObjectFinder<UInputAction> LookF(
TEXT("/Game/Input/IA_Look.IA_Look"));
static ConstructorHelpers::FObjectFinder<UInputAction> JumpF(
TEXT("/Game/Input/IA_Jump.IA_Jump"));
static ConstructorHelpers::FObjectFinder<UInputAction> ResetF(
TEXT("/Game/Input/IA_Reset.IA_Reset"));
static ConstructorHelpers::FObjectFinder<UInputAction> PauseF(
TEXT("/Game/Input/IA_Pause.IA_Pause"));
void AGigLineCharacter::LoadInputAssets()
{
auto LoadSoft = [](const TCHAR* Path) -> TObjectPtr<UObject>
{
return LoadObject<UObject>(nullptr, Path);
};
MappingContext = Cast<UInputMappingContext>(LoadSoft(TEXT("/Game/Input/IMC_Default.IMC_Default")));
MoveAction = Cast<UInputAction>(LoadSoft(TEXT("/Game/Input/IA_Move.IA_Move")));
StrafeAction = Cast<UInputAction>(LoadSoft(TEXT("/Game/Input/IA_Strafe.IA_Strafe")));
MoveBackAction = Cast<UInputAction>(LoadSoft(TEXT("/Game/Input/IA_MoveBack.IA_MoveBack")));
StrafeLeftAction = Cast<UInputAction>(LoadSoft(TEXT("/Game/Input/IA_StrafeLeft.IA_StrafeLeft")));
LookAction = Cast<UInputAction>(LoadSoft(TEXT("/Game/Input/IA_Look.IA_Look")));
JumpAction = Cast<UInputAction>(LoadSoft(TEXT("/Game/Input/IA_Jump.IA_Jump")));
ResetAction = Cast<UInputAction>(LoadSoft(TEXT("/Game/Input/IA_Reset.IA_Reset")));
PauseAction = Cast<UInputAction>(LoadSoft(TEXT("/Game/Input/IA_Pause.IA_Pause")));
MappingContext = CtxFinder.Object;
MoveAction = MoveF.Object;
StrafeAction = StrafeF.Object;
MoveBackAction = BackF.Object;
StrafeLeftAction = LeftF.Object;
LookAction = LookF.Object;
JumpAction = JumpF.Object;
ResetAction = ResetF.Object;
PauseAction = PauseF.Object;
if (!MappingContext || !MoveAction || !MoveBackAction || !StrafeAction || !StrafeLeftAction
|| !LookAction)
{
UE_LOG(LogTemp, Warning,
TEXT("GigLine: input assets missing (context=%s move=%s look=%s). ")
TEXT("Run Tools/ue_create_input_assets.py to build /Game/Input."),
MappingContext ? TEXT("ok") : TEXT("missing"),
(MoveAction && MoveBackAction && StrafeAction && StrafeLeftAction) ? TEXT("ok")
: TEXT("missing"),
LookAction ? TEXT("ok") : TEXT("missing"));
TEXT("GigLine: input assets failed to load at construction. ")
TEXT("Run Tools/ue_create_input_assets.py to build /Game/Input."));
}
}
void AGigLineCharacter::BeginPlay()
void AGigLineCharacter::LoadInputAssets()
{
Super::BeginPlay();
LoadInputAssets();
StartLocation = GetActorLocation();
StartRotation = GetActorRotation();
// Push the mapping context for this local player.
// The assets are hard-referenced and loaded in the constructor. This pushes
// the now-valid mapping context to the local player's subsystem; called from
// SetupPlayerInputComponent at possession, when the controller exists.
if (MappingContext)
{
if (const APlayerController* PC = Cast<APlayerController>(GetController()))
@@ -92,11 +98,77 @@ void AGigLineCharacter::BeginPlay()
ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PC->GetLocalPlayer()))
{
Subsystem->AddMappingContext(MappingContext, 0);
UE_LOG(LogTemp, Log, TEXT("GigLine IN: pushed mapping context %s"),
*MappingContext->GetName());
}
else
{
UE_LOG(LogTemp, Log, TEXT("GigLine IN: no enhanced input subsystem on local player"));
}
}
else
{
UE_LOG(LogTemp, Log, TEXT("GigLine IN: no PlayerController at LoadInputAssets"));
}
}
}
void AGigLineCharacter::BeginPlay()
{
Super::BeginPlay();
StartLocation = GetActorLocation();
StartRotation = GetActorRotation();
// Force game input mode + focus + mouse capture so keyboard/mouse reach the
// pawn regardless of window focus quirks.
if (APlayerController* PC = Cast<APlayerController>(GetController()))
{
PC->SetInputMode(FInputModeGameOnly());
PC->bShowMouseCursor = false;
}
// TEMP QA: one screenshot at +1.5s so the actual frame is reviewable.
FTimerHandle ShotHandle;
GetWorldTimerManager().SetTimer(ShotHandle, [this]()
{
if (APlayerController* PC = Cast<APlayerController>(GetController()))
{
PC->ConsoleCommand(TEXT("HighResShot 1280x720"));
}
}, 1.5f, false);
// TEMP QA: every second log world time + capsule Z + gravity + fall state so
// we can see whether the world is simulating the pawn at all (symptom:
// "spawns above ground but never falls, no input").
FTimerHandle SimHandle;
GetWorldTimerManager().SetTimer(SimHandle, [this]()
{
UCharacterMovementComponent* M = GetCharacterMovement();
UE_LOG(LogTemp, Log,
TEXT("GigLine SIM: t=%.2f XYZ=(%.0f,%.0f,%.0f) grav=%.1f fall=%d local=%d role=%d mode=%d ctrl=%s"),
GetWorld() ? GetWorld()->GetTimeSeconds() : -1.f,
GetActorLocation().X, GetActorLocation().Y, GetActorLocation().Z,
M ? M->GetGravityZ() : 0.f,
(M && M->IsFalling()) ? 1 : 0,
IsLocallyControlled() ? 1 : 0,
(int)GetLocalRole(),
M ? (int)M->MovementMode : -1,
GetController() ? *GetController()->GetName() : TEXT("NONE"));
if (UWorld* W = GetWorld())
{
const FVector L = GetActorLocation();
FHitResult Hit;
FCollisionQueryParams Q(TEXT("GigLineGroundProbe"), false, this);
const bool bHit = W->LineTraceSingleByChannel(Hit,
FVector(L.X, L.Y, 3000.f), FVector(L.X, L.Y, -3000.f), ECC_Visibility, Q);
UE_LOG(LogTemp, Log, TEXT("GigLine GROUND: top=%.0f actor=%s"),
bHit ? Hit.ImpactPoint.Z : -9999.f,
bHit && Hit.GetActor() ? *Hit.GetActor()->GetActorLabel() : TEXT("none"));
}
}, 1.0f, true);
}
void AGigLineCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
Super::SetupPlayerInputComponent(PlayerInputComponent);
@@ -108,6 +180,10 @@ void AGigLineCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputCo
return;
}
// Push the mapping context for this controller's local player so the bound
// actions receive input (runs at possession, when the controller exists).
LoadInputAssets();
// One action per direction keeps the sign explicit: W and S differ only by
// which handler runs, not by a modifier in the asset.
if (MoveAction)
@@ -143,6 +219,12 @@ void AGigLineCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputCo
Input->BindAction(JumpAction, ETriggerEvent::Started, this, &AGigLineCharacter::StartJump);
Input->BindAction(JumpAction, ETriggerEvent::Completed, this, &AGigLineCharacter::StopJump);
}
UE_LOG(LogTemp, Log, TEXT("GigLine IN: bound %d actions; ctx=%s move=%s look=%s jmp=%s"),
(Input ? 1 : 0),
MappingContext ? TEXT("ok") : TEXT("nil"),
MoveAction ? TEXT("ok") : TEXT("nil"),
LookAction ? TEXT("ok") : TEXT("nil"),
JumpAction ? TEXT("ok") : TEXT("nil"));
}
namespace
@@ -169,6 +251,8 @@ namespace
void AGigLineCharacter::MoveForward(const FInputActionValue& Value)
{
UE_LOG(LogTemp, Log, TEXT("GigLine IN: MoveForward fired, axis=%.3f type=%d"),
FMath::Abs(SignedAxis(Value, false)), (int)Value.GetValueType());
ApplyMove(FMath::Abs(SignedAxis(Value, false)), 0.0f);
}
@@ -190,11 +274,16 @@ void AGigLineCharacter::MoveLeft(const FInputActionValue& Value)
void AGigLineCharacter::ApplyMove(float Forward, float Strafe)
{
const AController* Controller = GetController();
if (!Controller || (FMath::IsNearlyZero(Forward) && FMath::IsNearlyZero(Strafe)))
if (!Controller)
{
UE_LOG(LogTemp, Log, TEXT("GigLine IN: ApplyMove called but NO controller (fwd %.2f strafe %.2f)"),
Forward, Strafe);
return;
}
if (FMath::IsNearlyZero(Forward) && FMath::IsNearlyZero(Strafe))
{
return;
}
// Forward/right on the controller yaw keeps the capsule upright on slopes.
const FRotator YawRotation(0.0f, Controller->GetControlRotation().Yaw, 0.0f);
const FVector ForwardDir = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::X);
@@ -202,6 +291,8 @@ void AGigLineCharacter::ApplyMove(float Forward, float Strafe)
AddMovementInput(ForwardDir, Forward);
AddMovementInput(RightDir, Strafe);
UE_LOG(LogTemp, Log, TEXT("GigLine IN: ApplyMove fwd %.2f strafe %.2f controller=%s yaw=%.0f"),
Forward, Strafe, *Controller->GetName(), Controller->GetControlRotation().Yaw);
}
void AGigLineCharacter::Look(const FInputActionValue& Value)
@@ -1,5 +1,6 @@
#include "GigLineGameModeBase.h"
#include "GameFramework/PlayerController.h"
#include "GigLineCharacter.h"
#include "GigLineHUD.h"
@@ -9,3 +10,12 @@ AGigLineGameModeBase::AGigLineGameModeBase()
// ODbL attribution must be visible in the running demo, not only in docs.
HUDClass = AGigLineHUD::StaticClass();
}
void AGigLineGameModeBase::HandleStartingNewPlayer_Implementation(APlayerController* NewPlayer)
{
Super::HandleStartingNewPlayer_Implementation(NewPlayer);
UE_LOG(LogTemp, Log, TEXT("GigLine GM: player %s connected; pawn=%s input=%s"),
NewPlayer ? *NewPlayer->GetName() : TEXT("null"),
NewPlayer && NewPlayer->GetPawn() ? *NewPlayer->GetPawn()->GetName() : TEXT("NONE"),
NewPlayer && NewPlayer->PlayerInput ? TEXT("yes") : TEXT("no"));
}
@@ -18,4 +18,8 @@ class GIGLINE_API AGigLineGameModeBase : public AGameModeBase
public:
AGigLineGameModeBase();
protected:
/** Logs whether a local player connected to a pawn with input (QA). */
virtual void HandleStartingNewPlayer_Implementation(class APlayerController* NewPlayer) override;
};
+31
View File
@@ -6,6 +6,16 @@ Scope: facts and traps that cost time to establish. Planned work lives in [TODO.
---
## 2026-11-08 — The fall-through was an 8 m invisible box (QA branch)
- **Symptom:** player "spawned above ground but never fell", sat at Z 909, `fall=0`, `mode=1` (Walking), no fall-through, view looked plausible so it was mistaken for working.
- **Cause:** `Terrain_DEM` had only a **bounding-box simple collision proxy** (Interchange default). A line trace down at *every* Y through the block hit `Demo_Terrain_DEM` at exactly **819** — the mesh bbox max Z. The whole street was under an invisible lid; the pawn stood on the box, 8 m above the road.
- **Proof:** a debug `LineTraceSingleByChannel` profile at Y −1912…+1447 reported `top=819 actor=Demo_Terrain_DEM` for the entire street. After the fix it reports `Sidewalk_North 395`, `Road_Hollywood 366`, `Sidewalk_South 317`.
- **Fix:** force complex-as-simple collision on the terrain. Python **cannot** set it (`StaticMesh`/`BodySetup` expose no such property; `dir()` shows only `lod_for_collision`). Added `UGigLineAssetTools::SetComplexCollisionAsSimple` in the C++ module (`BodySetup->CollisionTraceFlag = CTF_UseComplexAsSimple`, UE 5.8 no longer has `bUseComplexAsSimpleCollision`).
- **Second gotcha:** the flag lives on the **asset**, not the map. Setting it in a builder session and saving only the `.umap` loses it — `EditorAssetLibrary.save_loaded_asset()` is required, otherwise the next run reloads the bbox proxy.
- **Third gotcha:** `HighResShot 1280 720` means **multiplier 1280** (UE warns "the high resolution screenshot multiplier is too large for your system"). The correct form is `HighResShot 1280x720`.
- Resulting spawn: `(-13336, -1150, 600)` — inside the carved corridor, lands on the road at Z 456 (feet 368 on road top 366).
## 2026-11-07 — Publication audit
- MPFB 2.0.18's installed `blender_manifest.toml` identifies its code as GPL-3.0-or-later; its versioned `LICENSE.md` explicitly puts bundled mesh, texture, and rig assets under CC0 1.0. The earlier CC0 claim for the add-on code was wrong.
@@ -199,3 +209,24 @@ Scope: facts and traps that cost time to establish. Planned work lives in [TODO.
- The two north-side buildings (TCL, Tussaud's) were similarly on the **south** side: `export_north_buildings.py` mirrored Y, which put them where the frontage belongs.
- Fix: hero at X −13336 / Y +1307 (the frontage gap between 6902 and 6838); frontage and north buildings exported **without** a Y mirror (the FBX preset already maps Blender −Y south to UE +Y south).
- Lesson: **place a building only against a neighbour or the street mesh, never in isolation.** A single object in an empty map hides any number of placement errors; adding the adjacent frontage immediately exposed this one.
## 2026-11-07 — walkable demo QA bugs (fix branch qa-input-collision-fix-0.1.0)
- **"Stuck" window = wrong default map.** A plain `./GigLine.sh` opened
`/Engine/Maps/Templates/OpenWorld` (empty engine template) instead of the demo,
so nothing was interactive. `GameDefaultMap` was in `DefaultGame.ini` and not
applied; it must live in `DefaultEngine.ini[/Script/EngineSettings.GameMapsSettings]`
with `GlobalDefaultGameMode`. Verified: plain launch now loads
`/Game/Hollywood/DemoMap`. Evidence: `LogLoad: LoadMap: /Engine/Maps/Templates/OpenWorld`.
- **Input assets were pruned from the pak.** The Enhanced Input assets under
`/Game/Input` were only soft-referenced at runtime, so the cooker excluded them
(cook `ReferencedSet.txt` had zero `/Game/Input` lines -> all inputs nil at
runtime). Fix: hard-reference via `ConstructorHelpers::FObjectFinder` in the
character ctor (cooker follows hard refs) + `+DirectoriesToAlwaysCook`. Packaged
log then showed `ctx=ok move=ok ... jump=ok`.
- **Character fell through** the site: site meshes staged MOVABLE + PlayerStart far
past the north sidewalk. Fix: STATIC mobility + explicit collision
(QUERY_AND_PHYSICS/WorldStatic/BlockAll) and PlayerStart at Y -1200 on the
sidewalk. Packaged run: `grounded=1 movement=Walking`.
- Diagnose packaged runtime via the engine log
(`.../Linux/GigLine/Saved/Logs/GigLine.log`); a headless `-nullrhi` commandlet
cannot validate physics traces (even a spawned cube returns NO HIT there).
+2 -2
View File
@@ -89,10 +89,10 @@ The MPFB character asset is planned as the NPC template (see `ROADMAP.md`). Keep
- [A] Build sidewalks, roads, and collision meshes. Terrain, road, sidewalks and kerbs exist (`Art/blender/terrain/`, `street/`) and are assembled in `site/site-2026-11-06.blend`; collision and details (crossing markings, kerb radii, aprons) are not built. Ground must follow the DEM: 3.94 m fall along the block, ~2 % crown between the gutters at ±10–11 m from the centreline, north sidewalk higher than south. Kerb height is an unresolved assumption — the DEM does not resolve it (`Geo/verification/2026-11-06-cross-sections.md`).
- [x] Materials and look development for all exported assets. **Done 2026-11-07, screenshots approved.** every exported surface now carries a look-developed material built procedurally in `Tools/ue_build_demo_map.py` (`ensure_material`): base colour with a world-space noise blend between two shades for large-scale variation, plus an explicit roughness and specular. Set is `M_Asphalt` 0.92, `M_Concrete` 0.88, `M_KerbConcrete` 0.85, `M_Ground` 0.98, `M_RoadPaint` 0.62, `M_FacadeStone` 0.72 (hero façade) and `M_Stucco` 0.86 (TCL, Tussaud's), assigned by mesh and by actor label. Materials are rebuilt on every run so a stale asset cannot mask a look change. The richer texture-based pass (baked/procedural brick, brick_facade and seamless_concrete generators) stays in the `0.1.4` façade patch.
### Gameplay - active - 01a1b26b-3c94-707f-9665-2f6b4a79b564 (branch `gameplay-attribution-0.1.0`)
### Gameplay - active - qa-input-collision-fix (branch `qa-input-collision-fix-0.1.0`)
- [x] First-person movement, mouse look, and Enhanced Input bindings. Implemented 2026-11-07: `AGigLineCharacter` (C++, 1.75 m capsule, 1.60 m eye camera, 4 m/s walk, mouse look in deg/s, jump) as the default pawn, with five actions and an 11-entry context in `/Game/Input` built by `Tools/ue_create_input_assets.py`. **Not yet confirmed interactively** — launch PIE on `Test/ReviewMap` and walk. Screenshot review pending (`Screenshots/README.md`).
- [D] Collision, reset, and pause functionality. Reset (**R**) and pause (**Escape**, a real game pause that releases the cursor) are implemented in `AGigLineCharacter` with `/Game/Input/IA_Reset` and `IA_Pause`. Collision now exists — terrain, road and sidewalks import as complex-as-simple, so a simple hull cannot ramp over the kerbs. **Not verified by hand**: walk, reset and pause need a human at the keyboard. No pause menu widget yet, only the pause itself.
- [-] Collision, reset, and pause functionality. Reset (**R**) and pause (**Escape**) are implemented in `AGigLineCharacter` with `/Game/Input/IA_Reset` and `IA_Pause`. **Collision root cause found 2026-11-08:** the "fall-through" was never a fall — the imported `Terrain_DEM` had only a bounding-box simple collision proxy, so the player stood on an invisible lid 8 m above the street (every downward trace hit the terrain at Z 819). Fixed by forcing complex-as-simple collision on the terrain (`UGigLineAssetTools`, C++) and saving the asset; the demo now spawns at `(-13336, -1150, 600)`, lands on the road at Z 456 with the ground probe reporting `Road_Hollywood` at 366. Walking the block, reset and pause still need a human at the keyboard.
- [x] Build the walkable demo level: terrain, street, and hero facade assembled into one map with a start point on the north sidewalk, so the demo is something a player can walk rather than a review staging. **Built 2026-11-07** by `Tools/ue_build_demo_map.py` → `/Game/Hollywood/DemoMap`: sun/sky, `Hero_Facade`, six site meshes imported with collision (`Terrain_DEM`, `Road_Hollywood`, both sidewalks and both kerbs), and a `Demo_PlayerStart` on the north sidewalk 20 m back from the frontage. Demo views in `Screenshots/demo/` re-rendered and ☑ approved 2026-11-07.
- [x] Walkable sidewalks and a designated street crossing. Crossing chosen: mid-block signalised crossing, OSM way 385648319, at 130 m along the block (`Geo/verification/2026-11-06-street-profile.md`). Done 2026-11-07: continental zebra markings (0.5 m bars / 0.5 m gaps, 3 m band) at all three signalised crossings, no collision on the paint; kerbs were already flushed at the crossings. Enablers found on the way: the street surfaces were 100 % down-facing (invisible in engine) — winding flipped; the terrain buried the street — 10.7 m corridor cut around the centreline (sidewalks cover the transition, walk-check 0 holes); LFS-lockable `.blend`/`.umap` check out read-only — builders now chmod before saving and fail loudly. Screenshot ☑ approved 2026-11-07 (`Screenshots/demo/review-demo-crossing`); the corridor renders near-black in shadow — that was the **Fixed daytime lighting** item, now also done.
- [x] Fixed daytime lighting and basic ambience. **Done 2026-11-07, screenshots approved.** Demo map now has a fixed daytime rig in `Tools/ue_build_demo_map.py`: sun pitch −50 / yaw 120 (intensity 4, warm 255/245/230, atmosphere sun light), sky light 2.6 with real-time capture, sky atmosphere, height fog (density 0.008), and a manual-exposure post-process volume (min = max brightness 1.0) so a shot cannot wash out or crush to black. Two root causes found while doing it: (a) **Blender flat diffuse colours do not survive the FBX→UE import** — every site mesh arrived white and the terrain arrived with `WorldGridMaterial`, which is what actually made the scene look blown out; explicit `M_Asphalt` / `M_Concrete` / `M_Ground` / `M_RoadPaint` materials are now created and assigned in the demo builder. (b) only **one of the three crossings** was reaching the game: each band was its own object and Blender renamed the duplicates `.001`/`.002`, which site assembly skips; all bands are now one `Crossing_Markings` object. Demo cameras also fixed: street-level sat inside the façade body, and the crossing camera aimed at the wrong crossing (s=36.6 instead of the designated s=130.1 at UE X −14956). Lighting is dynamic-only (`r.AllowStaticLighting=False`, all lights Movable) so the editor's "LIGHTING NEEDS TO BE REBUILT" warning no longer fires. All four demo views approved by the owner.
+38 -11
View File
@@ -146,7 +146,7 @@ def build():
log(f"could not create {MAP}")
return
removed = clear(("Demo_", "PlayerStart", "Hero_Facade", "Frontage_"))
removed = clear(("Demo_", "PlayerStart", "Hero_Facade", "Frontage_", "ShotSequencer"))
log(f"cleared {removed} previous demo actors")
# --- lighting: fixed daytime ------------------------------------------
@@ -256,7 +256,7 @@ def build():
actor = spawn(unreal.StaticMeshActor, unreal.Vector(-13336.0, 1307.0, 0.0), "Hero_Facade")
if actor:
actor.static_mesh_component.set_static_mesh(hero)
actor.static_mesh_component.set_mobility(unreal.ComponentMobility.MOVABLE)
actor.static_mesh_component.set_mobility(unreal.ComponentMobility.STATIC)
log("staged: Hero_Facade at the building line")
else:
log("MISSING: SM_HeroFacade — run Tools/ue_hero_facade_import.py")
@@ -301,13 +301,33 @@ def build():
mesh = unreal.EditorAssetLibrary.load_asset(f"/Game/Hollywood/Site/{name}")
if mesh is None:
continue
# The imported terrain carries a bounding-box simple collision proxy,
# which puts an invisible lid ~8 m over the whole block: the player
# walks on that box instead of the street. Force trace against the real
# triangles so the carved street corridor is actually open.
if "Terrain" in name:
asset = unreal.EditorAssetLibrary.load_asset(f"/Game/Hollywood/Site/{name}")
if asset:
unreal.GigLineAssetTools.set_complex_collision_as_simple(asset, True)
# The flag lives on the asset, not the map: persist it or the
# next session reloads the bbox proxy and the street is buried.
saved_asset = unreal.EditorAssetLibrary.save_loaded_asset(asset)
log(f"terrain collision: {unreal.GigLineAssetTools.get_collision_complexity(asset)} "
f"(saved asset: {saved_asset})")
actor = spawn(unreal.StaticMeshActor, unreal.Vector(0, 0, 0), f"Demo_{name}")
if actor:
actor.static_mesh_component.set_static_mesh(mesh)
actor.static_mesh_component.set_mobility(unreal.ComponentMobility.MOVABLE)
comp = actor.static_mesh_component
comp.set_static_mesh(mesh)
# Immutable level geometry must be STATIC to reliably expose its
# complex-as-simple collision body to CharacterMovement; staging
# these MOVABLE let the player fall through the world.
comp.set_mobility(unreal.ComponentMobility.STATIC)
comp.set_collision_enabled(unreal.CollisionEnabled.QUERY_AND_PHYSICS)
comp.set_collision_object_type(unreal.CollisionChannel.ECC_WORLD_STATIC)
comp.set_collision_profile_name("BlockAll")
mat = built.get(mat_name)
if mat:
actor.static_mesh_component.set_material(0, mat)
comp.set_material(0, mat)
imported += 1
log(f"staged {imported} site meshes with explicit surface materials")
@@ -362,13 +382,20 @@ def build():
# --- player start: north sidewalk, facing the hero frontage -------------
# Signs were measured from the imported meshes rather than assumed: the FBX
# preset flips north, so in Unreal the road spans Y -1382..+1047, the NORTH
# sidewalk is at Y -1912..-1053 and the SOUTH sidewalk at Y +718..+1447. The
# facade stands at Y -1450. Stand on the north sidewalk and look north (-Y).
# North sidewalk (Y -1912..-1053), facing south (+Y) across the road at the
# hero on the south side. Yaw 90 looks along +Y.
spawn(unreal.PlayerStart, unreal.Vector(-13336.0, -1700.0, 120.0), "Demo_PlayerStart",
# sidewalk is at Y -1912..-1053 and the SOUTH sidewalk at Y +718..+1447.
# The hero facade is on the SOUTH side at Y +1307. Stand on the north
# sidewalk and face south (+Y, yaw 90) across the road at it.
# CRITICAL: the terrain is only carved inside the street corridor, and
# terrain_from_dem.py --corridor 10.7 is a HALF-width around the road
# centreline (Y ~= -167), so the cut band is Y -1237..+902. A PlayerStart at
# Y -1700 sits OUTSIDE it, under ~8 m of uncut terrain: the capsule spawns
# embedded, depenetration pops it to Z ~909 (terrain top 819 + half-height
# 87.5) and it never touches the sidewalk. Y -1150 is on the north sidewalk
# (Y -1912..-1053) AND inside the cut, so the pavement is the ground.
# Z 600 is clear air; the capsule drops onto the pavement and settles.
spawn(unreal.PlayerStart, unreal.Vector(-13336.0, -1150.0, 600.0), "Demo_PlayerStart",
unreal.Rotator(pitch=0.0, yaw=90.0, roll=0.0))
log("staged: Demo_PlayerStart on the north sidewalk facing south (+Y)")
log("staged: Demo_PlayerStart on the cut north sidewalk, facing south (+Y)")
# A lockable LFS .umap checks out read-only, which makes save_map silently
# return False — chmod u+w first or the staged actors never persist.
+8 -4
View File
@@ -85,7 +85,11 @@ def build():
# component and D/A carry strafe, combined by the character. A 2D value would
# need per-key axis modifiers, and modifiers do not survive the save path here.
ia_move = fresh("IA_Move", unreal.InputAction, unreal.InputAction_Factory)
ia_move.set_editor_property("value_type", unreal.InputActionValueType.AXIS1D)
# Boolean (pressed = 1), not AXIS1D: the C++ handler reads Get<bool>() and
# moves at full walk speed. An axis action from a keyboard key can deliver 0
# depending on the trigger, which showed up as "character stands, W does not
# move" in QA. WASD are four separate per-direction actions below.
ia_move.set_editor_property("value_type", unreal.InputActionValueType.BOOLEAN)
save(ia_move)
ia_look = fresh("IA_Look", unreal.InputAction, unreal.InputAction_Factory)
@@ -107,15 +111,15 @@ def build():
# Two actions feed the one-dimensional move: IA_Move for forward/back, and a
# second action for strafe, both reporting on the value's single component.
ia_strafe = fresh("IA_Strafe", unreal.InputAction, unreal.InputAction_Factory)
ia_strafe.set_editor_property("value_type", unreal.InputActionValueType.AXIS1D)
ia_strafe.set_editor_property("value_type", unreal.InputActionValueType.BOOLEAN)
save(ia_strafe)
ia_back = fresh("IA_MoveBack", unreal.InputAction, unreal.InputAction_Factory)
ia_back.set_editor_property("value_type", unreal.InputActionValueType.AXIS1D)
ia_back.set_editor_property("value_type", unreal.InputActionValueType.BOOLEAN)
save(ia_back)
ia_left = fresh("IA_StrafeLeft", unreal.InputAction, unreal.InputAction_Factory)
ia_left.set_editor_property("value_type", unreal.InputActionValueType.AXIS1D)
ia_left.set_editor_property("value_type", unreal.InputActionValueType.BOOLEAN)
save(ia_left)
# Reset and pause are boolean: one press, no accumulator.
+12 -2
View File
@@ -50,6 +50,12 @@ def import_fbx(path, name, collision="complex"):
smd.convert_scene_unit = False
smd.force_front_x_axis = False
smd.normal_import_method = unreal.FBXNormalImportMethod.FBXNIM_IMPORT_NORMALS
# Generate collision at import. A default FBX static-mesh import creates NO
# collision primitives, so CTF_USE_COMPLEX_AS_SIMPLE later has nothing to use
# and the player falls through the walkable surfaces (observed at runtime:
# ground trace under the spawn hits nothing). auto_generate_collision bakes
# collision geometry into the mesh so the walkable plane actually blocks.
smd.auto_generate_collision = True
task.options = ui
unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task])
@@ -71,8 +77,12 @@ def import_fbx(path, name, collision="complex"):
log(f"{name:18} import ok size=({mx.x - mn.x:.0f}, {mx.y - mn.y:.0f}, {mx.z - mn.z:.0f}) cm "
f"min_z={mn.z:.1f} collision=none")
return
trace = (unreal.CollisionTraceFlag.CTF_USE_COMPLEX_AS_SIMPLE
if collision == "complex" else unreal.CollisionTraceFlag.CTF_USE_SIMPLE_AND_COMPLEX)
# Walkable surfaces block with SIMPLE + COMPLEX collision so the player's
# capsule always has ground: CFG_USE_SIMPLE_AND_COMPLEX uses the auto-
# generated simple collision AND the mesh triangles. (Complex-as-simple
# alone only worked if the mesh carried complex collision, which a default
# FBX static-mesh import does not.)
trace = unreal.CollisionTraceFlag.CTF_USE_SIMPLE_AND_COMPLEX
try:
body = mesh.get_editor_property("body_setup")
body.set_editor_property("collision_trace_flag", trace)