diff --git a/.gitattributes b/.gitattributes index be559ae..5b9e84b 100644 --- a/.gitattributes +++ b/.gitattributes @@ -13,6 +13,7 @@ /SourceAssets/** -whitespace /gameFramework/third_party/** -whitespace /Docs/Production/Licenses/** -whitespace +/Content/IRONSPORE/Raw/*-MIT.txt -whitespace *.uasset binary *.umap binary diff --git a/Config/DefaultEditorPerProjectUserSettings.ini b/Config/DefaultEditorPerProjectUserSettings.ini new file mode 100644 index 0000000..a096489 --- /dev/null +++ b/Config/DefaultEditorPerProjectUserSettings.ini @@ -0,0 +1,4 @@ +[/Script/ModelContextProtocolEngine.ModelContextProtocolSettings] +bAutoStartServer=True +ServerPortNumber=8000 +ServerUrlPath=/mcp diff --git a/Config/DefaultEngine.ini b/Config/DefaultEngine.ini new file mode 100644 index 0000000..f673b9a --- /dev/null +++ b/Config/DefaultEngine.ini @@ -0,0 +1,94 @@ +[/Script/Engine.CollisionProfile] ++Profiles=(Name="Projectile",CollisionEnabled=QueryOnly,ObjectTypeName="Projectile",CustomResponses=,HelpMessage="Preset for projectiles",bCanModify=True) ++DefaultChannelResponses=(Channel=ECC_GameTraceChannel1,Name="Projectile",DefaultResponse=ECR_Block,bTraceType=False,bStaticObject=False) ++EditProfiles=(Name="Trigger",CustomResponses=((Channel=Projectile, Response=ECR_Ignore))) + +[/Script/EngineSettings.GameMapsSettings] +EditorStartupMap=/Game/IRONSPORE/Maps/L_Hangar.L_Hangar +LocalMapOptions= +TransitionMap= +bUseSplitscreen=True +TwoPlayerSplitscreenLayout=Horizontal +ThreePlayerSplitscreenLayout=FavorTop +GameInstanceClass=/Script/UEAdapter.IronsGameInstance +GameDefaultMap=/Game/IRONSPORE/Maps/L_Hangar.L_Hangar +ServerDefaultMap=/Engine/Maps/Entry +GlobalDefaultGameMode=/Script/ueRouge.IronsGameMode +GlobalDefaultServerGameMode=None + +[/Script/Engine.RendererSettings] +r.ReflectionMethod=1 +r.GenerateMeshDistanceFields=True +r.DynamicGlobalIlluminationMethod=1 +r.Mobile.EnableNoPrecomputedLightingCSMShader=1 +r.DefaultFeature.AutoExposure.ExtendDefaultLuminanceRange=True +r.DefaultFeature.AutoExposure.ExtendDefaultLuminanceRange=True +r.DefaultFeature.AutoExposure.ExtendDefaultLuminanceRange=True +r.DefaultFeature.AutoExposure.ExtendDefaultLuminanceRange=true +r.Shadow.Virtual.Enable=1 + +r.DefaultFeature.LocalExposure.HighlightContrastScale=0.8 +r.DefaultFeature.LocalExposure.HighlightContrastScale=0.8 +r.DefaultFeature.LocalExposure.HighlightContrastScale=0.8 + +r.DefaultFeature.LocalExposure.ShadowContrastScale=0.8 +r.DefaultFeature.LocalExposure.ShadowContrastScale=0.8 +r.DefaultFeature.LocalExposure.ShadowContrastScale=0.8 +r.DefaultFeature.LightUnits=2 +r.AllowStaticLighting=False +RedChromaticityCoordinate=(X=0.640000,Y=0.330000) +GreenChromaticityCoordinate=(X=0.300000,Y=0.600000) +BlueChromaticityCoordinate=(X=0.150000,Y=0.060000) +WhiteChromaticityCoordinate=(X=0.312700,Y=0.329000) +WorkingColorSpaceChoice=sRGB +r.LegacyLuminanceFactors=False +r.Lumen.ScreenTracingSource=0 +r.FirstPerson.SelfShadow=1 + +r.SkinCache.CompileShaders=True +r.SkinCache.CompileShaders=True +r.RayTracing=False + +r.RayTracing.RayTracingProxies.ProjectEnabled=True +r.RayTracing.RayTracingProxies.ProjectEnabled=True +r.Substrate=True +r.Substrate=False +r.Refraction.Blur=False +r.Lumen.HardwareRayTracing=False +r.Lumen.Reflections.HardwareRayTracing.Translucent.Refraction.EnableForProject=False +r.ReflectionCaptureResolution=512 +r.Lumen.TraceMeshSDFs=0 +r.Lumen.TranslucencyReflections.FrontLayer.EnableForProject=False +r.DistanceFields.DefaultVoxelDensity=0.200000 +r.Lumen.HardwareRayTracing.LightingMode=0 + +r.Substrate.ProjectGBufferFormat=0 + +[/Script/WindowsTargetPlatform.WindowsTargetSettings] +DefaultGraphicsRHI=DefaultGraphicsRHI_DX12 +DefaultGraphicsRHI=DefaultGraphicsRHI_DX12 +DefaultGraphicsRHI=DefaultGraphicsRHI_DX12 +-D3D12TargetedShaderFormats=PCD3D_SM5 ++D3D12TargetedShaderFormats=PCD3D_SM6 +-D3D11TargetedShaderFormats=PCD3D_SM5 ++D3D11TargetedShaderFormats=PCD3D_SM5 + +[/Script/HardwareTargeting.HardwareTargetingSettings] +TargetedHardwareClass=EHardwareClass::Desktop +AppliedTargetedHardwareClass=Desktop +DefaultGraphicsPerformance=EGraphicsPreset::Maximum +AppliedDefaultGraphicsPerformance=Maximum + +[/Script/Engine.Engine] +NearClipPlane=5.000000 + ++ActiveGameNameRedirects=(OldGameName="UEIntro",NewGameName="/Script/ueRouge") ++ActiveGameNameRedirects=(OldGameName="/Script/UEIntro",NewGameName="/Script/ueRouge") ++ActiveGameNameRedirects=(OldGameName="TP_FirstPersonBP",NewGameName="/Script/FirstTemplate") ++ActiveGameNameRedirects=(OldGameName="/Script/TP_FirstPersonBP",NewGameName="/Script/FirstTemplate") + +[/Script/AIModule.AISystem] +bForgetStaleActors=True + +[/Script/Engine.UserInterfaceSettings] +UIScaleRule=ShortestSide diff --git a/Config/DefaultGame.ini b/Config/DefaultGame.ini new file mode 100644 index 0000000..4c6358d --- /dev/null +++ b/Config/DefaultGame.ini @@ -0,0 +1,33 @@ +[/Script/EngineSettings.GeneralProjectSettings] +ProjectID=F52E421C4BD006FE53164BAB2B05F455 +ProjectName=IRONSPORE +ProjectDisplayedTitle=NSLOCTEXT("IRONSPORE", "Title", "IRONSPORE") +Description=Mech extraction prototype powered by independent gameFramework + +[ConsoleVariables] +CommonUI.CheckKeyboardFocusAndParentage=1 +CommonUI.DisallowUserFocusedWidgetForPendingFocusRecipient=1 +CommonUI.FallbackToDesiredOnAutoRestoreFailure=1 + +[/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="PrimaryAssetLabel",AssetBaseClass=/Script/Engine.PrimaryAssetLabel,bHasBlueprintClasses=False,bIsEditorOnly=True,Directories=((Path="/Game")),SpecificAssets=,Rules=(Priority=-1,ChunkId=-1,bApplyRecursively=True,CookRule=Unknown)) +; Required by the editor MCP GameFeatures toolset; the packaged game does not load this editor dependency. ++PrimaryAssetTypesToScan=(PrimaryAssetType="GameFeatureData",AssetBaseClass="/Script/GameFeatures.GameFeatureData",bHasBlueprintClasses=False,bIsEditorOnly=True,Directories=,SpecificAssets=,Rules=(Priority=-1,ChunkId=-1,bApplyRecursively=True,CookRule=AlwaysCook)) +bOnlyCookProductionAssets=False +bShouldManagerDetermineTypeAndName=False +bShouldGuessTypeAndNameInEditor=True +bShouldAcquireMissingChunksOnLoad=False +bShouldWarnAboutInvalidAssets=True +MetaDataTagsForAssetRegistry=() + +[/Script/UnrealEd.ProjectPackagingSettings] +BuildConfiguration=PPBC_Development +bUsePakFile=True +bUseIoStore=True +bCookAll=False ++MapsToCook=(FilePath="/Game/IRONSPORE/Maps/L_Hangar") ++MapsToCook=(FilePath="/Game/IRONSPORE/Maps/L_TrainingRange") ++MapsToCook=(FilePath="/Game/IRONSPORE/Maps/L_Warzone") ++DirectoriesToAlwaysCook=(Path="/Game/IRONSPORE") ++DirectoriesToAlwaysStageAsNonUFS=(Path="IRONSPORE/Raw") diff --git a/Config/DefaultInput.ini b/Config/DefaultInput.ini new file mode 100644 index 0000000..f419ff0 --- /dev/null +++ b/Config/DefaultInput.ini @@ -0,0 +1,119 @@ +[/Script/Engine.InputSettings] +-AxisConfig=(AxisKeyName="Gamepad_LeftX",AxisProperties=(DeadZone=0.25,Exponent=1.f,Sensitivity=1.f)) +-AxisConfig=(AxisKeyName="Gamepad_LeftY",AxisProperties=(DeadZone=0.25,Exponent=1.f,Sensitivity=1.f)) +-AxisConfig=(AxisKeyName="Gamepad_RightX",AxisProperties=(DeadZone=0.25,Exponent=1.f,Sensitivity=1.f)) +-AxisConfig=(AxisKeyName="Gamepad_RightY",AxisProperties=(DeadZone=0.25,Exponent=1.f,Sensitivity=1.f)) +-AxisConfig=(AxisKeyName="MouseX",AxisProperties=(DeadZone=0.f,Exponent=1.f,Sensitivity=0.07f)) +-AxisConfig=(AxisKeyName="MouseY",AxisProperties=(DeadZone=0.f,Exponent=1.f,Sensitivity=0.07f)) +-AxisConfig=(AxisKeyName="Mouse2D",AxisProperties=(DeadZone=0.f,Exponent=1.f,Sensitivity=0.07f)) ++AxisConfig=(AxisKeyName="Mouse2D",AxisProperties=(DeadZone=0.000000,Sensitivity=0.070000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="Gamepad_LeftX",AxisProperties=(DeadZone=0.250000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="Gamepad_LeftY",AxisProperties=(DeadZone=0.250000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="Gamepad_RightX",AxisProperties=(DeadZone=0.250000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="Gamepad_RightY",AxisProperties=(DeadZone=0.250000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="MouseX",AxisProperties=(DeadZone=0.000000,Sensitivity=0.070000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="MouseY",AxisProperties=(DeadZone=0.000000,Sensitivity=0.070000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="MouseWheelAxis",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="Gamepad_LeftTriggerAxis",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="Gamepad_RightTriggerAxis",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="Gamepad_Special_Left_X",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="Gamepad_Special_Left_Y",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="Vive_Left_Trigger_Axis",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="Vive_Left_Trackpad_X",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="Vive_Left_Trackpad_Y",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="Vive_Right_Trigger_Axis",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="Vive_Right_Trackpad_X",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="Vive_Right_Trackpad_Y",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="MixedReality_Left_Trigger_Axis",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="MixedReality_Left_Thumbstick_X",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="MixedReality_Left_Thumbstick_Y",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="MixedReality_Left_Trackpad_X",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="MixedReality_Left_Trackpad_Y",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="MixedReality_Right_Trigger_Axis",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="MixedReality_Right_Thumbstick_X",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="MixedReality_Right_Thumbstick_Y",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="MixedReality_Right_Trackpad_X",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="MixedReality_Right_Trackpad_Y",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusTouch_Left_Grip_Axis",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusTouch_Left_Trigger_Axis",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusTouch_Left_Thumbstick_X",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusTouch_Left_Thumbstick_Y",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusTouch_Right_Grip_Axis",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusTouch_Right_Trigger_Axis",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusTouch_Right_Thumbstick_X",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusTouch_Right_Thumbstick_Y",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="ValveIndex_Left_Grip_Axis",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="ValveIndex_Left_Grip_Force",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="ValveIndex_Left_Trigger_Axis",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="ValveIndex_Left_Thumbstick_X",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="ValveIndex_Left_Thumbstick_Y",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="ValveIndex_Left_Trackpad_X",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="ValveIndex_Left_Trackpad_Y",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="ValveIndex_Left_Trackpad_Force",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="ValveIndex_Left_Trackpad_Touch",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="ValveIndex_Right_Grip_Axis",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="ValveIndex_Right_Grip_Force",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="ValveIndex_Right_Trigger_Axis",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="ValveIndex_Right_Thumbstick_X",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="ValveIndex_Right_Thumbstick_Y",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="ValveIndex_Right_Trackpad_X",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="ValveIndex_Right_Trackpad_Y",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="ValveIndex_Right_Trackpad_Force",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusTouch_Left_Thumbstick",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusTouch_Left_FaceButton1",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusTouch_Left_Trigger",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusTouch_Left_FaceButton2",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusTouch_Left_IndexPointing",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusTouch_Left_ThumbUp",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusTouch_Left_ThumbRest",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusTouch_Right_Thumbstick",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusTouch_Right_FaceButton1",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusTouch_Right_Trigger",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusTouch_Right_FaceButton2",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusTouch_Right_IndexPointing",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusTouch_Right_ThumbUp",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusTouch_Right_ThumbRest",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusHand_Left_ThumbPinchStrength",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusHand_Left_IndexPinchStrength",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusHand_Left_MiddlePinchStrength",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusHand_Left_RingPinchStrength",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusHand_Left_PinkPinchStrength",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusHand_Right_ThumbPinchStrength",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusHand_Right_IndexPinchStrength",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusHand_Right_MiddlePinchStrength",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusHand_Right_RingPinchStrength",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusHand_Right_PinkPinchStrength",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="Cosmos_Left_Grip_Axis",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="Cosmos_Left_Trigger_Axis",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="Cosmos_Left_Thumbstick_X",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="Cosmos_Left_Thumbstick_Y",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="Cosmos_Right_Grip_Axis",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="Cosmos_Right_Trigger_Axis",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="Cosmos_Right_Thumbstick_X",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="Cosmos_Right_Thumbstick_Y",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +bAltEnterTogglesFullscreen=True +bF11TogglesFullscreen=True +bUseMouseForTouch=False +bEnableMouseSmoothing=True +bEnableFOVScaling=True +bCaptureMouseOnLaunch=True +bEnableLegacyInputScales=True +bEnableMotionControls=True +bFilterInputByPlatformUser=False +bShouldFlushPressedKeysOnViewportFocusLost=True +bAlwaysShowTouchInterface=False +bShowConsoleOnFourFingerTap=True +bEnableGestureRecognizer=False +bUseAutocorrect=False +DefaultViewportMouseCaptureMode=CapturePermanently +DefaultViewportMouseLockMode=LockAlways +FOVScale=0.011110 +DoubleClickTime=0.200000 +DefaultPlayerInputClass=/Script/EnhancedInput.EnhancedPlayerInput +DefaultInputComponentClass=/Script/EnhancedInput.EnhancedInputComponent +DefaultTouchInterface=None +-ConsoleKeys=Tilde ++ConsoleKeys=Tilde + +[EnhancedInputPlatformSettings_Windows EnhancedInputPlatformSettings] +bShouldLogMappingContextRedirects=False diff --git a/Config/Localization/Game_Compile.ini b/Config/Localization/Game_Compile.ini new file mode 100644 index 0000000..8a85ed3 --- /dev/null +++ b/Config/Localization/Game_Compile.ini @@ -0,0 +1,30 @@ +; THESE ARE GENERATED FILES, DO NOT EDIT DIRECTLY! +; USE THE LOCALIZATION DASHBOARD IN THE UNREAL EDITOR TO EDIT THE CONFIGURATION +[CommonSettings] +SourcePath=Content/Localization/Game +DestinationPath=Content/Localization/Game +ManifestName=Game.manifest +ArchiveName=Game.archive +ResourceName=Game.locres +bSkipSourceCheck=false +bValidateFormatPatterns=true +bValidateSafeWhitespace=false +bValidateRichTextTags=false +NativeCulture=en +CulturesToGenerate=en +CulturesToGenerate=ar +CulturesToGenerate=ko +CulturesToGenerate=ja +CulturesToGenerate=zh-Hans +CulturesToGenerate=fr +CulturesToGenerate=de +CulturesToGenerate=it +CulturesToGenerate=es +CulturesToGenerate=es-419 +CulturesToGenerate=pl +CulturesToGenerate=pt-BR +CulturesToGenerate=ru +CulturesToGenerate=tr + +[GatherTextStep0] +CommandletClass=GenerateTextLocalizationResource diff --git a/Config/Localization/Game_Export.ini b/Config/Localization/Game_Export.ini new file mode 100644 index 0000000..84602f9 --- /dev/null +++ b/Config/Localization/Game_Export.ini @@ -0,0 +1,31 @@ +; THESE ARE GENERATED FILES, DO NOT EDIT DIRECTLY! +; USE THE LOCALIZATION DASHBOARD IN THE UNREAL EDITOR TO EDIT THE CONFIGURATION +[CommonSettings] +SourcePath=Content/Localization/Game +DestinationPath=Content/Localization/Game +NativeCulture=en +CulturesToGenerate=en +CulturesToGenerate=ar +CulturesToGenerate=ko +CulturesToGenerate=ja +CulturesToGenerate=zh-Hans +CulturesToGenerate=fr +CulturesToGenerate=de +CulturesToGenerate=it +CulturesToGenerate=es +CulturesToGenerate=es-419 +CulturesToGenerate=pl +CulturesToGenerate=pt-BR +CulturesToGenerate=ru +CulturesToGenerate=tr +ManifestName=Game.manifest +ArchiveName=Game.archive +PortableObjectName=Game.po + +[GatherTextStep0] +CommandletClass=InternationalizationExport +bExportLoc=true +LocalizedTextCollapseMode=ELocalizedTextCollapseMode::IdenticalTextIdAndSource +POFormat=EPortableObjectFormat::Unreal +ShouldPersistCommentsOnExport=false +ShouldAddSourceLocationsAsComments=true diff --git a/Config/Localization/Game_ExportDialogueScript.ini b/Config/Localization/Game_ExportDialogueScript.ini new file mode 100644 index 0000000..d820864 --- /dev/null +++ b/Config/Localization/Game_ExportDialogueScript.ini @@ -0,0 +1,26 @@ +; THESE ARE GENERATED FILES, DO NOT EDIT DIRECTLY! +; USE THE LOCALIZATION DASHBOARD IN THE UNREAL EDITOR TO EDIT THE CONFIGURATION +[CommonSettings] +SourcePath=Content/Localization/Game +DestinationPath=Content/Localization/Game +NativeCulture=en +CulturesToGenerate=en +CulturesToGenerate=ar +CulturesToGenerate=ko +CulturesToGenerate=ja +CulturesToGenerate=zh-Hans +CulturesToGenerate=fr +CulturesToGenerate=de +CulturesToGenerate=it +CulturesToGenerate=es +CulturesToGenerate=es-419 +CulturesToGenerate=pl +CulturesToGenerate=pt-BR +CulturesToGenerate=ru +CulturesToGenerate=tr +ManifestName=Game.manifest +ArchiveName=Game.archive +DialogueScriptName=GameDialogue.csv + +[GatherTextStep0] +CommandletClass=ExportDialogueScript diff --git a/Config/Localization/Game_Gather.ini b/Config/Localization/Game_Gather.ini new file mode 100644 index 0000000..4b0bd42 --- /dev/null +++ b/Config/Localization/Game_Gather.ini @@ -0,0 +1,47 @@ +; THESE ARE GENERATED FILES, DO NOT EDIT DIRECTLY! +; USE THE LOCALIZATION DASHBOARD IN THE UNREAL EDITOR TO EDIT THE CONFIGURATION +[CommonSettings] +ManifestDependencies=../../Engine/Content/Localization/Engine/Engine.manifest +ManifestDependencies=../../Engine/Content/Localization/Editor/Editor.manifest +SourcePath=Content/Localization/Game +DestinationPath=Content/Localization/Game +ManifestName=Game.manifest +ArchiveName=Game.archive +NativeCulture=en +CulturesToGenerate=en +CulturesToGenerate=ar +CulturesToGenerate=ko +CulturesToGenerate=ja +CulturesToGenerate=zh-Hans +CulturesToGenerate=fr +CulturesToGenerate=de +CulturesToGenerate=it +CulturesToGenerate=es +CulturesToGenerate=es-419 +CulturesToGenerate=pl +CulturesToGenerate=pt-BR +CulturesToGenerate=ru +CulturesToGenerate=tr + +[GatherTextStep0] +CommandletClass=GatherTextFromAssets +IncludePathFilters=Content/DemoTemplate/* +ExcludePathFilters=Content/Localization/* +PackageFileNameFilters=*.umap +PackageFileNameFilters=*.uasset +ShouldExcludeDerivedClasses=false +ShouldGatherFromEditorOnlyData=false +SkipGatherCache=false + +[GatherTextStep1] +CommandletClass=GenerateGatherManifest + +[GatherTextStep2] +CommandletClass=GenerateGatherArchive + +[GatherTextStep3] +CommandletClass=GenerateTextLocalizationReport +bWordCountReport=true +WordCountReportName=Game.csv +bConflictReport=true +ConflictReportName=Game_Conflicts.txt diff --git a/Config/Localization/Game_GenerateReports.ini b/Config/Localization/Game_GenerateReports.ini new file mode 100644 index 0000000..0f106dd --- /dev/null +++ b/Config/Localization/Game_GenerateReports.ini @@ -0,0 +1,26 @@ +; THESE ARE GENERATED FILES, DO NOT EDIT DIRECTLY! +; USE THE LOCALIZATION DASHBOARD IN THE UNREAL EDITOR TO EDIT THE CONFIGURATION +[CommonSettings] +SourcePath=Content/Localization/Game +DestinationPath=Content/Localization/Game +ManifestName=Game.manifest +ArchiveName=Game.archive +CulturesToGenerate=en +CulturesToGenerate=ar +CulturesToGenerate=ko +CulturesToGenerate=ja +CulturesToGenerate=zh-Hans +CulturesToGenerate=fr +CulturesToGenerate=de +CulturesToGenerate=it +CulturesToGenerate=es +CulturesToGenerate=es-419 +CulturesToGenerate=pl +CulturesToGenerate=pt-BR +CulturesToGenerate=ru +CulturesToGenerate=tr + +[GatherTextStep0] +CommandletClass=GenerateTextLocalizationReport +bWordCountReport=true +WordCountReportName=Game.csv diff --git a/Config/Localization/Game_Import.ini b/Config/Localization/Game_Import.ini new file mode 100644 index 0000000..5dc022c --- /dev/null +++ b/Config/Localization/Game_Import.ini @@ -0,0 +1,29 @@ +; THESE ARE GENERATED FILES, DO NOT EDIT DIRECTLY! +; USE THE LOCALIZATION DASHBOARD IN THE UNREAL EDITOR TO EDIT THE CONFIGURATION +[CommonSettings] +SourcePath=Content/Localization/Game +DestinationPath=Content/Localization/Game +NativeCulture=en +CulturesToGenerate=en +CulturesToGenerate=ar +CulturesToGenerate=ko +CulturesToGenerate=ja +CulturesToGenerate=zh-Hans +CulturesToGenerate=fr +CulturesToGenerate=de +CulturesToGenerate=it +CulturesToGenerate=es +CulturesToGenerate=es-419 +CulturesToGenerate=pl +CulturesToGenerate=pt-BR +CulturesToGenerate=ru +CulturesToGenerate=tr +ManifestName=Game.manifest +ArchiveName=Game.archive +PortableObjectName=Game.po + +[GatherTextStep0] +CommandletClass=InternationalizationExport +bImportLoc=true +LocalizedTextCollapseMode=ELocalizedTextCollapseMode::IdenticalTextIdAndSource +POFormat=EPortableObjectFormat::Unreal diff --git a/Config/Localization/Game_ImportDialogue.ini b/Config/Localization/Game_ImportDialogue.ini new file mode 100644 index 0000000..21f6261 --- /dev/null +++ b/Config/Localization/Game_ImportDialogue.ini @@ -0,0 +1,27 @@ +; THESE ARE GENERATED FILES, DO NOT EDIT DIRECTLY! +; USE THE LOCALIZATION DASHBOARD IN THE UNREAL EDITOR TO EDIT THE CONFIGURATION +[CommonSettings] +SourcePath=Content/Localization/Game +ManifestName=Game.manifest +ArchiveName=Game.archive +NativeCulture=en +CulturesToGenerate=en +CulturesToGenerate=ar +CulturesToGenerate=ko +CulturesToGenerate=ja +CulturesToGenerate=zh-Hans +CulturesToGenerate=fr +CulturesToGenerate=de +CulturesToGenerate=it +CulturesToGenerate=es +CulturesToGenerate=es-419 +CulturesToGenerate=pl +CulturesToGenerate=pt-BR +CulturesToGenerate=ru +CulturesToGenerate=tr + +[GatherTextStep0] +CommandletClass=ImportLocalizedDialogue +RawAudioPath= +ImportedDialogueFolder=ImportedDialogue +bImportNativeAsSource=false diff --git a/Config/Localization/Game_ImportDialogueScript.ini b/Config/Localization/Game_ImportDialogueScript.ini new file mode 100644 index 0000000..4cc1fea --- /dev/null +++ b/Config/Localization/Game_ImportDialogueScript.ini @@ -0,0 +1,26 @@ +; THESE ARE GENERATED FILES, DO NOT EDIT DIRECTLY! +; USE THE LOCALIZATION DASHBOARD IN THE UNREAL EDITOR TO EDIT THE CONFIGURATION +[CommonSettings] +SourcePath=Content/Localization/Game +DestinationPath=Content/Localization/Game +NativeCulture=en +CulturesToGenerate=en +CulturesToGenerate=ar +CulturesToGenerate=ko +CulturesToGenerate=ja +CulturesToGenerate=zh-Hans +CulturesToGenerate=fr +CulturesToGenerate=de +CulturesToGenerate=it +CulturesToGenerate=es +CulturesToGenerate=es-419 +CulturesToGenerate=pl +CulturesToGenerate=pt-BR +CulturesToGenerate=ru +CulturesToGenerate=tr +ManifestName=Game.manifest +ArchiveName=Game.archive +DialogueScriptName=GameDialogue.csv + +[GatherTextStep0] +CommandletClass=ImportDialogueScript diff --git a/Content/IRONSPORE/Art/Environment/Hangar_Amber.uasset b/Content/IRONSPORE/Art/Environment/Hangar_Amber.uasset new file mode 100644 index 0000000..b998b83 Binary files /dev/null and b/Content/IRONSPORE/Art/Environment/Hangar_Amber.uasset differ diff --git a/Content/IRONSPORE/Art/Environment/Hangar_CoolLamp.uasset b/Content/IRONSPORE/Art/Environment/Hangar_CoolLamp.uasset new file mode 100644 index 0000000..f4b755d Binary files /dev/null and b/Content/IRONSPORE/Art/Environment/Hangar_CoolLamp.uasset differ diff --git a/Content/IRONSPORE/Art/Environment/Hangar_Panel.uasset b/Content/IRONSPORE/Art/Environment/Hangar_Panel.uasset new file mode 100644 index 0000000..fca87e5 Binary files /dev/null and b/Content/IRONSPORE/Art/Environment/Hangar_Panel.uasset differ diff --git a/Content/IRONSPORE/Art/Environment/Hangar_Rubber.uasset b/Content/IRONSPORE/Art/Environment/Hangar_Rubber.uasset new file mode 100644 index 0000000..58a1671 Binary files /dev/null and b/Content/IRONSPORE/Art/Environment/Hangar_Rubber.uasset differ diff --git a/Content/IRONSPORE/Art/Environment/Hangar_Steel.uasset b/Content/IRONSPORE/Art/Environment/Hangar_Steel.uasset new file mode 100644 index 0000000..c54768e Binary files /dev/null and b/Content/IRONSPORE/Art/Environment/Hangar_Steel.uasset differ diff --git a/Content/IRONSPORE/Art/Environment/SM_HangarBay.uasset b/Content/IRONSPORE/Art/Environment/SM_HangarBay.uasset new file mode 100644 index 0000000..786cf9f Binary files /dev/null and b/Content/IRONSPORE/Art/Environment/SM_HangarBay.uasset differ diff --git a/Content/IRONSPORE/Art/Materials/M_Industrial.uasset b/Content/IRONSPORE/Art/Materials/M_Industrial.uasset new file mode 100644 index 0000000..098fd81 Binary files /dev/null and b/Content/IRONSPORE/Art/Materials/M_Industrial.uasset differ diff --git a/Content/IRONSPORE/Art/Mechs/Armor.uasset b/Content/IRONSPORE/Art/Mechs/Armor.uasset new file mode 100644 index 0000000..68ab369 Binary files /dev/null and b/Content/IRONSPORE/Art/Mechs/Armor.uasset differ diff --git a/Content/IRONSPORE/Art/Mechs/Mechanism.uasset b/Content/IRONSPORE/Art/Mechs/Mechanism.uasset new file mode 100644 index 0000000..970c746 Binary files /dev/null and b/Content/IRONSPORE/Art/Mechs/Mechanism.uasset differ diff --git a/Content/IRONSPORE/Art/Mechs/Reactor.uasset b/Content/IRONSPORE/Art/Mechs/Reactor.uasset new file mode 100644 index 0000000..1b900e0 Binary files /dev/null and b/Content/IRONSPORE/Art/Mechs/Reactor.uasset differ diff --git a/Content/IRONSPORE/Art/Mechs/SM_MechScout.uasset b/Content/IRONSPORE/Art/Mechs/SM_MechScout.uasset new file mode 100644 index 0000000..b34fe99 Binary files /dev/null and b/Content/IRONSPORE/Art/Mechs/SM_MechScout.uasset differ diff --git a/Content/IRONSPORE/Art/Mechs/Trim.uasset b/Content/IRONSPORE/Art/Mechs/Trim.uasset new file mode 100644 index 0000000..ff8c6c6 Binary files /dev/null and b/Content/IRONSPORE/Art/Mechs/Trim.uasset differ diff --git a/Content/IRONSPORE/Audio/S_Impact.uasset b/Content/IRONSPORE/Audio/S_Impact.uasset new file mode 100644 index 0000000..db4ee0f Binary files /dev/null and b/Content/IRONSPORE/Audio/S_Impact.uasset differ diff --git a/Content/IRONSPORE/Audio/S_Secondary.uasset b/Content/IRONSPORE/Audio/S_Secondary.uasset new file mode 100644 index 0000000..8134678 Binary files /dev/null and b/Content/IRONSPORE/Audio/S_Secondary.uasset differ diff --git a/Content/IRONSPORE/Audio/S_Shot.uasset b/Content/IRONSPORE/Audio/S_Shot.uasset new file mode 100644 index 0000000..0b80d2c Binary files /dev/null and b/Content/IRONSPORE/Audio/S_Shot.uasset differ diff --git a/Content/IRONSPORE/Audio/S_UI.uasset b/Content/IRONSPORE/Audio/S_UI.uasset new file mode 100644 index 0000000..4b0b675 Binary files /dev/null and b/Content/IRONSPORE/Audio/S_UI.uasset differ diff --git a/Content/IRONSPORE/Blueprints/BP_IronsCharacter.uasset b/Content/IRONSPORE/Blueprints/BP_IronsCharacter.uasset new file mode 100644 index 0000000..ae3604b Binary files /dev/null and b/Content/IRONSPORE/Blueprints/BP_IronsCharacter.uasset differ diff --git a/Content/IRONSPORE/Blueprints/BP_IronsGameMode.uasset b/Content/IRONSPORE/Blueprints/BP_IronsGameMode.uasset new file mode 100644 index 0000000..dca5965 Binary files /dev/null and b/Content/IRONSPORE/Blueprints/BP_IronsGameMode.uasset differ diff --git a/Content/IRONSPORE/Maps/L_Hangar.umap b/Content/IRONSPORE/Maps/L_Hangar.umap new file mode 100644 index 0000000..84109ed Binary files /dev/null and b/Content/IRONSPORE/Maps/L_Hangar.umap differ diff --git a/Content/IRONSPORE/Maps/L_TrainingRange.umap b/Content/IRONSPORE/Maps/L_TrainingRange.umap new file mode 100644 index 0000000..3d4c9f9 Binary files /dev/null and b/Content/IRONSPORE/Maps/L_TrainingRange.umap differ diff --git a/Content/IRONSPORE/Maps/L_Warzone.umap b/Content/IRONSPORE/Maps/L_Warzone.umap new file mode 100644 index 0000000..c930fb7 Binary files /dev/null and b/Content/IRONSPORE/Maps/L_Warzone.umap differ diff --git a/Content/IRONSPORE/Raw/Fonts/IronsSansSC-Regular.ttf b/Content/IRONSPORE/Raw/Fonts/IronsSansSC-Regular.ttf new file mode 100644 index 0000000..ca74bc4 Binary files /dev/null and b/Content/IRONSPORE/Raw/Fonts/IronsSansSC-Regular.ttf differ diff --git a/Content/IRONSPORE/Raw/Localization/en.json b/Content/IRONSPORE/Raw/Localization/en.json new file mode 100644 index 0000000..8013f74 --- /dev/null +++ b/Content/IRONSPORE/Raw/Localization/en.json @@ -0,0 +1,1920 @@ +{ + "entries": [ + { + "key": "range.choose", + "value": "Left: targets. Right: timed gates. Beyond the targets: salvage." + }, + { + "key": "range.start", + "value": "01 / START" + }, + { + "key": "range.upper", + "value": "02 / RAMP" + }, + { + "key": "range.landing", + "value": "BRAKE HERE" + }, + { + "key": "range.gallery", + "value": "TARGET RANGE" + }, + { + "key": "range.salvage", + "value": "SALVAGE BAY" + }, + { + "key": "range.material", + "value": "MATERIALS" + }, + { + "key": "range.parts", + "value": "FIELD PARTS" + }, + { + "key": "range.targetGuide", + "value": "Hit both targets. Hits: {0}. Use the reset desk to retry." + }, + { + "key": "range.brake", + "value": "Stop inside the stripes at the far end of the platform." + }, + { + "key": "range.finish", + "value": "Course time: {0:F1} s. Cross the start gate again to retry." + }, + { + "key": "range.recoveryGuide", + "value": "Search a case [{0}], select cargo [{1}], then stop and hold [{2}] to send." + }, + { + "key": "range.recoverySent", + "value": "Cargo sent. Call the transport at the orange pad [{0}]." + }, + { + "key": "range.board", + "value": "Transport ready. Board at the orange pad [{0}]." + }, + { + "key": "range.reset", + "value": "Reset drills [{0}]" + }, + { + "key": "range.gate", + "value": "Gate cleared. Follow the lane." + }, + { + "key": "range.hit", + "value": "Target hits: {0}" + }, + { + "key": "range.ready", + "value": "Training equipment reset." + }, + { + "key": "ui.home.subtitle", + "value": "MECH RECOVERY OPERATIONS" + }, + { + "key": "ui.home.loadout", + "value": "STARTING LOADOUT" + }, + { + "key": "ui.slot.0", + "value": "PRIMARY" + }, + { + "key": "ui.slot.1", + "value": "SECONDARY" + }, + { + "key": "ui.slot.2", + "value": "PROPULSION" + }, + { + "key": "ui.slot.3", + "value": "DEFENSE" + }, + { + "key": "ui.home.part", + "value": "{0} / {1} [{2} M / {3} P]" + }, + { + "key": "ui.home.budget", + "value": "MASS {0} / {1} POWER {2} / {3}" + }, + { + "key": "ui.home.deploy", + "value": "DEPLOY SORTIE" + }, + { + "key": "ui.home.range", + "value": "DRIVING RANGE" + }, + { + "key": "ui.home.resume", + "value": "RESUME SORTIE" + }, + { + "key": "ui.home.locked", + "value": "Selected gear is out of stock. Standard slots can use loaners. All models are available in the range." + }, + { + "key": "ui.home.moveHint", + "value": "{0} move / mouse aim / {1} perspective / {2} boost / {3} jump & lift" + }, + { + "key": "ui.home.fireHint", + "value": "{0} primary / {1} precision aim / {2} secondary / {3} shield / {4} interact" + }, + { + "key": "ui.home.inventory", + "value": "HANGAR INVENTORY" + }, + { + "key": "ui.home.stock", + "value": "ALLOY {0} CELLS {1} SCHEMATICS {2} EXTRACTIONS {3} ROUTES CLEARED {4}" + }, + { + "key": "ui.home.researched", + "value": "{0} / RESEARCHED" + }, + { + "key": "ui.home.research", + "value": "{0} / RESEARCH: 2 ALLOY + 1 CELL" + }, + { + "key": "ui.home.quit", + "value": "QUIT" + }, + { + "key": "ui.home.language", + "value": "语言 / Language · {0}" + }, + { + "key": "ui.home.languageHelp", + "value": "Choose language" + }, + { + "key": "ui.language.current", + "value": "{0} / Selected" + }, + { + "key": "ui.hud.operation", + "value": "CURRENT OPERATION" + }, + { + "key": "ui.hud.armor", + "value": "ARMOR {0}" + }, + { + "key": "ui.hud.energy", + "value": "ENERGY {0}" + }, + { + "key": "ui.hud.shield", + "value": "SHIELD ACTIVE" + }, + { + "key": "ui.hud.podUsed", + "value": "POD USED / 1 OF 1" + }, + { + "key": "ui.hud.podPreparing", + "value": "PREPARING POD..." + }, + { + "key": "ui.hud.podReady", + "value": "POD READY / {0} SELECT CARGO" + }, + { + "key": "ui.hud.hint", + "value": "{0} VIEW {1} BOOST {2} LIFT {3} SECONDARY {4} SHIELD {5} INTERACT {6} CARGO HOLD {7} SEND POD {8} PAUSE" + }, + { + "key": "ui.hud.zone", + "value": "ZONE {0} / 5" + }, + { + "key": "ui.hud.status", + "value": "{0} {1}" + }, + { + "key": "ui.hud.complete", + "value": "MISSION COMPLETE / bring your cargo to extraction" + }, + { + "key": "ui.hud.boss", + "value": "Defeat the Overseer" + }, + { + "key": "ui.hud.escort", + "value": "Escort the engineering crawler to the far gate" + }, + { + "key": "ui.hud.routeOpen", + "value": "Route unlocked / search, extract or proceed" + }, + { + "key": "ui.hud.objective", + "value": "Recover navigation data at the relay" + }, + { + "key": "ui.hud.cockpit", + "value": "COCKPIT" + }, + { + "key": "ui.hud.shoulder", + "value": "SHOULDER" + }, + { + "key": "ui.hud.telemetry", + "value": "{0}\n{1} / {2}\nCARGO {3} / 16\n{4}" + }, + { + "key": "ui.hud.transportReady", + "value": "TRANSPORT READY / BOARD AT PAD" + }, + { + "key": "ui.hud.transportEta", + "value": "TRANSPORT ETA {0}s" + }, + { + "key": "ui.hud.extractionAvailable", + "value": "EXTRACTION AVAILABLE / ORANGE PAD" + }, + { + "key": "ui.cargo.title", + "value": "Cargo and ejection safe case" + }, + { + "key": "ui.cargo.used", + "value": "Pod launched. On destruction, only items in the safe case return." + }, + { + "key": "ui.cargo.help", + "value": "Drag an item or select it and click a cell. Press R to rotate. The safe case returns on destruction. Mark cargo, close this panel and hold {0} while stationary for three seconds to launch the pod." + }, + { + "key": "ui.cargo.stack", + "value": "{0} {1} ×{2}" + }, + { + "key": "ui.cargo.selected", + "value": "[SELECTED]" + }, + { + "key": "ui.cargo.capacity", + "value": "Hold {0}/16 cells Pod {1}/4 cells" + }, + { + "key": "ui.return", + "value": "RETURN TO PILOT" + }, + { + "key": "ui.refit.title", + "value": "FIELD REFIT" + }, + { + "key": "ui.refit.help", + "value": "Recovered a replacement part. Compare mass and power before installing." + }, + { + "key": "ui.refit.equipped", + "value": "EQUIPPED / {0} {1} M / {2} P" + }, + { + "key": "ui.refit.recovered", + "value": "RECOVERED / {0} {1} M / {2} P" + }, + { + "key": "ui.refit.install", + "value": "INSTALL RECOVERED PART" + }, + { + "key": "ui.refit.keep", + "value": "KEEP CURRENT LOADOUT" + }, + { + "key": "ui.routes.split", + "value": "COMMIT TO AN ENDPOINT" + }, + { + "key": "ui.routes.title", + "value": "CHOOSE THE NEXT WARZONE" + }, + { + "key": "ui.routes.splitHelp", + "value": "Your loadout has had two zones to develop. Choose the mission your mech is ready to tackle." + }, + { + "key": "ui.routes.help", + "value": "Objectives and resource categories are known. Specific salvage is discovered inside the zone." + }, + { + "key": "ui.routes.boss", + "value": "OVERSEER ROUTE / assault relay / weapon salvage" + }, + { + "key": "ui.routes.escort", + "value": "ENGINEERING ESCORT / restore supply link / propulsion salvage" + }, + { + "key": "ui.routes.freight", + "value": "FREIGHT RECLAMATION / recover navigation data / alloy-rich" + }, + { + "key": "ui.routes.power", + "value": "POWER SUBSTATION / reactivate relay / cell-rich" + }, + { + "key": "ui.routes.stay", + "value": "STAY IN THIS WARZONE" + }, + { + "key": "ui.pause.title", + "value": "PAUSED" + }, + { + "key": "ui.pause.help", + "value": "Sortie progress is saved at checkpoints and every successful recovery." + }, + { + "key": "ui.pause.resume", + "value": "RESUME" + }, + { + "key": "ui.pause.range", + "value": "RETURN FROM RANGE" + }, + { + "key": "ui.pause.quit", + "value": "SAVE & QUIT" + }, + { + "key": "report.ready", + "value": "Choose a chassis and configure your sortie." + }, + { + "key": "report.range", + "value": "Driving range complete. Your sortie inventory is ready in the hangar." + }, + { + "key": "report.destroyed", + "value": "Mech destroyed. Lost {0} equipped items or cargo stacks. The safe case returned {1} items or stacks." + }, + { + "key": "report.complete", + "value": "MISSION COMPLETE / all carried materials and schematics secured." + }, + { + "key": "report.extracted", + "value": "Extracted. Equipped gear, cargo and safe case contents recovered." + }, + { + "key": "notice.unfinished", + "value": "An unfinished sortie is available. Resume it first." + }, + { + "key": "notice.budget", + "value": "Loadout exceeds chassis mass or power budget." + }, + { + "key": "notice.researchFirst", + "value": "Selected equipment is unavailable. Choose an owned item or manufacture one." + }, + { + "key": "notice.saveFailed", + "value": "Could not save. Please try again." + }, + { + "key": "notice.range", + "value": "DRIVING RANGE / {0} switches perspective. {1} operates nearby devices." + }, + { + "key": "notice.enter", + "value": "Search the zone. Complete the objective to open the onward route." + }, + { + "key": "notice.podSecured", + "value": "CARGO SECURED / banked immediately. Sortie continues." + }, + { + "key": "notice.cargoUnavailable", + "value": "Cargo selection is no longer available." + }, + { + "key": "notice.twoSlots", + "value": "The marked items do not fit in the 2×2 pod." + }, + { + "key": "notice.holdFull", + "value": "Cargo hold full. Send two stacks or extract." + }, + { + "key": "notice.recovered", + "value": "RECOVERED / {0} ×{1}" + }, + { + "key": "notice.partBudget", + "value": "That part exceeds the chassis budget." + }, + { + "key": "notice.refit", + "value": "FIELD REFIT / {0}" + }, + { + "key": "notice.routeOpen", + "value": "ROUTE OPEN / continue searching, extract, or use the onward gate." + }, + { + "key": "notice.complete", + "value": "MISSION COMPLETE / return to the extraction pad with your cargo." + }, + { + "key": "notice.objectiveFirst", + "value": "Complete the zone objective to open the route." + }, + { + "key": "notice.rangeComplete", + "value": "Range objective complete. Extract or keep testing your chassis." + }, + { + "key": "notice.extract", + "value": "Use the extraction pad to bring your cargo home." + }, + { + "key": "notice.researched", + "value": "Research complete. The recipe is unlocked; use materials to manufacture equipment." + }, + { + "key": "notice.researchCost", + "value": "Research needs a recovered schematic, 2 alloy and 1 cell." + }, + { + "key": "notice.transportInbound", + "value": "TRANSPORT INBOUND / you can move and fight while waiting." + }, + { + "key": "notice.extractionCalled", + "value": "EXTRACTION CALLED / hold for 15 seconds, then return to board." + }, + { + "key": "notice.transportReady", + "value": "TRANSPORT READY / return to the orange pad and press {0} to board." + }, + { + "key": "notice.escortLost", + "value": "ENGINEERING CRAWLER LOST / extraction remains available." + }, + { + "key": "prompt.search", + "value": "{0} SEARCH SALVAGE" + }, + { + "key": "prompt.relayOnline", + "value": "RELAY ONLINE" + }, + { + "key": "prompt.objective", + "value": "{0} RECOVER NAVIGATION DATA" + }, + { + "key": "prompt.route", + "value": "{0} CHOOSE ONWARD ROUTE" + }, + { + "key": "prompt.routeLocked", + "value": "ROUTE LOCKED / COMPLETE OBJECTIVE" + }, + { + "key": "prompt.board", + "value": "{0} BOARD / BANK CARGO" + }, + { + "key": "prompt.inbound", + "value": "TRANSPORT INBOUND" + }, + { + "key": "prompt.call", + "value": "{0} CALL EXTRACTION / 15s" + }, + { + "key": "world.extraction", + "value": "EXTRACTION" + }, + { + "key": "world.relay", + "value": "RELAY / ONWARD ROUTE" + }, + { + "key": "world.overseer", + "value": "OVERSEER" + }, + { + "key": "world.bulwark", + "value": "BULWARK" + }, + { + "key": "world.skimmer", + "value": "SKIMMER" + }, + { + "key": "world.sentinel", + "value": "SENTINEL" + }, + { + "key": "world.escort", + "value": "ENGINEERING CRAWLER" + }, + { + "key": "chassis.chassis_0.name", + "value": "Kestrel / Light" + }, + { + "key": "chassis.chassis_0.description", + "value": "Aerial mobility" + }, + { + "key": "chassis.chassis_1.name", + "value": "Warden / Medium" + }, + { + "key": "chassis.chassis_1.description", + "value": "Balanced assault" + }, + { + "key": "chassis.chassis_2.name", + "value": "Bastion / Heavy" + }, + { + "key": "chassis.chassis_2.description", + "value": "Armored advance" + }, + { + "key": "equipment.rapid.name", + "value": "Hail / rapid cannon" + }, + { + "key": "equipment.coil.name", + "value": "Lance / coil rifle" + }, + { + "key": "equipment.siege.name", + "value": "Maul / heavy cannon" + }, + { + "key": "equipment.guided.name", + "value": "Wisp / guided salvo" + }, + { + "key": "equipment.pulse.name", + "value": "Breaker / pulse burst" + }, + { + "key": "equipment.efficient.name", + "value": "Cruise / efficient drive" + }, + { + "key": "equipment.vector.name", + "value": "Vector / overdrive" + }, + { + "key": "equipment.composite_guard.name", + "value": "Composite plating" + }, + { + "key": "equipment.reactive_guard.name", + "value": "Reactive shield" + }, + { + "key": "item.alloy", + "value": "ALLOY" + }, + { + "key": "item.cells", + "value": "CELLS" + }, + { + "key": "item.blueprint_boost_efficient", + "value": "SCHEMATIC / EFFICIENT DRIVE" + }, + { + "key": "item.blueprint_coil_capacitor", + "value": "SCHEMATIC / COIL CAPACITOR" + }, + { + "key": "item.blueprint_reactive_guard", + "value": "SCHEMATIC / REACTIVE SHIELD" + }, + { + "key": "ui.cargo.hold", + "value": "Cargo hold · 4×4" + }, + { + "key": "ui.cargo.case", + "value": "Ejection safe case · 2×2" + }, + { + "key": "ui.cargo.selection", + "value": "Selected {0} {1}×{2} cells Quantity {3}" + }, + { + "key": "ui.cargo.rotate", + "value": "Rotate (R)" + }, + { + "key": "ui.cargo.cancel", + "value": "Deselect" + }, + { + "key": "ui.cargo.install", + "value": "Install" + }, + { + "key": "ui.cargo.mark", + "value": "Mark for pod" + }, + { + "key": "ui.cargo.unmark", + "value": "Unmark" + }, + { + "key": "notice.installFailed", + "value": "Cannot install. Check the loadout budget and the space for the replaced part." + }, + { + "key": "notice.placement", + "value": "Cannot place here. Check bounds, occupied cells and stack quantity." + }, + { + "key": "ui.home.chooseItem", + "value": "Equip {0} · {1}" + }, + { + "key": "ui.home.manufacture", + "value": "Build {0} · {1} alloy / {2} cells" + }, + { + "key": "settings.title", + "value": "SETTINGS" + }, + { + "key": "settings.help", + "value": "Changes are saved when you select Apply. Language changes immediately." + }, + { + "key": "settings.language", + "value": "Language" + }, + { + "key": "settings.audio", + "value": "Audio" + }, + { + "key": "settings.display", + "value": "Display" + }, + { + "key": "settings.controls", + "value": "Controls" + }, + { + "key": "settings.volume", + "value": "Master volume" + }, + { + "key": "settings.volumeHelp", + "value": "Adjust the volume of all game sounds. Set to 0% to mute." + }, + { + "key": "settings.percent", + "value": "{0}%" + }, + { + "key": "settings.multiplier", + "value": "{0}×" + }, + { + "key": "settings.quality", + "value": "Graphics quality" + }, + { + "key": "settings.quality.veryLow", + "value": "Very low" + }, + { + "key": "settings.quality.low", + "value": "Low" + }, + { + "key": "settings.quality.medium", + "value": "Medium" + }, + { + "key": "settings.quality.high", + "value": "High" + }, + { + "key": "settings.quality.veryHigh", + "value": "Very high" + }, + { + "key": "settings.quality.ultra", + "value": "Ultra" + }, + { + "key": "settings.resolution", + "value": "Resolution" + }, + { + "key": "settings.windowMode", + "value": "Window mode" + }, + { + "key": "settings.windowed", + "value": "Windowed" + }, + { + "key": "settings.fullscreen", + "value": "Fullscreen" + }, + { + "key": "settings.displayHelp", + "value": "After applying a display change, confirm it within 15 seconds or the previous display settings return." + }, + { + "key": "settings.display.confirm", + "value": "Keep these display settings?" + }, + { + "key": "settings.display.countdown", + "value": "The previous display settings return in {0} seconds." + }, + { + "key": "settings.display.keep", + "value": "Keep and save" + }, + { + "key": "settings.display.revert", + "value": "Revert" + }, + { + "key": "settings.display.reverted", + "value": "The previous display settings have been restored. Other edits are still pending." + }, + { + "key": "settings.mouse", + "value": "Mouse" + }, + { + "key": "settings.sensitivity", + "value": "Mouse sensitivity" + }, + { + "key": "settings.aimSensitivity", + "value": "Aim sensitivity multiplier" + }, + { + "key": "settings.bindings", + "value": "Key bindings" + }, + { + "key": "settings.bind.help", + "value": "Select a binding, then press a keyboard key or mouse button. Esc cancels capture and always works as a menu back key. Duplicate bindings are rejected." + }, + { + "key": "settings.bind.waiting", + "value": "Waiting for input" + }, + { + "key": "settings.bind.prompt", + "value": "Press a new key or mouse button for {0}. Press Esc to cancel." + }, + { + "key": "settings.bind.cancel", + "value": "Cancel binding" + }, + { + "key": "settings.bind.changed", + "value": "The binding for {0} has changed. Select Apply to save it." + }, + { + "key": "settings.bind.cancelled", + "value": "Binding cancelled; the previous key is unchanged." + }, + { + "key": "settings.bind.conflict", + "value": "This key is already assigned to {0}. Choose another key." + }, + { + "key": "settings.bind.reserved", + "value": "Esc is reserved for closing menus and cancelling input. Choose another key." + }, + { + "key": "settings.bind.unsupported", + "value": "Choose a keyboard key or mouse button." + }, + { + "key": "settings.reset", + "value": "Restore defaults" + }, + { + "key": "settings.resetHint", + "value": "Default settings and bindings are ready. Select Apply to save them." + }, + { + "key": "settings.back", + "value": "Back / discard edits" + }, + { + "key": "settings.apply", + "value": "Apply and save" + }, + { + "key": "settings.saved", + "value": "Settings saved." + }, + { + "key": "settings.saveFailed", + "value": "Settings could not be saved. Check available disk space and folder access, then try again." + }, + { + "key": "settings.action.move.up", + "value": "Move forward" + }, + { + "key": "settings.action.move.down", + "value": "Move backward" + }, + { + "key": "settings.action.move.left", + "value": "Move left" + }, + { + "key": "settings.action.move.right", + "value": "Move right" + }, + { + "key": "settings.action.fire", + "value": "Primary weapon" + }, + { + "key": "settings.action.aim", + "value": "Precision aim" + }, + { + "key": "settings.action.boost", + "value": "Boost" + }, + { + "key": "settings.action.jump", + "value": "Jump / lift" + }, + { + "key": "settings.action.view", + "value": "Switch perspective" + }, + { + "key": "settings.action.interact", + "value": "Interact" + }, + { + "key": "settings.action.inventory", + "value": "Open cargo hold" + }, + { + "key": "settings.action.cargo", + "value": "Launch cargo pod" + }, + { + "key": "settings.action.pause", + "value": "Pause" + }, + { + "key": "settings.action.secondary", + "value": "Secondary weapon" + }, + { + "key": "settings.action.guard", + "value": "Active defense" + }, + { + "key": "settings.key.mouseLeft", + "value": "Mouse left" + }, + { + "key": "settings.key.mouseRight", + "value": "Mouse right" + }, + { + "key": "settings.key.mouseMiddle", + "value": "Mouse middle" + }, + { + "key": "settings.key.mouseForward", + "value": "Mouse forward" + }, + { + "key": "settings.key.mouseBack", + "value": "Mouse back" + }, + { + "key": "settings.key.space", + "value": "Space" + }, + { + "key": "settings.key.leftShift", + "value": "Left Shift" + }, + { + "key": "settings.key.rightShift", + "value": "Right Shift" + }, + { + "key": "settings.action.reload", + "value": "Reload" + }, + { + "key": "ui.combat.nav", + "value": "SECTOR MAP" + }, + { + "key": "ui.combat.legend", + "value": "◇ Objective ○ Extraction" + }, + { + "key": "ui.combat.ammo", + "value": "MAGAZINE / UNLIMITED RESERVE" + }, + { + "key": "ui.combat.reload", + "value": "[{0}] Reload" + }, + { + "key": "ui.combat.loading", + "value": "Reloading {0:0.0}s" + }, + { + "key": "ui.combat.ready", + "value": "Ready" + }, + { + "key": "ui.combat.charging", + "value": "Cooldown {0:0.0}s" + }, + { + "key": "ui.combat.lowEnergy", + "value": "Insufficient energy" + }, + { + "key": "ui.combat.cargo", + "value": "Hold {0}/16 cells · Safe case {1}/4 cells" + }, + { + "key": "ui.combat.empty", + "value": "Empty slot" + }, + { + "key": "range.resetSign", + "value": "RESET DESK" + }, + { + "key": "base.home.title", + "value": "FORWARD BASE / MECH STANDBY" + }, + { + "key": "base.navigation", + "value": "BASE TERMINAL" + }, + { + "key": "base.enterHangar", + "value": "ENTER HANGAR" + }, + { + "key": "base.deploy", + "value": "DEPLOYMENT" + }, + { + "key": "base.hangar", + "value": "HANGAR" + }, + { + "key": "base.backHome", + "value": "HOME" + }, + { + "key": "base.home.description", + "value": "Refit your mech, sort recovered loot, and prepare for the next sortie." + }, + { + "key": "base.baseStatus", + "value": "BASE RESOURCES" + }, + { + "key": "base.recipes", + "value": "RESEARCHED RECIPES" + }, + { + "key": "base.record", + "value": "Sorties {0} / Extractions {1} / Objectives {2}" + }, + { + "key": "base.activeRun", + "value": "A sortie is in progress. Base inventory is read-only until you return." + }, + { + "key": "base.menuHint", + "value": "Mouse select · Tab focus · Enter confirm · Esc back" + }, + { + "key": "base.tab.loadout", + "value": "LOADOUT" + }, + { + "key": "base.tab.warehouse", + "value": "STASH" + }, + { + "key": "base.tab.research", + "value": "RESEARCH" + }, + { + "key": "base.preview", + "value": "MECH PREVIEW" + }, + { + "key": "base.previewHint", + "value": "Drag an empty part of the preview to rotate the mech." + }, + { + "key": "base.chassisStats", + "value": "Armor {0} / Cruise {1} m/s / Boost {2} m/s" + }, + { + "key": "base.chassis", + "value": "CHASSIS" + }, + { + "key": "base.installed", + "value": "EQUIPMENT" + }, + { + "key": "base.borrowed", + "value": "LOANED, RETURNED AT BASE" + }, + { + "key": "base.partStats", + "value": "Damage {0} / Mass {1} / Power {2}" + }, + { + "key": "base.compare", + "value": "Compared to current: damage {0:+0.##;-0.##;0}, mass {1:+0.##;-0.##;0}, power {2:+0.##;-0.##;0}" + }, + { + "key": "base.useBorrowed", + "value": "Use a basic loaned part" + }, + { + "key": "base.preparation", + "value": "CARRY-IN" + }, + { + "key": "base.cargo", + "value": "CARGO" + }, + { + "key": "base.safeCase", + "value": "SAFE CASE" + }, + { + "key": "base.reserved", + "value": "RESERVED" + }, + { + "key": "base.researchHelp", + "value": "Keep the physical blueprint. Research unlocks a recipe permanently; manufacture uses unreserved stash materials to create an item." + }, + { + "key": "base.researchCost", + "value": "Research: 2 Alloy + 1 Cell" + }, + { + "key": "base.manufacture", + "value": "MANUFACTURE" + }, + { + "key": "base.deployHelp", + "value": "Deploying transfers selected equipment and cargo out of the stash. Destruction protects the safe case; equipment and loot outside it are lost. Loaned parts are returned at base." + }, + { + "key": "base.confirm", + "value": "CONFIRM" + }, + { + "key": "warehouse.title", + "value": "BASE STASH" + }, + { + "key": "warehouse.pending", + "value": "PENDING {0}" + }, + { + "key": "warehouse.occupancy", + "value": "Occupied {0} / {1} cells" + }, + { + "key": "warehouse.stock", + "value": "Alloy {0} / Cells {1}" + }, + { + "key": "warehouse.controls", + "value": "Drag to move · R rotate preview · Ctrl+click stage/unstage · Shift+drag split" + }, + { + "key": "warehouse.filter.all", + "value": "ALL" + }, + { + "key": "warehouse.filter.equipment", + "value": "GEAR" + }, + { + "key": "warehouse.filter.material", + "value": "MATERIAL" + }, + { + "key": "warehouse.filter.blueprint", + "value": "BLUEPRINT" + }, + { + "key": "warehouse.search", + "value": "Find" + }, + { + "key": "warehouse.sort", + "value": "SORT" + }, + { + "key": "warehouse.claim", + "value": "CLAIM" + }, + { + "key": "warehouse.claimAll", + "value": "CLAIM WHAT FITS" + }, + { + "key": "warehouse.inboxHelp", + "value": "Recovered items are saved. Make room and claim them before starting a new sortie." + }, + { + "key": "warehouse.emptyInbox", + "value": "No pending items" + }, + { + "key": "warehouse.selectHelp", + "value": "Select an item to inspect it, then click an empty cell to place it. Carry-in reservations transfer only when you deploy." + }, + { + "key": "warehouse.itemInfo", + "value": "Size {0}×{1} / Quantity {2} / ID {3}" + }, + { + "key": "warehouse.equip", + "value": "EQUIP" + }, + { + "key": "warehouse.rotate", + "value": "ROTATE PREVIEW" + }, + { + "key": "warehouse.unstage", + "value": "UNSTAGE" + }, + { + "key": "warehouse.recycle", + "value": "RECYCLE FOR 2 ALLOY" + }, + { + "key": "warehouse.discard", + "value": "DISCARD" + }, + { + "key": "warehouse.discardConfirm", + "value": "Permanently discard {0} ×{1}? This frees its storage space." + }, + { + "key": "warehouse.sortFirst", + "value": "Claim or discard pending items before starting a new sortie." + }, + { + "key": "warehouse.incompatible", + "value": "This item does not fit that equipment slot." + }, + { + "key": "warehouse.noSpace", + "value": "No suitable space is available. Items and materials were kept." + }, + { + "key": "warehouse.missing", + "value": "A selected item is unavailable. Select your equipment again." + }, + { + "key": "warehouse.alreadyEquipped", + "value": "This item is reserved in an equipment slot. Choose another part or a loaned part first." + }, + { + "key": "warehouse.manufactureFailed", + "value": "Cannot manufacture. Check research, unreserved materials, and space for the output." + }, + { + "key": "warehouse.saved", + "value": "Changes saved." + }, + { + "key": "warehouse.split", + "value": "SPLIT MATERIALS" + }, + { + "key": "warehouse.splitHelp", + "value": "Choose how many to move. The remainder stays in its original stack." + }, + { + "key": "warehouse.quantity", + "value": "Quantity" + }, + { + "key": "warehouse.recoveredPending", + "value": "Pod cargo recovered. {0} pending items can be sorted after you return." + }, + { + "key": "warehouse.available", + "value": "Available after reservations: Alloy {0} / Cells {1}" + }, + { + "key": "base.boostStats", + "value": "Mobility ×{0:0.##} / Mass {1} / Power {2}" + }, + { + "key": "base.moduleStats", + "value": "Mass {0} / Power {1}" + }, + { + "key": "base.boostCompare", + "value": "Compared to current: mobility {0:+0.##;-0.##;0}, mass {1:+0.##;-0.##;0}, power {2:+0.##;-0.##;0}" + }, + { + "key": "base.moduleCompare", + "value": "Compared to current: mass {0:+0.##;-0.##;0}, power {1:+0.##;-0.##;0}" + }, + { + "key": "base.cancel", + "value": "CANCEL" + }, + { + "key": "hangar.equipment", + "value": "EQUIPMENT" + }, + { + "key": "hangar.carry", + "value": "CARRIED ITEMS" + }, + { + "key": "hangar.draft", + "value": "Preparation · items transfer when deploying" + }, + { + "key": "hangar.readOnly", + "value": "Active sortie · actual loadout / read only" + }, + { + "key": "hangar.owned", + "value": "OWNED" + }, + { + "key": "hangar.emptySlot", + "value": "EMPTY" + }, + { + "key": "hangar.reserved", + "value": "RESERVED" + }, + { + "key": "hangar.capacity", + "value": "{0} / {1} cells" + }, + { + "key": "hangar.protected", + "value": "PROTECTED · retained if the mech is destroyed" + }, + { + "key": "hangar.details", + "value": "Selected item / actions" + }, + { + "key": "ue.subtitle", + "value": "MECH EXTRACTION / ORBITAL FRONTIER" + }, + { + "key": "ue.baseTitle", + "value": "HANGAR COMMAND" + }, + { + "key": "ue.enterHangar", + "value": "ENTER HANGAR" + }, + { + "key": "ue.deploy", + "value": "DEPLOY" + }, + { + "key": "ue.resume", + "value": "RESUME SORTIE" + }, + { + "key": "ue.training", + "value": "TRAINING RANGE" + }, + { + "key": "ue.research", + "value": "RESEARCH & FABRICATION" + }, + { + "key": "ue.settings", + "value": "SETTINGS" + }, + { + "key": "ue.performance", + "value": "PERFORMANCE" + }, + { + "key": "ue.perfOn", + "value": "ON" + }, + { + "key": "ue.perfOff", + "value": "OFF" + }, + { + "key": "ue.perfPosition", + "value": "POSITION" + }, + { + "key": "ue.perfTopRight", + "value": "TOP RIGHT" + }, + { + "key": "ue.perfTopLeft", + "value": "TOP LEFT" + }, + { + "key": "ue.perfBottomRight", + "value": "BOTTOM RIGHT" + }, + { + "key": "ue.perfBottomLeft", + "value": "BOTTOM LEFT" + }, + { + "key": "ue.perfHint", + "value": "60 FPS BUDGET: 16.67 ms / — MEANS UNAVAILABLE" + }, + { + "key": "ue.perfFps", + "value": "FPS" + }, + { + "key": "ue.perfFrame", + "value": "FRAME" + }, + { + "key": "ue.perfGpu", + "value": "GPU" + }, + { + "key": "ue.perfGame", + "value": "CPU GAME" + }, + { + "key": "ue.perfRender", + "value": "CPU RENDER" + }, + { + "key": "ue.perfRhi", + "value": "RHI" + }, + { + "key": "ue.quit", + "value": "EXIT GAME" + }, + { + "key": "ue.baseHint", + "value": "Equip. Search. Extract alive." + }, + { + "key": "ue.pilot", + "value": "PILOT DOSSIER" + }, + { + "key": "ue.warehouse", + "value": "BASE WAREHOUSE" + }, + { + "key": "ue.pending", + "value": "RECOVERY INBOX" + }, + { + "key": "ue.sorties", + "value": "SORTIES" + }, + { + "key": "ue.returns", + "value": "RETURNS" + }, + { + "key": "ue.activeRun", + "value": "ACTIVE SORTIE / RESUMABLE" + }, + { + "key": "ue.standby", + "value": "BASE STANDBY" + }, + { + "key": "ue.carryHint", + "value": "DRAG TO PLACE / R ROTATES / SECURE CASE SURVIVES" + }, + { + "key": "ue.fieldSystems", + "value": "FIELD SYSTEMS" + }, + { + "key": "ue.fieldHint", + "value": "INSTALL REAL PARTS / SELECT CARGO FOR RECOVERY" + }, + { + "key": "ue.fieldEquipment", + "value": "INSTALLED PARTS / FIELD MODIFICATIONS" + }, + { + "key": "ue.warehouseHint", + "value": "SCROLL TO BROWSE / LIMITED CAPACITY / UNIQUE ITEMS" + }, + { + "key": "ue.primary", + "value": "PRIMARY" + }, + { + "key": "ue.secondary", + "value": "SECONDARY" + }, + { + "key": "ue.booster", + "value": "BOOSTER" + }, + { + "key": "ue.guard", + "value": "GUARD" + }, + { + "key": "ue.mass", + "value": "MASS" + }, + { + "key": "ue.power", + "value": "POWER" + }, + { + "key": "ue.cargo", + "value": "CARGO" + }, + { + "key": "ue.safeCase", + "value": "SECURE CASE / 2 × 2" + }, + { + "key": "ue.cellsUsed", + "value": "CELLS USED" + }, + { + "key": "ue.sort", + "value": "SORT STASH" + }, + { + "key": "ue.podSelect", + "value": "SELECT RECOVERY CARGO" + }, + { + "key": "ue.podRule", + "value": "ONE POD PER SORTIE / 2 × 2 / UP TO FOUR ITEMS" + }, + { + "key": "ue.holdPod", + "value": "CLOSE INVENTORY, STAND STILL, HOLD" + }, + { + "key": "ue.selected", + "value": "SELECTED" + }, + { + "key": "ue.uninstall", + "value": "UNINSTALL TO CARGO" + }, + { + "key": "ue.borrow", + "value": "BORROW BASELINE PART" + }, + { + "key": "ue.borrowed", + "value": "BORROWED" + }, + { + "key": "ue.equip", + "value": "EQUIP ITEM" + }, + { + "key": "ue.toCargo", + "value": "PREPARE CARGO" + }, + { + "key": "ue.toSafe", + "value": "PREPARE SECURE CASE" + }, + { + "key": "ue.unprepare", + "value": "CANCEL PREPARATION" + }, + { + "key": "ue.install", + "value": "INSTALL PART" + }, + { + "key": "ue.transfer", + "value": "TRANSFER CONTAINER" + }, + { + "key": "ue.recycle", + "value": "RECYCLE KNOWN BLUEPRINT" + }, + { + "key": "ue.dragHint", + "value": "DRAG / R ROTATES / QUANTITY SPLITS STACKS" + }, + { + "key": "ue.selectItem", + "value": "SELECT AN ITEM FOR ACTIONS" + }, + { + "key": "ue.back", + "value": "BACK" + }, + { + "key": "ue.researchHint", + "value": "RESEARCH COSTS 2 ALLOY + 1 CELL. FABRICATION USES PHYSICAL STOCK." + }, + { + "key": "ue.manufacture", + "value": "FABRICATE PART" + }, + { + "key": "ue.inboxHint", + "value": "OVERFLOW IS PRESERVED HERE. CLEAR INBOX BEFORE A NEW SORTIE." + }, + { + "key": "ue.claim", + "value": "CLAIM" + }, + { + "key": "ue.claimAll", + "value": "CLAIM ALL" + }, + { + "key": "ue.discard", + "value": "DISCARD" + }, + { + "key": "ue.confirmDiscard", + "value": "CLICK DISCARD AGAIN TO CONFIRM." + }, + { + "key": "ue.settingsHint", + "value": "DISPLAY, MOUSE, AUDIO AND KEY BINDINGS APPLY AND SAVE." + }, + { + "key": "ue.display", + "value": "DISPLAY" + }, + { + "key": "ue.fullscreen", + "value": "TOGGLE FULLSCREEN / WINDOWED" + }, + { + "key": "ue.sensitivity", + "value": "MOUSE SENSITIVITY" + }, + { + "key": "ue.aimSensitivity", + "value": "AIM SENSITIVITY" + }, + { + "key": "ue.fire", + "value": "PRIMARY FIRE" + }, + { + "key": "ue.aim", + "value": "AIM" + }, + { + "key": "ue.invertY", + "value": "INVERT MOUSE Y" + }, + { + "key": "ue.controls", + "value": "KEYBOARD & MOUSE" + }, + { + "key": "ue.forward", + "value": "FORWARD" + }, + { + "key": "ue.backward", + "value": "BACKWARD" + }, + { + "key": "ue.left", + "value": "LEFT" + }, + { + "key": "ue.right", + "value": "RIGHT" + }, + { + "key": "ue.jump", + "value": "JUMP / LIGHT-CHASSIS LIFT" + }, + { + "key": "ue.boost", + "value": "BOOST" + }, + { + "key": "ue.reload", + "value": "RELOAD" + }, + { + "key": "ue.view", + "value": "COCKPIT / SHOULDER VIEW" + }, + { + "key": "ue.interact", + "value": "INTERACT / BOARD" + }, + { + "key": "ue.inventory", + "value": "INVENTORY" + }, + { + "key": "ue.pod", + "value": "RECOVERY POD (HOLD)" + }, + { + "key": "ue.pressKey", + "value": "PRESS A KEY / ESC CANCELS" + }, + { + "key": "ue.keyConflict", + "value": "THAT KEY IS ALREADY ASSIGNED." + }, + { + "key": "ue.volume", + "value": "VOLUME" + }, + { + "key": "ue.quality", + "value": "CYCLE QUALITY" + }, + { + "key": "ue.paused", + "value": "SORTIE PAUSED" + }, + { + "key": "ue.upgrade", + "value": "FIELD UPGRADE" + }, + { + "key": "ue.pauseHint", + "value": "REVIEW YOUR CARGO OR SAVE YOUR SORTIE AND RETURN TO BASE." + }, + { + "key": "ue.saveAndHome", + "value": "SAVE & RETURN HOME" + }, + { + "key": "ue.upgradeDamage", + "value": "FIREPOWER / INCREASE DAMAGE" + }, + { + "key": "ue.upgradeRepair", + "value": "FIELD REPAIR / RESTORE ARMOR" + }, + { + "key": "ue.upgradeEnergy", + "value": "ENERGY SERVICE / RESTORE ENERGY" + }, + { + "key": "ue.armor", + "value": "ARMOR" + }, + { + "key": "ue.energy", + "value": "ENERGY" + }, + { + "key": "ue.stage", + "value": "SECTOR" + }, + { + "key": "ue.salvage", + "value": "CARGO" + }, + { + "key": "ue.routeDone", + "value": "ROUTE COMPLETE / EXTRACT TO BANK LOOT" + }, + { + "key": "ue.objectiveDone", + "value": "OBJECTIVE COMPLETE / NEXT SECTOR UNLOCKED" + }, + { + "key": "ue.findObjective", + "value": "SEARCH SALVAGE AND FIND THE OBJECTIVE TERMINAL" + }, + { + "key": "ue.podUsed", + "value": "RECOVERY POD: USED" + }, + { + "key": "ue.podAvailable", + "value": "RECOVERY POD: 1 / 1" + }, + { + "key": "ue.reloading", + "value": "RELOADING" + }, + { + "key": "ue.wait", + "value": "EXTRACTION ARRIVES IN" + }, + { + "key": "ue.podHolding", + "value": "RECOVERY ARMING / HOLD POSITION" + }, + { + "key": "ue.footer", + "value": "KEYBOARD & MOUSE / ESC BACK / SEARCH, FIGHT, EXTRACT" + }, + { + "key": "ue.completed", + "value": "COMPLETED ROUTES" + }, + { + "key": "ue.rangeStats", + "value": "RANGE / HITS / GATES / TIME" + }, + { + "key": "ue.rangeReset", + "value": "RANGE TIMER AND HITS RESET." + }, + { + "key": "ue.courseGate", + "value": "TIMED GATE" + }, + { + "key": "ue.extraction", + "value": "EXTRACTION PAD" + }, + { + "key": "ue.routeBoss", + "value": "BOSS ROUTE" + }, + { + "key": "ue.routeEscort", + "value": "ESCORT ROUTE" + }, + { + "key": "ue.rangeResetLabel", + "value": "RESET RANGE" + }, + { + "key": "ue.saved", + "value": "SAVED" + }, + { + "key": "ue.invalid", + "value": "THAT ACTION IS NOT AVAILABLE." + }, + { + "key": "ue.missing", + "value": "ITEM NOT FOUND." + }, + { + "key": "ue.noSpace", + "value": "NOT ENOUGH SPACE OR ITEM DOES NOT FIT." + }, + { + "key": "ue.locked", + "value": "ACTION LOCKED IN THE CURRENT STATE." + }, + { + "key": "ue.budget", + "value": "LOADOUT EXCEEDS CHASSIS MASS OR POWER BUDGET." + }, + { + "key": "ue.funds", + "value": "INSUFFICIENT AVAILABLE MATERIALS." + }, + { + "key": "ue.duplicate", + "value": "THIS SOURCE OR EVENT HAS ALREADY BEEN PROCESSED." + }, + { + "key": "ue.saveFailed", + "value": "SAVE FAILED. TRANSACTION NOT COMMITTED. CHECK DISK." + }, + { + "key": "ue.corrupt", + "value": "PROFILE AND BACKUP UNREADABLE. ORIGINAL FILES PRESERVED." + }, + { + "key": "ue.unsupported", + "value": "UNSUPPORTED SAVE VERSION." + }, + { + "key": "ue.destroyed", + "value": "MECH DESTROYED. SECURE CASE PRESERVED; OTHER CARGO LOST." + }, + { + "key": "ue.extracted", + "value": "EXTRACTED. RECOVERED ITEMS BANKED." + }, + { + "key": "ue.nothingNear", + "value": "MOVE CLOSER TO SALVAGE, TERMINALS OR EXTRACTION." + }, + { + "key": "ue.loot", + "value": "SALVAGE RECOVERED" + }, + { + "key": "ue.called", + "value": "EXTRACTION CALLED. WAIT, THEN BOARD MANUALLY." + }, + { + "key": "ue.finalObjective", + "value": "DEFEAT THE BOSS OR COMPLETE ESCORT, THEN EXTRACT." + }, + { + "key": "ue.escort", + "value": "ESCORT TRANSPORT" + }, + { + "key": "ue.explore", + "value": "SEARCH CRATES / FIGHT / ACTIVATE TERMINALS / GREEN PAD EXTRACTS" + }, + { + "key": "ue.search", + "value": "SEARCH" + }, + { + "key": "ue.board", + "value": "BOARD & EXTRACT" + }, + { + "key": "ue.call", + "value": "CALL EXTRACTION" + }, + { + "key": "ue.nextStage", + "value": "NEXT SECTOR (RIGHT GATE: ESCORT ROUTE)" + }, + { + "key": "ue.activate", + "value": "ACTIVATE OBJECTIVE TERMINAL" + }, + { + "key": "ue.readyToBoard", + "value": "TRANSPORT READY. RETURN TO PAD AND PRESS INTERACT TO BOARD." + }, + { + "key": "ue.podSent", + "value": "SELECTED CARGO RECOVERED. THIS SORTIE'S POD IS USED." + }, + { + "key": "ue.podInterrupted", + "value": "RECOVERY INTERRUPTED BY MOVEMENT, AIRBORNE STATE OR DAMAGE." + }, + { + "key": "ue.escortLost", + "value": "ESCORT TRANSPORT DESTROYED." + } + ] +} diff --git a/Content/IRONSPORE/Raw/Localization/zh-Hans.json b/Content/IRONSPORE/Raw/Localization/zh-Hans.json new file mode 100644 index 0000000..5da20cb --- /dev/null +++ b/Content/IRONSPORE/Raw/Localization/zh-Hans.json @@ -0,0 +1,1920 @@ +{ + "entries": [ + { + "key": "range.choose", + "value": "左侧打靶,右侧计时穿门。越过靶区可练习回收。" + }, + { + "key": "range.start", + "value": "01 / 计时起点" + }, + { + "key": "range.upper", + "value": "02 / 高台门" + }, + { + "key": "range.landing", + "value": "在标线内停稳" + }, + { + "key": "range.gallery", + "value": "射击训练" + }, + { + "key": "range.salvage", + "value": "回收练习区" + }, + { + "key": "range.material", + "value": "材料示范箱" + }, + { + "key": "range.parts", + "value": "试装部件箱" + }, + { + "key": "range.targetGuide", + "value": "射击翻板和横移靶。已命中 {0} 次。到复位台可重练。" + }, + { + "key": "range.brake", + "value": "在高台尽头的标线内停稳。" + }, + { + "key": "range.finish", + "value": "本轮用时 {0:F1} 秒。再次穿过起点可重练。" + }, + { + "key": "range.recoveryGuide", + "value": "按 [{0}] 搜索箱子,按 [{1}] 选择货物,原地按住 [{2}] 发射。" + }, + { + "key": "range.recoverySent", + "value": "货舱已发射。前往橙色平台按 [{0}] 呼叫运输机。" + }, + { + "key": "range.board", + "value": "运输机已抵达。前往橙色平台按 [{0}] 登机。" + }, + { + "key": "range.reset", + "value": "重置练习 [{0}]" + }, + { + "key": "range.gate", + "value": "已通过起点,沿标线前进。" + }, + { + "key": "range.hit", + "value": "靶标命中 {0} 次" + }, + { + "key": "range.ready", + "value": "训练设备已复位。" + }, + { + "key": "ui.home.subtitle", + "value": "机甲回收行动" + }, + { + "key": "ui.home.loadout", + "value": "出击配装" + }, + { + "key": "ui.slot.0", + "value": "主武器" + }, + { + "key": "ui.slot.1", + "value": "副武装" + }, + { + "key": "ui.slot.2", + "value": "推进器" + }, + { + "key": "ui.slot.3", + "value": "防护模块" + }, + { + "key": "ui.home.part", + "value": "{0} / {1} [载重 {2} / 功率 {3}]" + }, + { + "key": "ui.home.budget", + "value": "载重 {0} / {1} 功率 {2} / {3}" + }, + { + "key": "ui.home.deploy", + "value": "开始出击" + }, + { + "key": "ui.home.range", + "value": "驾驶试验场" + }, + { + "key": "ui.home.resume", + "value": "继续出击" + }, + { + "key": "ui.home.locked", + "value": "所选装备库存不足。基础槽位可借用救济装,试验场可试用所有型号。" + }, + { + "key": "ui.home.moveHint", + "value": "{0} 移动 / 鼠标瞄准 / {1} 切换视角 / {2} 推进 / {3} 跳跃与升空" + }, + { + "key": "ui.home.fireHint", + "value": "{0} 主武器 / {1} 精瞄 / {2} 副武装 / {3} 护盾 / {4} 交互" + }, + { + "key": "ui.home.inventory", + "value": "机库库存" + }, + { + "key": "ui.home.stock", + "value": "合金 {0} 电芯 {1} 蓝图 {2} 撤离 {3} 终局达成 {4}" + }, + { + "key": "ui.home.researched", + "value": "{0} / 已研究" + }, + { + "key": "ui.home.research", + "value": "{0} / 研究:2 合金 + 1 电芯" + }, + { + "key": "ui.home.quit", + "value": "退出游戏" + }, + { + "key": "ui.home.language", + "value": "语言 / Language · {0}" + }, + { + "key": "ui.home.languageHelp", + "value": "选择语言" + }, + { + "key": "ui.language.current", + "value": "{0} / 当前语言" + }, + { + "key": "ui.hud.operation", + "value": "当前任务" + }, + { + "key": "ui.hud.armor", + "value": "装甲 {0}" + }, + { + "key": "ui.hud.energy", + "value": "能源 {0}" + }, + { + "key": "ui.hud.shield", + "value": "护盾生效" + }, + { + "key": "ui.hud.podUsed", + "value": "回收舱已用 / 1 次机会" + }, + { + "key": "ui.hud.podPreparing", + "value": "正在准备回收舱…" + }, + { + "key": "ui.hud.podReady", + "value": "回收舱就绪 / {0} 选择货物" + }, + { + "key": "ui.hud.hint", + "value": "{0} 视角 {1} 推进 {2} 升空 {3} 副武装 {4} 护盾 {5} 交互 {6} 货仓 按住 {7} 发射回收舱 {8} 暂停" + }, + { + "key": "ui.hud.zone", + "value": "战区 {0} / 5" + }, + { + "key": "ui.hud.status", + "value": "{0} {1}" + }, + { + "key": "ui.hud.complete", + "value": "任务完成 / 携带货物前往撤离点" + }, + { + "key": "ui.hud.boss", + "value": "击败督军" + }, + { + "key": "ui.hud.escort", + "value": "护送工程履带车抵达远端闸门" + }, + { + "key": "ui.hud.routeOpen", + "value": "路线已开放 / 搜索、撤离或前往下一站" + }, + { + "key": "ui.hud.objective", + "value": "前往中继站回收导航数据" + }, + { + "key": "ui.hud.cockpit", + "value": "第一人称" + }, + { + "key": "ui.hud.shoulder", + "value": "越肩视角" + }, + { + "key": "ui.hud.telemetry", + "value": "{0}\n{1} / {2}\n货仓 {3} / 16\n{4}" + }, + { + "key": "ui.hud.transportReady", + "value": "运输机已到达 / 前往平台登机" + }, + { + "key": "ui.hud.transportEta", + "value": "运输机抵达倒计时 {0} 秒" + }, + { + "key": "ui.hud.extractionAvailable", + "value": "可随时撤离 / 橙色平台" + }, + { + "key": "ui.cargo.title", + "value": "货仓与弹射式安全箱" + }, + { + "key": "ui.cargo.used", + "value": "主动货舱已发射。机毁时只有弹射式安全箱中的物品返航。" + }, + { + "key": "ui.cargo.help", + "value": "拖动物品或选中后点击目标格,按 R 旋转。安全箱在机毁时自动返航。标记货物后关闭面板,原地按住 {0} 三秒发射主动货舱。" + }, + { + "key": "ui.cargo.stack", + "value": "{0} {1} ×{2}" + }, + { + "key": "ui.cargo.selected", + "value": "[已选择]" + }, + { + "key": "ui.cargo.capacity", + "value": "普通货仓 {0}/16 格 主动货舱 {1}/4 格" + }, + { + "key": "ui.return", + "value": "返回驾驶" + }, + { + "key": "ui.refit.title", + "value": "战地改装" + }, + { + "key": "ui.refit.help", + "value": "已回收到替换部件。安装前请比较载重和功率。" + }, + { + "key": "ui.refit.equipped", + "value": "当前部件 / {0} 载重 {1} / 功率 {2}" + }, + { + "key": "ui.refit.recovered", + "value": "回收部件 / {0} 载重 {1} / 功率 {2}" + }, + { + "key": "ui.refit.install", + "value": "安装回收部件" + }, + { + "key": "ui.refit.keep", + "value": "保留当前配装" + }, + { + "key": "ui.routes.split", + "value": "选择终局方向" + }, + { + "key": "ui.routes.title", + "value": "选择下一战区" + }, + { + "key": "ui.routes.splitHelp", + "value": "已在两个战区积累装备。选择适合当前机甲的终局任务。" + }, + { + "key": "ui.routes.help", + "value": "可以预知任务与物资类别,具体战利品需进入战区后搜索。" + }, + { + "key": "ui.routes.boss", + "value": "督军路线 / 突击中继站 / 武器物资" + }, + { + "key": "ui.routes.escort", + "value": "工程护送 / 恢复补给线 / 推进器物资" + }, + { + "key": "ui.routes.freight", + "value": "货运回收区 / 回收导航数据 / 合金丰富" + }, + { + "key": "ui.routes.power", + "value": "能源变电站 / 重启中继站 / 电芯丰富" + }, + { + "key": "ui.routes.stay", + "value": "留在当前战区" + }, + { + "key": "ui.pause.title", + "value": "已暂停" + }, + { + "key": "ui.pause.help", + "value": "出击进度会在检查点及每次成功回收后保存。" + }, + { + "key": "ui.pause.resume", + "value": "继续游戏" + }, + { + "key": "ui.pause.range", + "value": "返回机库" + }, + { + "key": "ui.pause.quit", + "value": "保存并退出" + }, + { + "key": "report.ready", + "value": "选择底盘并配置出击装备。" + }, + { + "key": "report.range", + "value": "试验场测试结束。机库出击库存已就绪。" + }, + { + "key": "report.destroyed", + "value": "机体损毁。损失 {0} 件装备或货物,弹射式安全箱送回 {1} 件物品。" + }, + { + "key": "report.complete", + "value": "任务完成 / 随身材料与蓝图已全部入库。" + }, + { + "key": "report.extracted", + "value": "撤离成功。已装装备、普通货仓和安全箱物品已回收。" + }, + { + "key": "notice.unfinished", + "value": "还有未完成的出击,请先继续该次出击。" + }, + { + "key": "notice.budget", + "value": "配装超出底盘的载重或功率上限。" + }, + { + "key": "notice.researchFirst", + "value": "所选装备实物不足,请从仓库选择装备或制造。" + }, + { + "key": "notice.saveFailed", + "value": "保存失败,请重试。" + }, + { + "key": "notice.range", + "value": "驾驶试验场 / {0} 切换视角,{1} 操作附近设备。" + }, + { + "key": "notice.enter", + "value": "搜索战区,完成任务后开放下一站路线。" + }, + { + "key": "notice.podSecured", + "value": "货物回收成功 / 已立即入库,可继续出击。" + }, + { + "key": "notice.cargoUnavailable", + "value": "所选货物已不可用,请重新选择。" + }, + { + "key": "notice.twoSlots", + "value": "所选物品放不进 2×2 主动货舱。" + }, + { + "key": "notice.holdFull", + "value": "随身货仓已满,请发射回收舱或整机撤离。" + }, + { + "key": "notice.recovered", + "value": "已回收 / {0} ×{1}" + }, + { + "key": "notice.partBudget", + "value": "该部件会使配装超出底盘预算。" + }, + { + "key": "notice.refit", + "value": "战地改装完成 / {0}" + }, + { + "key": "notice.routeOpen", + "value": "路线已开放 / 继续搜索、撤离,或前往下一站闸门。" + }, + { + "key": "notice.complete", + "value": "任务完成 / 请携带货物返回撤离平台。" + }, + { + "key": "notice.objectiveFirst", + "value": "请先完成本战区任务,以开放路线。" + }, + { + "key": "notice.rangeComplete", + "value": "试验场任务完成。可以撤离或继续测试机甲。" + }, + { + "key": "notice.extract", + "value": "使用撤离平台将随身货物带回机库。" + }, + { + "key": "notice.researched", + "value": "研究完成:配方已解锁,可消耗材料制造装备。" + }, + { + "key": "notice.researchCost", + "value": "研究需要已回收的蓝图、2 合金和 1 电芯。" + }, + { + "key": "notice.transportInbound", + "value": "运输机正在途中 / 等待时可以自由移动与作战。" + }, + { + "key": "notice.extractionCalled", + "value": "已呼叫撤离 / 等待 15 秒后返回平台登机。" + }, + { + "key": "notice.transportReady", + "value": "接应运输机已抵达 / 返回橙色撤离坪,按 {0} 登机。" + }, + { + "key": "notice.escortLost", + "value": "工程履带车损毁 / 仍可前往撤离点撤离。" + }, + { + "key": "prompt.search", + "value": "{0} 搜索物资" + }, + { + "key": "prompt.relayOnline", + "value": "中继站已上线" + }, + { + "key": "prompt.objective", + "value": "{0} 取回导航数据" + }, + { + "key": "prompt.route", + "value": "{0} 选择下一站路线" + }, + { + "key": "prompt.routeLocked", + "value": "路线未开放 / 请先完成任务" + }, + { + "key": "prompt.board", + "value": "{0} 登机 / 带回随身物资" + }, + { + "key": "prompt.inbound", + "value": "运输机正在途中" + }, + { + "key": "prompt.call", + "value": "{0} 呼叫接应 / 15 秒" + }, + { + "key": "world.extraction", + "value": "撤离点" + }, + { + "key": "world.relay", + "value": "中继站 / 下一站路线" + }, + { + "key": "world.overseer", + "value": "督军" + }, + { + "key": "world.bulwark", + "value": "重装壁垒" + }, + { + "key": "world.skimmer", + "value": "掠行者" + }, + { + "key": "world.sentinel", + "value": "哨兵" + }, + { + "key": "world.escort", + "value": "工程履带车" + }, + { + "key": "chassis.chassis_0.name", + "value": "红隼 / 轻型" + }, + { + "key": "chassis.chassis_0.description", + "value": "空中机动" + }, + { + "key": "chassis.chassis_1.name", + "value": "守卫 / 中型" + }, + { + "key": "chassis.chassis_1.description", + "value": "均衡突击" + }, + { + "key": "chassis.chassis_2.name", + "value": "堡垒 / 重型" + }, + { + "key": "chassis.chassis_2.description", + "value": "重装推进" + }, + { + "key": "equipment.rapid.name", + "value": "冰雹 / 速射炮" + }, + { + "key": "equipment.coil.name", + "value": "长矛 / 电磁步枪" + }, + { + "key": "equipment.siege.name", + "value": "重锤 / 重型火炮" + }, + { + "key": "equipment.guided.name", + "value": "磷火 / 制导齐射" + }, + { + "key": "equipment.pulse.name", + "value": "破阵 / 脉冲爆发" + }, + { + "key": "equipment.efficient.name", + "value": "巡航 / 高效推进器" + }, + { + "key": "equipment.vector.name", + "value": "矢量 / 超载推进器" + }, + { + "key": "equipment.composite_guard.name", + "value": "复合装甲" + }, + { + "key": "equipment.reactive_guard.name", + "value": "反应护盾" + }, + { + "key": "item.alloy", + "value": "合金" + }, + { + "key": "item.cells", + "value": "电芯" + }, + { + "key": "item.blueprint_boost_efficient", + "value": "蓝图 / 高效推进器" + }, + { + "key": "item.blueprint_coil_capacitor", + "value": "蓝图 / 电磁电容" + }, + { + "key": "item.blueprint_reactive_guard", + "value": "蓝图 / 反应护盾" + }, + { + "key": "ui.cargo.hold", + "value": "普通货仓 · 4×4" + }, + { + "key": "ui.cargo.case", + "value": "弹射式安全箱 · 2×2" + }, + { + "key": "ui.cargo.selection", + "value": "已选 {0} {1}×{2} 格 移动数量 {3}" + }, + { + "key": "ui.cargo.rotate", + "value": "旋转 (R)" + }, + { + "key": "ui.cargo.cancel", + "value": "取消选择" + }, + { + "key": "ui.cargo.install", + "value": "安装" + }, + { + "key": "ui.cargo.mark", + "value": "标记发射" + }, + { + "key": "ui.cargo.unmark", + "value": "取消标记" + }, + { + "key": "notice.installFailed", + "value": "无法安装。请检查机体预算和旧件所需的空间。" + }, + { + "key": "notice.placement", + "value": "无法放置。请检查边界、占用和堆叠数量。" + }, + { + "key": "ui.home.chooseItem", + "value": "选用 {0} · {1}" + }, + { + "key": "ui.home.manufacture", + "value": "制造 {0} · 合金 {1} / 电芯 {2}" + }, + { + "key": "settings.title", + "value": "设置" + }, + { + "key": "settings.help", + "value": "点击应用后保存设置,语言切换即时生效。" + }, + { + "key": "settings.language", + "value": "语言" + }, + { + "key": "settings.audio", + "value": "音频" + }, + { + "key": "settings.display", + "value": "画面" + }, + { + "key": "settings.controls", + "value": "操作与按键" + }, + { + "key": "settings.volume", + "value": "总音量" + }, + { + "key": "settings.volumeHelp", + "value": "调节游戏内所有声音的音量,设为 0% 即可静音。" + }, + { + "key": "settings.percent", + "value": "{0}%" + }, + { + "key": "settings.multiplier", + "value": "{0} 倍" + }, + { + "key": "settings.quality", + "value": "画质" + }, + { + "key": "settings.quality.veryLow", + "value": "极低" + }, + { + "key": "settings.quality.low", + "value": "低" + }, + { + "key": "settings.quality.medium", + "value": "中" + }, + { + "key": "settings.quality.high", + "value": "高" + }, + { + "key": "settings.quality.veryHigh", + "value": "极高" + }, + { + "key": "settings.quality.ultra", + "value": "最高" + }, + { + "key": "settings.resolution", + "value": "分辨率" + }, + { + "key": "settings.windowMode", + "value": "显示模式" + }, + { + "key": "settings.windowed", + "value": "窗口" + }, + { + "key": "settings.fullscreen", + "value": "全屏" + }, + { + "key": "settings.displayHelp", + "value": "更改分辨率或显示模式后,需要在 15 秒内确认,否则自动恢复上一次显示设置。" + }, + { + "key": "settings.display.confirm", + "value": "保留当前显示设置?" + }, + { + "key": "settings.display.countdown", + "value": "将在 {0} 秒后自动恢复上一次显示设置。" + }, + { + "key": "settings.display.keep", + "value": "保留并保存" + }, + { + "key": "settings.display.revert", + "value": "恢复原设置" + }, + { + "key": "settings.display.reverted", + "value": "已恢复上一次显示设置,其他修改仍待应用。" + }, + { + "key": "settings.mouse", + "value": "鼠标" + }, + { + "key": "settings.sensitivity", + "value": "鼠标灵敏度" + }, + { + "key": "settings.aimSensitivity", + "value": "精瞄灵敏度倍率" + }, + { + "key": "settings.bindings", + "value": "按键设置" + }, + { + "key": "settings.bind.help", + "value": "点击按键后,按下键盘按键或鼠标按钮进行改绑。Esc 取消改绑,并始终可用于返回菜单;重复按键会提示冲突。" + }, + { + "key": "settings.bind.waiting", + "value": "等待按键" + }, + { + "key": "settings.bind.prompt", + "value": "请为“{0}”按下新的键盘按键或鼠标按钮,按 Esc 取消。" + }, + { + "key": "settings.bind.cancel", + "value": "取消改绑" + }, + { + "key": "settings.bind.changed", + "value": "已更改“{0}”的按键,点击应用后保存。" + }, + { + "key": "settings.bind.cancelled", + "value": "已取消改绑,保留原按键。" + }, + { + "key": "settings.bind.conflict", + "value": "该按键已用于“{0}”,请选择其他按键。" + }, + { + "key": "settings.bind.reserved", + "value": "Esc 用于关闭菜单和取消输入,请选择其他按键。" + }, + { + "key": "settings.bind.unsupported", + "value": "请选择键盘按键或鼠标按钮。" + }, + { + "key": "settings.reset", + "value": "恢复默认" + }, + { + "key": "settings.resetHint", + "value": "已恢复默认设置与按键,点击应用后保存。" + }, + { + "key": "settings.back", + "value": "返回 / 放弃修改" + }, + { + "key": "settings.apply", + "value": "应用并保存" + }, + { + "key": "settings.saved", + "value": "设置已保存。" + }, + { + "key": "settings.saveFailed", + "value": "设置保存失败,请检查磁盘空间与目录写入权限后重试。" + }, + { + "key": "settings.action.move.up", + "value": "向前移动" + }, + { + "key": "settings.action.move.down", + "value": "向后移动" + }, + { + "key": "settings.action.move.left", + "value": "向左移动" + }, + { + "key": "settings.action.move.right", + "value": "向右移动" + }, + { + "key": "settings.action.fire", + "value": "主武器" + }, + { + "key": "settings.action.aim", + "value": "精瞄" + }, + { + "key": "settings.action.boost", + "value": "推进" + }, + { + "key": "settings.action.jump", + "value": "跳跃 / 升空" + }, + { + "key": "settings.action.view", + "value": "切换视角" + }, + { + "key": "settings.action.interact", + "value": "交互" + }, + { + "key": "settings.action.inventory", + "value": "打开货仓" + }, + { + "key": "settings.action.cargo", + "value": "发射回收舱" + }, + { + "key": "settings.action.pause", + "value": "暂停" + }, + { + "key": "settings.action.secondary", + "value": "副武装" + }, + { + "key": "settings.action.guard", + "value": "主动防护" + }, + { + "key": "settings.key.mouseLeft", + "value": "鼠标左键" + }, + { + "key": "settings.key.mouseRight", + "value": "鼠标右键" + }, + { + "key": "settings.key.mouseMiddle", + "value": "鼠标中键" + }, + { + "key": "settings.key.mouseForward", + "value": "鼠标前侧键" + }, + { + "key": "settings.key.mouseBack", + "value": "鼠标后侧键" + }, + { + "key": "settings.key.space", + "value": "空格" + }, + { + "key": "settings.key.leftShift", + "value": "左 Shift" + }, + { + "key": "settings.key.rightShift", + "value": "右 Shift" + }, + { + "key": "settings.action.reload", + "value": "换弹" + }, + { + "key": "ui.combat.nav", + "value": "区域地图" + }, + { + "key": "ui.combat.legend", + "value": "◇ 目标 ○ 撤离点" + }, + { + "key": "ui.combat.ammo", + "value": "弹匣 / 无限备弹" + }, + { + "key": "ui.combat.reload", + "value": "[{0}] 换弹" + }, + { + "key": "ui.combat.loading", + "value": "换弹中 {0:0.0} 秒" + }, + { + "key": "ui.combat.ready", + "value": "就绪" + }, + { + "key": "ui.combat.charging", + "value": "冷却 {0:0.0} 秒" + }, + { + "key": "ui.combat.lowEnergy", + "value": "能源不足" + }, + { + "key": "ui.combat.cargo", + "value": "普通货仓 {0}/16 格 · 安全箱 {1}/4 格" + }, + { + "key": "ui.combat.empty", + "value": "未安装" + }, + { + "key": "range.resetSign", + "value": "训练复位台" + }, + { + "key": "base.home.title", + "value": "前沿基地 / 机甲待命" + }, + { + "key": "base.navigation", + "value": "基地终端" + }, + { + "key": "base.enterHangar", + "value": "进入机库" + }, + { + "key": "base.deploy", + "value": "作战部署" + }, + { + "key": "base.hangar", + "value": "机库" + }, + { + "key": "base.backHome", + "value": "返回主页" + }, + { + "key": "base.home.description", + "value": "整备机体、整理战利品,然后决定下一次出击。" + }, + { + "key": "base.baseStatus", + "value": "基地资源" + }, + { + "key": "base.recipes", + "value": "已研究配方" + }, + { + "key": "base.record", + "value": "出击 {0} / 撤离 {1} / 目标达成 {2}" + }, + { + "key": "base.activeRun", + "value": "存在未结束出击。基地库存当前只读,请继续出击后返航整理。" + }, + { + "key": "base.menuHint", + "value": "鼠标选择 · Tab 切换焦点 · Enter 确认 · Esc 返回" + }, + { + "key": "base.tab.loadout", + "value": "机体整备" + }, + { + "key": "base.tab.warehouse", + "value": "基地仓库" + }, + { + "key": "base.tab.research", + "value": "研究制造" + }, + { + "key": "base.preview", + "value": "机体预览" + }, + { + "key": "base.previewHint", + "value": "在预览空白区域拖动可旋转机体。" + }, + { + "key": "base.chassisStats", + "value": "装甲 {0} / 巡航 {1} m/s / 推进 {2} m/s" + }, + { + "key": "base.chassis", + "value": "底盘" + }, + { + "key": "base.installed", + "value": "装备槽" + }, + { + "key": "base.borrowed", + "value": "借用,返航归还" + }, + { + "key": "base.partStats", + "value": "伤害 {0} / 载重 {1} / 功率 {2}" + }, + { + "key": "base.compare", + "value": "相对当前装备:伤害 {0:+0.##;-0.##;0} 载重 {1:+0.##;-0.##;0} 功率 {2:+0.##;-0.##;0}" + }, + { + "key": "base.useBorrowed", + "value": "改用基础借用件" + }, + { + "key": "base.preparation", + "value": "随行预装" + }, + { + "key": "base.cargo", + "value": "普通货仓" + }, + { + "key": "base.safeCase", + "value": "安全箱" + }, + { + "key": "base.reserved", + "value": "已整备" + }, + { + "key": "base.researchHelp", + "value": "蓝图作为实物保留。研究永久解锁配方,制造消耗仓库内未预装的材料并生成实物。" + }, + { + "key": "base.researchCost", + "value": "研究:2 合金 + 1 电芯" + }, + { + "key": "base.manufacture", + "value": "制造一件" + }, + { + "key": "base.deployHelp", + "value": "确认出击后,整备装备和预装货物将从仓库携出。机毁保全安全箱,箱外装备与物资损失。借用件返基地归还。" + }, + { + "key": "base.confirm", + "value": "确认" + }, + { + "key": "warehouse.title", + "value": "基地仓库" + }, + { + "key": "warehouse.pending", + "value": "待整理 {0}" + }, + { + "key": "warehouse.occupancy", + "value": "占用 {0} / {1} 格" + }, + { + "key": "warehouse.stock", + "value": "合金 {0} / 电芯 {1}" + }, + { + "key": "warehouse.controls", + "value": "拖放移动 · R 旋转预览 · Ctrl+单击预装/取消 · Shift+拖动拆分" + }, + { + "key": "warehouse.filter.all", + "value": "全部" + }, + { + "key": "warehouse.filter.equipment", + "value": "装备" + }, + { + "key": "warehouse.filter.material", + "value": "材料" + }, + { + "key": "warehouse.filter.blueprint", + "value": "蓝图" + }, + { + "key": "warehouse.search", + "value": "查找" + }, + { + "key": "warehouse.sort", + "value": "整理" + }, + { + "key": "warehouse.claim", + "value": "入仓" + }, + { + "key": "warehouse.claimAll", + "value": "一键尝试入仓" + }, + { + "key": "warehouse.inboxHelp", + "value": "回收物资已保存。腾出仓库空间后领取,清空待整理区才能开启新的正式出击。" + }, + { + "key": "warehouse.emptyInbox", + "value": "暂无待整理物资" + }, + { + "key": "warehouse.selectHelp", + "value": "选择物品查看详情;选择后点击空格可摆放。预装只预占,确认出击才携出。" + }, + { + "key": "warehouse.itemInfo", + "value": "尺寸 {0}×{1} / 数量 {2} / 编号 {3}" + }, + { + "key": "warehouse.equip", + "value": "装备" + }, + { + "key": "warehouse.rotate", + "value": "旋转预览" + }, + { + "key": "warehouse.unstage", + "value": "取消预装" + }, + { + "key": "warehouse.recycle", + "value": "蓝图换取 2 合金" + }, + { + "key": "warehouse.discard", + "value": "弃置" + }, + { + "key": "warehouse.discardConfirm", + "value": "确定永久弃置「{0}」×{1}?此操作会释放占用空间。" + }, + { + "key": "warehouse.sortFirst", + "value": "请先整理或弃置待整理物资,再开启新的正式出击。" + }, + { + "key": "warehouse.incompatible", + "value": "物品与装备槽不兼容。" + }, + { + "key": "warehouse.noSpace", + "value": "没有合适的连续空位;物品和材料已保留。" + }, + { + "key": "warehouse.missing", + "value": "所选实物已不存在,请重新选择装备。" + }, + { + "key": "warehouse.alreadyEquipped", + "value": "此物品已预占装备槽。请先改用其他装备或借用件。" + }, + { + "key": "warehouse.manufactureFailed", + "value": "无法制造:请检查已研究配方、未预装材料及产物所需空位。" + }, + { + "key": "warehouse.saved", + "value": "操作已保存。" + }, + { + "key": "warehouse.split", + "value": "拆分材料" + }, + { + "key": "warehouse.splitHelp", + "value": "选择本次移动的数量;其余材料保留在原堆叠。" + }, + { + "key": "warehouse.quantity", + "value": "数量" + }, + { + "key": "warehouse.recoveredPending", + "value": "货舱物资已回收,其中 {0} 件待返航后整理入仓。" + }, + { + "key": "warehouse.available", + "value": "扣除预装后可用:合金 {0} / 电芯 {1}" + }, + { + "key": "base.boostStats", + "value": "机动倍率 {0:0.##} / 载重 {1} / 功率 {2}" + }, + { + "key": "base.moduleStats", + "value": "载重 {0} / 功率 {1}" + }, + { + "key": "base.boostCompare", + "value": "相对当前:机动倍率 {0:+0.##;-0.##;0} / 载重 {1:+0.##;-0.##;0} / 功率 {2:+0.##;-0.##;0}" + }, + { + "key": "base.moduleCompare", + "value": "相对当前:载重 {0:+0.##;-0.##;0} / 功率 {1:+0.##;-0.##;0}" + }, + { + "key": "base.cancel", + "value": "取消" + }, + { + "key": "hangar.equipment", + "value": "机体装备" + }, + { + "key": "hangar.carry", + "value": "携带物品" + }, + { + "key": "hangar.draft", + "value": "整备草案 · 确认出击时转移实物" + }, + { + "key": "hangar.readOnly", + "value": "出击进行中 · 当前实物装备 / 只读" + }, + { + "key": "hangar.owned", + "value": "自有装备" + }, + { + "key": "hangar.emptySlot", + "value": "空槽" + }, + { + "key": "hangar.reserved", + "value": "预占" + }, + { + "key": "hangar.capacity", + "value": "{0} / {1} 格" + }, + { + "key": "hangar.protected", + "value": "机毁保全 · 机体损毁后仍可带回" + }, + { + "key": "hangar.details", + "value": "选中物品 / 操作" + }, + { + "key": "ue.subtitle", + "value": "机甲搜打撤 · 轨道边境" + }, + { + "key": "ue.baseTitle", + "value": "机库指挥台" + }, + { + "key": "ue.enterHangar", + "value": "进入机库" + }, + { + "key": "ue.deploy", + "value": "整备并出击" + }, + { + "key": "ue.resume", + "value": "继续出击" + }, + { + "key": "ue.training", + "value": "试验场" + }, + { + "key": "ue.research", + "value": "研究与制造" + }, + { + "key": "ue.settings", + "value": "设置" + }, + { + "key": "ue.performance", + "value": "性能统计" + }, + { + "key": "ue.perfOn", + "value": "开启" + }, + { + "key": "ue.perfOff", + "value": "关闭" + }, + { + "key": "ue.perfPosition", + "value": "显示位置" + }, + { + "key": "ue.perfTopRight", + "value": "右上角" + }, + { + "key": "ue.perfTopLeft", + "value": "左上角" + }, + { + "key": "ue.perfBottomRight", + "value": "右下角" + }, + { + "key": "ue.perfBottomLeft", + "value": "左下角" + }, + { + "key": "ue.perfHint", + "value": "60 FPS 参考预算 16.67 ms · — 表示暂无数据" + }, + { + "key": "ue.perfFps", + "value": "FPS" + }, + { + "key": "ue.perfFrame", + "value": "帧耗时" + }, + { + "key": "ue.perfGpu", + "value": "GPU" + }, + { + "key": "ue.perfGame", + "value": "CPU 游戏" + }, + { + "key": "ue.perfRender", + "value": "CPU 渲染" + }, + { + "key": "ue.perfRhi", + "value": "RHI" + }, + { + "key": "ue.quit", + "value": "退出游戏" + }, + { + "key": "ue.baseHint", + "value": "整备装备,搜索物资,活着撤离。" + }, + { + "key": "ue.pilot", + "value": "驾驶员档案" + }, + { + "key": "ue.warehouse", + "value": "基地仓库" + }, + { + "key": "ue.pending", + "value": "待整理物资" + }, + { + "key": "ue.sorties", + "value": "出击" + }, + { + "key": "ue.returns", + "value": "撤离" + }, + { + "key": "ue.activeRun", + "value": "任务进行中 · 可继续" + }, + { + "key": "ue.standby", + "value": "基地待命" + }, + { + "key": "ue.carryHint", + "value": "拖动放置 · R 旋转 · 安全箱机毁保全" + }, + { + "key": "ue.fieldSystems", + "value": "战地改装与回收" + }, + { + "key": "ue.fieldHint", + "value": "安装实物装备,选择货舱物品后回收" + }, + { + "key": "ue.fieldEquipment", + "value": "当前实物装备 · 可战地改装" + }, + { + "key": "ue.warehouseHint", + "value": "滚轮浏览 · 有限容量 · 物品唯一归属" + }, + { + "key": "ue.primary", + "value": "主武器" + }, + { + "key": "ue.secondary", + "value": "副武器" + }, + { + "key": "ue.booster", + "value": "推进器" + }, + { + "key": "ue.guard", + "value": "防御模块" + }, + { + "key": "ue.mass", + "value": "载重" + }, + { + "key": "ue.power", + "value": "功率" + }, + { + "key": "ue.cargo", + "value": "货仓" + }, + { + "key": "ue.safeCase", + "value": "安全箱 · 2 × 2" + }, + { + "key": "ue.cellsUsed", + "value": "格已用" + }, + { + "key": "ue.sort", + "value": "整理仓库" + }, + { + "key": "ue.podSelect", + "value": "选择主动回收物品" + }, + { + "key": "ue.podRule", + "value": "每次出击一舱 · 2 × 2 · 最多四件" + }, + { + "key": "ue.holdPod", + "value": "关闭物品栏,落地静止长按" + }, + { + "key": "ue.selected", + "value": "已选择" + }, + { + "key": "ue.uninstall", + "value": "卸入货仓" + }, + { + "key": "ue.borrow", + "value": "借用基础件" + }, + { + "key": "ue.borrowed", + "value": "借用件" + }, + { + "key": "ue.equip", + "value": "装备此物品" + }, + { + "key": "ue.toCargo", + "value": "预装到货仓" + }, + { + "key": "ue.toSafe", + "value": "预装到安全箱" + }, + { + "key": "ue.unprepare", + "value": "取消预装" + }, + { + "key": "ue.install", + "value": "安装装备" + }, + { + "key": "ue.transfer", + "value": "转移至另一容器" + }, + { + "key": "ue.recycle", + "value": "回收已研究蓝图" + }, + { + "key": "ue.dragHint", + "value": "拖拽摆放 · R 旋转 · 数量可拆分" + }, + { + "key": "ue.selectItem", + "value": "选择物品查看操作" + }, + { + "key": "ue.back", + "value": "返回" + }, + { + "key": "ue.researchHint", + "value": "研究消耗合金 2、电芯 1;制造按配方消耗实物材料。" + }, + { + "key": "ue.manufacture", + "value": "制造装备" + }, + { + "key": "ue.inboxHint", + "value": "仓库满时物资保留在此;整理完才能再次出击。" + }, + { + "key": "ue.claim", + "value": "收入仓库" + }, + { + "key": "ue.claimAll", + "value": "全部收入仓库" + }, + { + "key": "ue.discard", + "value": "丢弃" + }, + { + "key": "ue.confirmDiscard", + "value": "再次点击丢弃以确认。" + }, + { + "key": "ue.settingsHint", + "value": "显示、鼠标、声音与键位立即生效并保存。" + }, + { + "key": "ue.display", + "value": "显示" + }, + { + "key": "ue.fullscreen", + "value": "切换全屏 / 窗口" + }, + { + "key": "ue.sensitivity", + "value": "鼠标灵敏度" + }, + { + "key": "ue.aimSensitivity", + "value": "瞄准灵敏度倍率" + }, + { + "key": "ue.fire", + "value": "主武器开火" + }, + { + "key": "ue.aim", + "value": "瞄准" + }, + { + "key": "ue.invertY", + "value": "反转鼠标纵轴" + }, + { + "key": "ue.controls", + "value": "键鼠操作" + }, + { + "key": "ue.forward", + "value": "向前" + }, + { + "key": "ue.backward", + "value": "向后" + }, + { + "key": "ue.left", + "value": "向左" + }, + { + "key": "ue.right", + "value": "向右" + }, + { + "key": "ue.jump", + "value": "跳跃 / 轻型悬浮" + }, + { + "key": "ue.boost", + "value": "推进冲刺" + }, + { + "key": "ue.reload", + "value": "装填" + }, + { + "key": "ue.view", + "value": "切换第一 / 越肩视角" + }, + { + "key": "ue.interact", + "value": "交互 / 登机" + }, + { + "key": "ue.inventory", + "value": "携带物品" + }, + { + "key": "ue.pod", + "value": "主动回收(长按)" + }, + { + "key": "ue.pressKey", + "value": "请按新按键 · Esc 取消" + }, + { + "key": "ue.keyConflict", + "value": "该按键已被其他操作使用。" + }, + { + "key": "ue.volume", + "value": "音量" + }, + { + "key": "ue.quality", + "value": "切换画质等级" + }, + { + "key": "ue.paused", + "value": "任务暂停" + }, + { + "key": "ue.upgrade", + "value": "局内改装" + }, + { + "key": "ue.pauseHint", + "value": "查看携带物资,或保存进度后返回基地。" + }, + { + "key": "ue.saveAndHome", + "value": "保存并返回主页" + }, + { + "key": "ue.upgradeDamage", + "value": "火力强化 · 提升伤害" + }, + { + "key": "ue.upgradeRepair", + "value": "战地修复 · 恢复装甲" + }, + { + "key": "ue.upgradeEnergy", + "value": "能源维护 · 恢复能源" + }, + { + "key": "ue.armor", + "value": "装甲" + }, + { + "key": "ue.energy", + "value": "能源" + }, + { + "key": "ue.stage", + "value": "区域" + }, + { + "key": "ue.salvage", + "value": "携带" + }, + { + "key": "ue.routeDone", + "value": "路线完成,仍需主动撤离" + }, + { + "key": "ue.objectiveDone", + "value": "目标完成,可前往下一片区" + }, + { + "key": "ue.findObjective", + "value": "搜索物资并寻找目标终端" + }, + { + "key": "ue.podUsed", + "value": "主动回收舱:已使用" + }, + { + "key": "ue.podAvailable", + "value": "主动回收舱:1 / 1" + }, + { + "key": "ue.reloading", + "value": "装填中" + }, + { + "key": "ue.wait", + "value": "接应抵达倒计时" + }, + { + "key": "ue.podHolding", + "value": "货舱回收准备中,保持静止" + }, + { + "key": "ue.footer", + "value": "键鼠操作 · Esc 返回 · 搜索,战斗,撤离" + }, + { + "key": "ue.completed", + "value": "完成路线" + }, + { + "key": "ue.rangeStats", + "value": "试验场 · 命中 / 关卡 / 用时" + }, + { + "key": "ue.rangeReset", + "value": "试验场计时和命中已重置。" + }, + { + "key": "ue.courseGate", + "value": "机动计时门" + }, + { + "key": "ue.extraction", + "value": "撤离平台" + }, + { + "key": "ue.routeBoss", + "value": "首领路线" + }, + { + "key": "ue.routeEscort", + "value": "护送路线" + }, + { + "key": "ue.rangeResetLabel", + "value": "重置试验场" + }, + { + "key": "ue.saved", + "value": "操作已保存" + }, + { + "key": "ue.invalid", + "value": "该操作当前不可用。" + }, + { + "key": "ue.missing", + "value": "找不到对应物品。" + }, + { + "key": "ue.noSpace", + "value": "空间不足或物品无法放入。" + }, + { + "key": "ue.locked", + "value": "当前状态禁止该操作。" + }, + { + "key": "ue.budget", + "value": "装备超出底盘载重或功率预算。" + }, + { + "key": "ue.funds", + "value": "可用材料不足。" + }, + { + "key": "ue.duplicate", + "value": "该物资或事件已经处理。" + }, + { + "key": "ue.saveFailed", + "value": "保存失败,操作未结算。请检查磁盘。" + }, + { + "key": "ue.corrupt", + "value": "存档及备份无法读取,已保留原文件。" + }, + { + "key": "ue.unsupported", + "value": "存档版本不受支持。" + }, + { + "key": "ue.destroyed", + "value": "机体损毁。安全箱已保全,其余物资损失。" + }, + { + "key": "ue.extracted", + "value": "撤离成功,物资已回收入库。" + }, + { + "key": "ue.nothingNear", + "value": "靠近物资箱、终端或撤离点后交互。" + }, + { + "key": "ue.loot", + "value": "已搜索物资" + }, + { + "key": "ue.called", + "value": "已呼叫接应,等待后需主动登机。" + }, + { + "key": "ue.finalObjective", + "value": "完成首领战或护送后仍需主动撤离。" + }, + { + "key": "ue.escort", + "value": "护送运输车" + }, + { + "key": "ue.explore", + "value": "搜索箱体 · 击败敌人 · 终端推进 · 绿色平台撤离" + }, + { + "key": "ue.search", + "value": "搜索" + }, + { + "key": "ue.board", + "value": "登机并撤离" + }, + { + "key": "ue.call", + "value": "呼叫接应" + }, + { + "key": "ue.nextStage", + "value": "前往下一片区(右侧为护送路线)" + }, + { + "key": "ue.activate", + "value": "启动目标终端" + }, + { + "key": "ue.readyToBoard", + "value": "接应已抵达,回到平台按交互键登机。" + }, + { + "key": "ue.podSent", + "value": "选定货舱物资已发射回收,本次回收舱已用尽。" + }, + { + "key": "ue.podInterrupted", + "value": "回收被移动、起跳或命中打断。" + }, + { + "key": "ue.escortLost", + "value": "护送运输车已损毁。" + } + ] +} diff --git a/Content/IRONSPORE/Raw/NotoSansSC-OFL.txt b/Content/IRONSPORE/Raw/NotoSansSC-OFL.txt new file mode 100644 index 0000000..83c2cf3 --- /dev/null +++ b/Content/IRONSPORE/Raw/NotoSansSC-OFL.txt @@ -0,0 +1,58 @@ +© 2014-2021 Adobe (http://www.adobe.com/), with Reserved Font Name 'Source'. + +SIL Open Font License 1.1 +========================= + +PREAMBLE + +The goals of the Open Font License (OFL) are to stimulate worldwide development of collaborative font projects, to support the font creation efforts of academic and linguistic communities, and to provide a free and open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and redistributed freely as long as they are not sold by themselves. The fonts, including any derivative works, can be bundled, embedded, redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, however, cannot be released under any other type of license. The requirement for fonts to remain under this license does not apply to any document created using the fonts or their derivatives. + +DEFINITIONS +“Font Software” refers to the set of files released by the Copyright Holder(s) under this license and clearly marked as such. This may include source files, build scripts and documentation. + +“Reserved Font Name” refers to any names specified as such after the copyright statement(s). + +“Original Version” refers to the collection of Font Software components as distributed by the Copyright Holder(s). + +“Modified Version” refers to any derivative made by adding to, deleting, or substituting – in part or in whole – any of the components of the Original Version, by changing formats or by porting the Font Software to a new environment. + +“Author” refers to any designer, engineer, programmer, technical writer or other person who contributed to the Font Software. + + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining a copy of the Font Software, to use, study, copy, merge, embed, modify, redistribute, and sell modified and unmodified copies of the Font Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, redistributed and/or sold with any software, provided that each copy contains the above copyright notice and this license. These can be included either as stand-alone text files, human-readable headers or in the appropriate machine-readable metadata fields within text or binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font Name(s) unless explicit written permission is granted by the corresponding Copyright Holder. This restriction only applies to the primary font name as presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font Software shall not be used to promote, endorse or advertise any Modified Version, except to acknowledge the contribution(s) of the Copyright Holder(s) and the Author(s) or with their explicit written permission. + +5) The Font Software, modified or unmodified, in part or in whole, must be distributed entirely under this license, and must not be distributed under any other license. The requirement for fonts to remain under this license does not apply to any document created using the Font Software. + +TERMINATION + +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER + +THE FONT SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. + + +© 2003-2009 SIL International, all rights reserved, unless otherwise noted elsewhere on this page. +Provided by SIL’s Non-Roman Script Initiative. Contact us at nrsi@sil.org. \ No newline at end of file diff --git a/Content/IRONSPORE/Raw/nlohmann-json-3.12.0-MIT.txt b/Content/IRONSPORE/Raw/nlohmann-json-3.12.0-MIT.txt new file mode 100644 index 0000000..a1dacc8 --- /dev/null +++ b/Content/IRONSPORE/Raw/nlohmann-json-3.12.0-MIT.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2013-2025 Niels Lohmann + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Content/IRONSPORE/UI/Icons/T_MechFrontSilhouette.uasset b/Content/IRONSPORE/UI/Icons/T_MechFrontSilhouette.uasset new file mode 100644 index 0000000..dbfd247 Binary files /dev/null and b/Content/IRONSPORE/UI/Icons/T_MechFrontSilhouette.uasset differ diff --git a/Content/IRONSPORE/UI/Icons/T_coil.uasset b/Content/IRONSPORE/UI/Icons/T_coil.uasset new file mode 100644 index 0000000..5f509c7 Binary files /dev/null and b/Content/IRONSPORE/UI/Icons/T_coil.uasset differ diff --git a/Content/IRONSPORE/UI/Icons/T_composite_guard.uasset b/Content/IRONSPORE/UI/Icons/T_composite_guard.uasset new file mode 100644 index 0000000..adcbaa1 Binary files /dev/null and b/Content/IRONSPORE/UI/Icons/T_composite_guard.uasset differ diff --git a/Content/IRONSPORE/UI/Icons/T_efficient.uasset b/Content/IRONSPORE/UI/Icons/T_efficient.uasset new file mode 100644 index 0000000..71c4215 Binary files /dev/null and b/Content/IRONSPORE/UI/Icons/T_efficient.uasset differ diff --git a/Content/IRONSPORE/UI/Icons/T_guided.uasset b/Content/IRONSPORE/UI/Icons/T_guided.uasset new file mode 100644 index 0000000..0bf670b Binary files /dev/null and b/Content/IRONSPORE/UI/Icons/T_guided.uasset differ diff --git a/Content/IRONSPORE/UI/Icons/T_pulse.uasset b/Content/IRONSPORE/UI/Icons/T_pulse.uasset new file mode 100644 index 0000000..57fec87 Binary files /dev/null and b/Content/IRONSPORE/UI/Icons/T_pulse.uasset differ diff --git a/Content/IRONSPORE/UI/Icons/T_rapid.uasset b/Content/IRONSPORE/UI/Icons/T_rapid.uasset new file mode 100644 index 0000000..6d5fe82 Binary files /dev/null and b/Content/IRONSPORE/UI/Icons/T_rapid.uasset differ diff --git a/Content/IRONSPORE/UI/Icons/T_reactive_guard.uasset b/Content/IRONSPORE/UI/Icons/T_reactive_guard.uasset new file mode 100644 index 0000000..3a943eb Binary files /dev/null and b/Content/IRONSPORE/UI/Icons/T_reactive_guard.uasset differ diff --git a/Content/IRONSPORE/UI/Icons/T_siege.uasset b/Content/IRONSPORE/UI/Icons/T_siege.uasset new file mode 100644 index 0000000..b092cb4 Binary files /dev/null and b/Content/IRONSPORE/UI/Icons/T_siege.uasset differ diff --git a/Content/IRONSPORE/UI/Icons/T_vector.uasset b/Content/IRONSPORE/UI/Icons/T_vector.uasset new file mode 100644 index 0000000..e6c9e28 Binary files /dev/null and b/Content/IRONSPORE/UI/Icons/T_vector.uasset differ diff --git a/Content/IRONSPORE/UI/WBP_IronsRoot.uasset b/Content/IRONSPORE/UI/WBP_IronsRoot.uasset new file mode 100644 index 0000000..1da4099 Binary files /dev/null and b/Content/IRONSPORE/UI/WBP_IronsRoot.uasset differ diff --git a/Source/GameFrameworkExternal/GameFrameworkExternal.Build.cs b/Source/GameFrameworkExternal/GameFrameworkExternal.Build.cs new file mode 100644 index 0000000..5961389 --- /dev/null +++ b/Source/GameFrameworkExternal/GameFrameworkExternal.Build.cs @@ -0,0 +1,17 @@ +using System.IO; +using UnrealBuildTool; +public class GameFrameworkExternal : ModuleRules +{ + public GameFrameworkExternal(ReadOnlyTargetRules Target) : base(Target) + { + Type = ModuleType.External; + string Root = Path.GetFullPath(Path.Combine(ModuleDirectory, "../..")); + string Library = Path.Combine(Root, "Builds/gameFramework/vs2022-x64/Release/gameFramework.lib"); + if (Target.Platform != UnrealTargetPlatform.Win64 || Target.bDebugBuildsActuallyUseDebugCRT) + throw new BuildException("gameFramework host requires Win64 /MD Release ABI."); + if (!File.Exists(Library)) throw new BuildException("Build the independent core first: Tools/Framework/Build.ps1"); + PublicIncludePaths.Add(Path.Combine(Root, "gameFramework/include")); + PublicAdditionalLibraries.Add(Library); + ExternalDependencies.Add(Library); + } +} diff --git a/Source/IronsEditor/IronsEditor.Build.cs b/Source/IronsEditor/IronsEditor.Build.cs new file mode 100644 index 0000000..f1543e6 --- /dev/null +++ b/Source/IronsEditor/IronsEditor.Build.cs @@ -0,0 +1,9 @@ +using UnrealBuildTool; +public class IronsEditor : ModuleRules +{ + public IronsEditor(ReadOnlyTargetRules Target) : base(Target) + { + PCHUsage=PCHUsageMode.UseExplicitOrSharedPCHs; + PrivateDependencyModuleNames.AddRange(new[]{"Core","CoreUObject","Engine","UnrealEd","AssetTools","AssetRegistry","Kismet","KismetCompiler","UMG","UMGEditor","ToolsetRegistry","UEAdapter","GameFrameworkExternal","ueRouge","Json","MaterialEditor"}); + } +} diff --git a/Source/IronsEditor/Private/IronsBridgeTests.cpp b/Source/IronsEditor/Private/IronsBridgeTests.cpp new file mode 100644 index 0000000..4bf0a89 --- /dev/null +++ b/Source/IronsEditor/Private/IronsBridgeTests.cpp @@ -0,0 +1,7 @@ +#include "Misc/AutomationTest.h" +#include "Misc/Paths.h" +#include "IronsHost.h" +#if WITH_DEV_AUTOMATION_TESTS +IMPLEMENT_SIMPLE_AUTOMATION_TEST(FIronsHostSpec,"IRONSPORE.Host.AtomicV1",EAutomationTestFlags::EditorContext|EAutomationTestFlags::EngineFilter) +bool FIronsHostSpec::RunTest(const FString& Parameters){FString Report;bool Ok=RunIronsHostAcceptance(FPaths::ConvertRelativePathToFull(FPaths::ProjectSavedDir()/TEXT("QA/HostAcceptance")),Report);AddInfo(Report);TestTrue(TEXT("Atomic V1 host behavior"),Ok);return Ok;} +#endif diff --git a/Source/IronsEditor/Private/IronsMigrationToolset.cpp b/Source/IronsEditor/Private/IronsMigrationToolset.cpp new file mode 100644 index 0000000..f74eb92 --- /dev/null +++ b/Source/IronsEditor/Private/IronsMigrationToolset.cpp @@ -0,0 +1,99 @@ +#include "IronsMigrationToolset.h" +#include "IronsGame.h" +#include "Editor.h" +#include "FileHelpers.h" +#include "Engine/World.h" +#include "Engine/Engine.h" +#include "Engine/StaticMesh.h" +#include "GameFramework/WorldSettings.h" +#include "AssetToolsModule.h" +#include "AssetImportTask.h" +#include "Factories/MaterialFactoryNew.h" +#include "WidgetBlueprintFactory.h" +#include "WidgetBlueprint.h" +#include "Kismet2/KismetEditorUtilities.h" +#include "AssetRegistry/AssetRegistryModule.h" +#include "Materials/Material.h" +#include "Materials/MaterialExpressionVectorParameter.h" +#include "Materials/MaterialExpressionConstant.h" +#include "UObject/SavePackage.h" +#include "Misc/Paths.h" +#include "Misc/FileHelper.h" +#include "HAL/FileManager.h" +#include "Kismet/GameplayStatics.h" +#include "Serialization/JsonSerializer.h" +#include "Misc/PackageName.h" +#include "Modules/ModuleManager.h" +#include "ToolsetRegistry/UToolsetRegistry.h" + +namespace { +bool Bound(){return FPaths::ConvertRelativePathToFull(FPaths::GetProjectFilePath()).Equals(TEXT("D:/vibeCoding/ueRouge/ueRouge.uproject"),ESearchCase::IgnoreCase);} +bool Save(UObject* Object){auto Package=Object->GetOutermost();FString File=FPackageName::LongPackageNameToFilename(Package->GetName(),FPackageName::GetAssetPackageExtension());FSavePackageArgs Args;Args.TopLevelFlags=RF_Public|RF_Standalone;return UPackage::SavePackage(Package,Object,*File,Args);} +AIronsController* Controller(){return GEditor&&GEditor->PlayWorld?Cast(UGameplayStatics::GetPlayerController(GEditor->PlayWorld,0)):nullptr;} +} +FString UIronsMigrationToolset::Bootstrap(){ + if(!Bound()||!GEditor||GEditor->PlayWorld)return TEXT("ERROR: exact project required, PIE must be stopped"); + FAssetToolsModule& Module=FModuleManager::LoadModuleChecked(TEXT("AssetTools"));auto& Tools=Module.Get(); + for(const TCHAR* Path:{TEXT("/Game/IRONSPORE/Art/Mechs/SM_MechScout.SM_MechScout"),TEXT("/Game/IRONSPORE/Art/Environment/SM_HangarBay.SM_HangarBay")})if(auto Mesh=LoadObject(nullptr,Path)){if(Mesh->GetNumSourceModels()>0){auto& Settings=Mesh->GetSourceModel(0).BuildSettings;Settings.bRecomputeNormals=true;Settings.bRecomputeTangents=true;Settings.bRemoveDegenerates=true;Settings.bUseMikkTSpace=false;Mesh->PostEditChange();Save(Mesh);}} + FString MaterialPath=TEXT("/Game/IRONSPORE/Art/Materials/M_Industrial"); + if(!LoadObject(nullptr,*(MaterialPath+TEXT(".M_Industrial")))){ + auto Factory=NewObject();auto M=Cast(Tools.CreateAsset(TEXT("M_Industrial"),TEXT("/Game/IRONSPORE/Art/Materials"),UMaterial::StaticClass(),Factory));if(!M)return TEXT("ERROR: material"); + auto Color=NewObject(M);Color->ParameterName=TEXT("BaseColor");Color->DefaultValue=FLinearColor(.13f,.19f,.23f);M->GetExpressionCollection().AddExpression(Color);M->GetEditorOnlyData()->BaseColor.Expression=Color; + auto Rough=NewObject(M);Rough->R=.75f;M->GetExpressionCollection().AddExpression(Rough);M->GetEditorOnlyData()->Roughness.Expression=Rough;M->PostEditChange();if(!Save(M))return TEXT("ERROR: save material"); + } + FString WidgetPath=TEXT("/Game/IRONSPORE/UI/WBP_IronsRoot.WBP_IronsRoot");if(!LoadObject(nullptr,*WidgetPath)){auto F=NewObject();F->ParentClass=UIronsScreen::StaticClass();auto W=Tools.CreateAsset(TEXT("WBP_IronsRoot"),TEXT("/Game/IRONSPORE/UI"),UWidgetBlueprint::StaticClass(),F);if(!W||!Save(W))return TEXT("ERROR: widget");} + const TPair Classes[]={ {TEXT("BP_IronsCharacter"),AIronsCharacter::StaticClass()},{TEXT("BP_IronsGameMode"),AIronsGameMode::StaticClass()} }; + for(const auto& C:Classes){FString Path=TEXT("/Game/IRONSPORE/Blueprints/")+C.Key;if(!LoadObject(nullptr,*(Path+TEXT(".")+C.Key))){auto Package=CreatePackage(*Path);auto BP=FKismetEditorUtilities::CreateBlueprint(C.Value,Package,*C.Key,BPTYPE_Normal,UBlueprint::StaticClass(),UBlueprintGeneratedClass::StaticClass());if(!BP)return TEXT("ERROR: blueprint");FAssetRegistryModule::AssetCreated(BP);FKismetEditorUtilities::CompileBlueprint(BP);if(!Save(BP))return TEXT("ERROR: save blueprint");}} + const TCHAR* Names[]={TEXT("L_Hangar"),TEXT("L_TrainingRange"),TEXT("L_Warzone")};for(auto Name:Names){FString Path=FString(TEXT("/Game/IRONSPORE/Maps/"))+Name;if(FPackageName::DoesPackageExist(Path))continue;UWorld* W=UEditorLoadingAndSavingUtils::NewBlankMap(false);if(!W)return TEXT("ERROR: new map");W->GetWorldSettings()->DefaultGameMode=AIronsGameMode::StaticClass();if(!UEditorLoadingAndSavingUtils::SaveMap(W,Path))return TEXT("ERROR: save map");} + UEditorLoadingAndSavingUtils::LoadMap(TEXT("/Game/IRONSPORE/Maps/L_Hangar"));UEditorLoadingAndSavingUtils::SaveDirtyPackages(true,true);return TEXT("OK: three maps, UMG and Blueprint children, industrial material saved"); +} +FString UIronsMigrationToolset::ImportVisuals(){ + if(!Bound()||!GEditor||GEditor->PlayWorld)return TEXT("ERROR: exact project required, PIE must be stopped"); + auto& Tools=FModuleManager::LoadModuleChecked(TEXT("AssetTools")).Get();TArray Tasks; + for(const auto& Spec:TArray>{{TEXT("SourceAssets/Derived/UI"),TEXT("/Game/IRONSPORE/UI/Icons")},{TEXT("SourceAssets/Audio/Generated"),TEXT("/Game/IRONSPORE/Audio")}}){TArray Files;IFileManager::Get().FindFilesRecursive(Files,*(FPaths::ProjectDir()/Spec.Key),TEXT("*.*"),true,false);for(const auto& File:Files){FString Ext=FPaths::GetExtension(File);if(Ext!=TEXT("png")&&Ext!=TEXT("wav"))continue;auto T=NewObject();T->Filename=File;T->DestinationPath=Spec.Value;T->DestinationName=FPaths::GetBaseFilename(File);T->bAutomated=true;T->bReplaceExisting=true;T->bSave=true;Tasks.Add(T);}} + Tools.ImportAssetTasks(Tasks);int Count=0;for(auto T:Tasks)Count+=T->GetObjects().Num();UEditorLoadingAndSavingUtils::SaveDirtyPackages(true,true);return FString::Printf(TEXT("OK: %d imported objects"),Count); +} +FString UIronsMigrationToolset::CleanupTemplateRemainders(){ + if(!Bound()||!GEditor||GEditor->PlayWorld)return TEXT("ERROR: exact project required, PIE must be stopped"); + if(!FPackageName::DoesPackageExist(TEXT("/Game/IRONSPORE/Maps/L_Hangar"))||FPackageName::DoesPackageExist(TEXT("/Game/DemoTemplate/_Core/Lvl_IntroRoom")))return TEXT("ERROR: replacement must exist and template world must already be removed"); + FString Json;TSharedPtr Root;if(!FFileHelper::LoadFileToString(Json,*(FPaths::ProjectDir()/TEXT("Docs/QA/UE-Migration/template-manifest.json")))||!FJsonSerializer::Deserialize(TJsonReaderFactory<>::Create(Json),Root))return TEXT("ERROR: reviewed template manifest required"); + const TArray>* Roots=nullptr;if(!Root->TryGetArrayField(TEXT("roots"),Roots))return TEXT("ERROR: no manifest roots"); + FString Content=FPaths::ConvertRelativePathToFull(FPaths::ProjectContentDir());FPaths::NormalizeDirectoryName(Content);TArray DeleteFiles;TArray Directories; + const TArray Allowed={TEXT("DemoTemplate/"),TEXT("FirstPerson/"),TEXT("Characters/"),TEXT("Input/"),TEXT("LevelPrototyping/"),TEXT("__ExternalActors__/"),TEXT("__ExternalObjects__/"),TEXT("Localization/Game/")}; + for(auto& Value:*Roots){auto Entry=Value->AsObject();if(!Entry)return TEXT("ERROR: malformed entry");FString Virtual=Entry->GetStringField(TEXT("root"));FString RelativeRoot=Virtual;RelativeRoot.RemoveFromStart(TEXT("/Game/"));FString Directory=Content/TEXT("")+RelativeRoot;Directories.Add(Directory); + const TArray>* Files=nullptr;if(!Entry->TryGetArrayField(TEXT("files"),Files))return TEXT("ERROR: no files"); + for(auto& Item:*Files){FString Relative=Item->AsString();bool Approved=false;for(auto& Prefix:Allowed)if(Relative.StartsWith(Prefix))Approved=true;if(!Approved||Relative.Contains(TEXT("..")))return TEXT("ERROR: manifest path outside explicit template roots"); + FString File=FPaths::ConvertRelativePathToFull(Content/Relative);FPaths::NormalizeFilename(File);FPaths::CollapseRelativeDirectories(File);if(!File.StartsWith(Content+TEXT("/")))return TEXT("ERROR: absolute target escaped Content");if(!IFileManager::Get().FileExists(*File))continue; + FString Ext=FPaths::GetExtension(File);if(Ext==TEXT("uasset")||Ext==TEXT("umap")){if(!Relative.StartsWith(TEXT("__ExternalActors__/DemoTemplate/_Core/Lvl_IntroRoom/")))return TEXT("ERROR: remaining regular assets must be removed by EditorAssetSubsystem first");} + DeleteFiles.Add(File); + } + } + CollectGarbage(RF_NoFlags); + int Removed=0;for(auto& File:DeleteFiles)if(IFileManager::Get().Delete(*File,true,false,true))Removed++;else return TEXT("ERROR: file deletion failed inside reviewed template manifest"); + for(auto& Directory:Directories){TArray Remain;IFileManager::Get().FindFilesRecursive(Remain,*Directory,TEXT("*.*"),true,false);if(Remain.IsEmpty()&&Directory.StartsWith(Content+TEXT("/")))IFileManager::Get().DeleteDirectory(*Directory,false,true);} + FModuleManager::LoadModuleChecked(TEXT("AssetRegistry")).Get().ScanPathsSynchronous({TEXT("/Game")},true); + return FString::Printf(TEXT("OK: %d manifest-listed remnants removed through bound native editor tool"),Removed); +} +FString UIronsMigrationToolset::Snapshot(){ + if(!Bound())return TEXT("ERROR: wrong project");FString Result=TEXT("project=")+FPaths::ConvertRelativePathToFull(FPaths::GetProjectFilePath());Result+=TEXT("\nmap=")+(GEditor&&GEditor->GetEditorWorldContext().World()?GEditor->GetEditorWorldContext().World()->GetPathName():TEXT("none"));Result+=FString::Printf(TEXT("\nPIE=%s"),GEditor&&GEditor->PlayWorld?TEXT("true"):TEXT("false"));if(auto C=Controller()){auto H=C->Host();Result+=TEXT("\npage=")+C->Page+TEXT("\ndataRoot=")+H->DataRoot+TEXT("\nprofile=")+UTF8_TO_TCHAR(gf::encode(H->Core().state()).c_str());}return Result; +} +FString UIronsMigrationToolset::Acceptance(const FString& Command,const FString& Argument){ + if(!Bound())return TEXT("ERROR: wrong project");auto C=Controller();if(!C)return TEXT("ERROR: no PIE controller");auto H=C->Host();if(!H||!H->DataRoot.Contains(TEXT("/QA/")))return TEXT("ERROR: isolated QA data root required"); + if(Command==TEXT("page"))C->SetPage(Argument); + else if(Command==TEXT("language"))H->SetLanguage(Argument==TEXT("en")); + else if(Command==TEXT("stock"))H->Accept(H->Core().seedAcceptanceStock()); + else if(Command==TEXT("launch"))C->Launch(Argument==TEXT("training")); + else if(Command==TEXT("loot"))H->Accept(H->Core().addLoot("alloy",4,"ue-acceptance-crate")); + else if(Command==TEXT("pod")){std::vector V;for(auto& I:H->Core().state().run.cargo)if(gf::Grid::material(I.itemId))V.push_back(I.uid);H->Accept(H->Core().recoverPod(V));} + else if(Command==TEXT("return")){if(H->FinishSortie(false))C->ReturnHome();} + else if(Command==TEXT("destroy")){if(H->FinishSortie(true))C->ReturnHome();} + else if(Command==TEXT("home"))C->ReturnHome(); + else if(Command==TEXT("view")){if(C->Mech())C->Mech()->SwitchView();} + else return TEXT("ERROR: unknown acceptance command"); + return TEXT("OK: ")+Command; +} +class FIronsEditorModule : public IModuleInterface { + void StartupModule()override{UToolsetRegistry::RegisterToolsetClass(UIronsMigrationToolset::StaticClass());} + void ShutdownModule()override{UToolsetRegistry::UnregisterToolsetClass(UIronsMigrationToolset::StaticClass());} +}; +IMPLEMENT_MODULE(FIronsEditorModule,IronsEditor) diff --git a/Source/IronsEditor/Private/IronsMigrationToolset.h b/Source/IronsEditor/Private/IronsMigrationToolset.h new file mode 100644 index 0000000..19d966f --- /dev/null +++ b/Source/IronsEditor/Private/IronsMigrationToolset.h @@ -0,0 +1,20 @@ +#pragma once +#include "CoreMinimal.h" +#include "ToolsetRegistry/ToolsetDefinition.h" +#include "IronsMigrationToolset.generated.h" +UCLASS(BlueprintType) +class UIronsMigrationToolset : public UToolsetDefinition +{ + GENERATED_BODY() +public: + /** Creates IRONSPORE maps, material, Blueprint and UMG child classes through editor APIs. */ + UFUNCTION(meta=(AICallable),Category="IRONSPORE") static FString Bootstrap(); + /** Imports project SVG-derived PNG and generated WAV assets. Existing project assets are preserved. */ + UFUNCTION(meta=(AICallable),Category="IRONSPORE") static FString ImportVisuals(); + /** Removes only pre-recorded template source remnants and orphan actor packages after their template worlds were removed. */ + UFUNCTION(meta=(AICallable),Category="IRONSPORE") static FString CleanupTemplateRemainders(); + /** Queries the exact project, map, PIE state and core snapshot without modifying game state. */ + UFUNCTION(meta=(AICallable),Category="IRONSPORE") static FString Snapshot(); + /** Drives the task's isolated PIE acceptance profile. Never operates on normal campaign progress. */ + UFUNCTION(meta=(AICallable),Category="IRONSPORE") static FString Acceptance(const FString& Command,const FString& Argument); +}; diff --git a/Source/UEAdapter/Private/IronsHost.cpp b/Source/UEAdapter/Private/IronsHost.cpp new file mode 100644 index 0000000..6e4f3a8 --- /dev/null +++ b/Source/UEAdapter/Private/IronsHost.cpp @@ -0,0 +1,149 @@ +#include "IronsHost.h" +#include "Modules/ModuleManager.h" +#include "Misc/Paths.h" +#include "Misc/FileHelper.h" +#include "Misc/CommandLine.h" +#include "Misc/Parse.h" +#include "Misc/ConfigCacheIni.h" +#include "HAL/PlatformFileManager.h" +#include "Serialization/JsonReader.h" +#include "Serialization/JsonSerializer.h" +#include "Windows/AllowWindowsPlatformTypes.h" +#include +#include "Windows/HideWindowsPlatformTypes.h" + +IMPLEMENT_MODULE(FDefaultModuleImpl, UEAdapter) + +namespace { +std::string Utf8(const FString& S){FTCHARToUTF8 U(*S);return std::string(U.Get(),U.Length());} +FString Wide(std::string_view S){return FString(UTF8_TO_TCHAR(std::string(S).c_str()));} +bool ReadValidated(const FString& Path,std::string& Bytes){ + TArray Data;if(!FFileHelper::LoadFileToArray(Data,*Path))return false; + Bytes.assign(reinterpret_cast(Data.GetData()),Data.Num());gf::Profile P;return static_cast(gf::decode(Bytes,P)); +} +struct FDiskStorage : gf::IProfileStorage { + FString Path; + explicit FDiskStorage(FString P):Path(MoveTemp(P)){} + gf::Result load(std::string& Bytes)override { + auto& Files=FPlatformFileManager::Get().GetPlatformFile(); + if(!Files.FileExists(*Path)&&!Files.FileExists(*(Path+TEXT(".bak"))))return gf::Result::fail(gf::Code::Missing); + if(ReadValidated(Path,Bytes)||ReadValidated(Path+TEXT(".bak"),Bytes))return {}; + return gf::Result::fail(gf::Code::Corrupt,"Both profile and backup are unreadable; files preserved"); + } + gf::Result commitAtomically(std::string_view Bytes)override { + gf::Profile Candidate;if(!gf::decode(Bytes,Candidate))return gf::Result::fail(gf::Code::Corrupt); + auto& Files=FPlatformFileManager::Get().GetPlatformFile();Files.CreateDirectoryTree(*FPaths::GetPath(Path)); + std::string Old;bool Exists=Files.FileExists(*Path),Valid=Exists&&ReadValidated(Path,Old); + if(Exists&&!Valid&&!ReadValidated(Path+TEXT(".bak"),Old))return gf::Result::fail(gf::Code::Corrupt,"Refusing to overwrite damaged progress"); + FString Temp=Path+TEXT(".tmp"); + HANDLE File=CreateFileW(*Temp,GENERIC_WRITE,0,nullptr,CREATE_ALWAYS,FILE_ATTRIBUTE_NORMAL|FILE_FLAG_WRITE_THROUGH,nullptr); + if(File==INVALID_HANDLE_VALUE)return gf::Result::fail(gf::Code::SaveFailed,"Create temporary profile"); + DWORD Written=0;bool Ok=WriteFile(File,Bytes.data(),static_cast(Bytes.size()),&Written,nullptr)&&Written==Bytes.size(); + Ok=FlushFileBuffers(File)&&Ok;CloseHandle(File); + if(Ok){ + if(Valid)Ok=ReplaceFileW(*Path,*Temp,*(Path+TEXT(".bak")),0,nullptr,nullptr)!=0; + else Ok=MoveFileExW(*Temp,*Path,MOVEFILE_REPLACE_EXISTING|MOVEFILE_WRITE_THROUGH)!=0; + } + if(!Ok){Files.DeleteFile(*Temp);return gf::Result::fail(gf::Code::SaveFailed,"Atomic replacement failed");} + return {}; + } +}; +struct FGuidIds : gf::IIdGenerator {std::string next()override{return Utf8(FGuid::NewGuid().ToString(EGuidFormats::Digits));}}; +void LoadTable(const FString& Path,TMap& Table){ + FString Json;TSharedPtr Root; + if(FFileHelper::LoadFileToString(Json,*Path)&&FJsonSerializer::Deserialize(TJsonReaderFactory<>::Create(Json),Root)&&Root){ + const TArray>* Entries=nullptr;if(Root->TryGetArrayField(TEXT("entries"),Entries))for(auto& E:*Entries){auto O=E->AsObject();if(O)Table.Add(O->GetStringField(TEXT("key")),O->GetStringField(TEXT("value")));} + } +} +} +class FIronsSessionHost { +public: + FDiskStorage Storage;FGuidIds Ids;gf::Session Session; + explicit FIronsSessionHost(const FString& Path):Storage(Path),Session(Storage,Ids){} +}; +void FIronsHostDeleter::operator()(FIronsSessionHost* Ptr)const{delete Ptr;} +UIronsGameInstance::UIronsGameInstance()=default; +UIronsGameInstance::~UIronsGameInstance()=default; +void UIronsGameInstance::Init(){ + Super::Init(); + DataRoot=FPaths::ConvertRelativePathToFull(FPaths::ProjectSavedDir()/TEXT("IRONSPORE")); + FString Override;if(FParse::Value(FCommandLine::Get(),TEXT("IronsporeDataRoot="),Override))DataRoot=FPaths::ConvertRelativePathToFull(Override);FPaths::NormalizeDirectoryName(DataRoot); + Campaign.Reset(new FIronsSessionHost(DataRoot/TEXT("Campaign/profile-v1.json"))); + bReady=Accept(Campaign->Session.load()); + if(bReady&&Campaign->Session.state().run.destructionPending)bReady=Accept(Campaign->Session.finish(true)); + FString Raw=FPaths::ProjectContentDir()/TEXT("IRONSPORE/Raw"); + LoadTable(Raw/TEXT("Localization/zh-Hans.json"),Chinese);LoadTable(Raw/TEXT("Localization/en.json"),English); + Keys={TEXT("W"),TEXT("S"),TEXT("A"),TEXT("D"),TEXT("SpaceBar"),TEXT("LeftShift"),TEXT("Q"),TEXT("R"),TEXT("V"),TEXT("E"),TEXT("Tab"),TEXT("G"),TEXT("F"),TEXT("LeftMouseButton"),TEXT("RightMouseButton")}; + PreferencesIni=GGameUserSettingsIni; + if(FParse::Param(FCommandLine::Get(),TEXT("IronsporeQA"))&&DataRoot.Contains(TEXT("/QA/"))){ + PreferencesIni=DataRoot/TEXT("UserPreferences.ini");FConfigFile Preferences; + if(FPlatformFileManager::Get().GetPlatformFile().FileExists(*PreferencesIni))Preferences.Read(PreferencesIni); + // UE 5.8 does not create unknown files through GConfig::SetBool/SetInt. + Preferences.NoSave=false;Preferences.bCanSaveAllSections=true;GConfig->Add(PreferencesIni,Preferences); + } + GConfig->GetBool(TEXT("IRONSPORE"),TEXT("English"),bEnglish,PreferencesIni); + GConfig->GetFloat(TEXT("IRONSPORE"),TEXT("Sensitivity"),Sensitivity,PreferencesIni); + GConfig->GetFloat(TEXT("IRONSPORE"),TEXT("AimSensitivity"),AimSensitivity,PreferencesIni); + GConfig->GetFloat(TEXT("IRONSPORE"),TEXT("MasterVolume"),MasterVolume,PreferencesIni); + GConfig->GetBool(TEXT("IRONSPORE"),TEXT("InvertY"),bInvertY,PreferencesIni); + GConfig->GetBool(TEXT("IRONSPORE"),TEXT("ShowPerformance"),bShowPerformance,PreferencesIni); + GConfig->GetInt(TEXT("IRONSPORE"),TEXT("PerformanceCorner"),PerformanceCorner,PreferencesIni);PerformanceCorner=FMath::Clamp(PerformanceCorner,0,3); + TArray SavedKeys;GConfig->GetArray(TEXT("IRONSPORE"),TEXT("Keys"),SavedKeys,PreferencesIni);if(SavedKeys.Num()==Keys.Num())Keys=SavedKeys; +} +void UIronsGameInstance::Shutdown(){if(bReady)Campaign->Session.save();Training.Reset();Campaign.Reset();Super::Shutdown();} +gf::Session& UIronsGameInstance::Core(){return bTraining?Training->Session:Campaign->Session;} +const gf::Session& UIronsGameInstance::Core()const{return bTraining?Training->Session:Campaign->Session;} +FString UIronsGameInstance::Label(const FString& Key)const {const auto& Table=bEnglish?English:Chinese;if(const FString* V=Table.Find(Key))return *V;return Key;} +FText UIronsGameInstance::Text(const FString& Key)const{return FText::FromString(Label(Key));} +FString UIronsGameInstance::ItemName(std::string_view Id)const { + FString I=Wide(Id);FString Key=TEXT("item.")+I;FString R=Label(Key); + if(R!=Key)return R;Key=TEXT("equipment.")+I+TEXT(".name");R=Label(Key);return R==Key?I:R; +} +void UIronsGameInstance::SetLanguage(bool EnglishLanguage){bEnglish=EnglishLanguage;SavePreferences();} +void UIronsGameInstance::SavePreferences(){ + GConfig->SetBool(TEXT("IRONSPORE"),TEXT("English"),bEnglish,PreferencesIni); + GConfig->SetFloat(TEXT("IRONSPORE"),TEXT("Sensitivity"),Sensitivity,PreferencesIni); + GConfig->SetFloat(TEXT("IRONSPORE"),TEXT("AimSensitivity"),AimSensitivity,PreferencesIni); + GConfig->SetFloat(TEXT("IRONSPORE"),TEXT("MasterVolume"),MasterVolume,PreferencesIni); + GConfig->SetBool(TEXT("IRONSPORE"),TEXT("InvertY"),bInvertY,PreferencesIni); + GConfig->SetBool(TEXT("IRONSPORE"),TEXT("ShowPerformance"),bShowPerformance,PreferencesIni); + GConfig->SetInt(TEXT("IRONSPORE"),TEXT("PerformanceCorner"),FMath::Clamp(PerformanceCorner,0,3),PreferencesIni); + GConfig->SetArray(TEXT("IRONSPORE"),TEXT("Keys"),Keys,PreferencesIni); + if(PreferencesIni==GGameUserSettingsIni)GConfig->Flush(false,PreferencesIni); + else if(auto File=GConfig->FindConfigFile(PreferencesIni)){ + FPlatformFileManager::Get().GetPlatformFile().CreateDirectoryTree(*FPaths::GetPath(PreferencesIni));File->Write(PreferencesIni); + } +} +bool UIronsGameInstance::Accept(const gf::Result& R){ + if(R){Notice(TEXT("ue.saved"));return true;} + static const TCHAR* Errors[]={TEXT("ue.saved"),TEXT("ue.invalid"),TEXT("ue.missing"),TEXT("ue.locked"),TEXT("ue.budget"),TEXT("ue.funds"),TEXT("ue.duplicate"),TEXT("ue.saveFailed"),TEXT("ue.corrupt"),TEXT("ue.unsupported")}; + // Codes include NoSpace before Locked; keep explicit mapping. + switch(R.code){case gf::Code::NoSpace:Notice(TEXT("ue.noSpace"));break;case gf::Code::Locked:Notice(TEXT("ue.locked"));break;case gf::Code::Budget:Notice(TEXT("ue.budget"));break;case gf::Code::Funds:Notice(TEXT("ue.funds"));break;case gf::Code::Duplicate:Notice(TEXT("ue.duplicate"));break;case gf::Code::SaveFailed:Notice(TEXT("ue.saveFailed"));break;case gf::Code::Corrupt:Notice(TEXT("ue.corrupt"));break;case gf::Code::Unsupported:Notice(TEXT("ue.unsupported"));break;default:Notice(Errors[FMath::Min(static_cast(R.code),2)]);break;} + return false; +} +void UIronsGameInstance::Notice(const FString& Key,const FString& Detail){StatusKey=Key;StatusDetail=Detail;StatusUntil=FPlatformTime::Seconds()+5;} +bool UIronsGameInstance::BeginSortie(){if(!bReady)return false;if(Core().state().run.active)return true;Combat={};return Accept(Core().begin(FMath::Rand()));} +bool UIronsGameInstance::FinishSortie(bool Destroyed){bool Ok=Accept(Core().finish(Destroyed));if(Ok)Notice(Destroyed?TEXT("ue.destroyed"):TEXT("ue.extracted"));return Ok;} +void UIronsGameInstance::EnterTraining(){ + if(bTraining)return;Training.Reset(new FIronsSessionHost(DataRoot/TEXT("Training/profile-v1.json"))); + if(!Accept(Training->Session.load())){Training.Reset();return;} + bTraining=true;if(!Core().state().run.active){Core().seedAcceptanceStock();Core().begin(7001,true);}Combat={}; +} +void UIronsGameInstance::LeaveTraining(){if(!bTraining)return;Core().finish(false);Training.Reset();bTraining=false;Combat={};} + +bool RunIronsHostAcceptance(const FString& Root,FString& Report){ + if(!Root.Contains(TEXT("/QA/"))){Report=TEXT("Isolated QA root required");return false;} + int Passed=0,Failed=0;auto Check=[&](bool Value,const TCHAR* Name){if(Value)Passed++;else Failed++;Report+=FString::Printf(TEXT("%s: %s\n"),Value?TEXT("PASS"):TEXT("FAIL"),Name);}; + FString Folder=Root/FGuid::NewGuid().ToString(EGuidFormats::Digits);FIronsSessionHost A(Folder/TEXT("campaign-v1.json"));auto& S=A.Session; + Check(static_cast(S.load()),TEXT("Create atomic V1 profile"));Check(S.state().version==1&&S.state().warehouse.size()==4,TEXT("Fresh owned baseline")); + Check(static_cast(S.begin(20261113)),TEXT("Deploy via independent library"));Check(static_cast(S.addLoot("alloy",4,"ue-source")),TEXT("Loot commit")); + auto Uid=S.state().run.cargo.front().uid;Check(static_cast(S.recoverPod({Uid})),TEXT("Pod banks once"));Check(!S.recoverPod({Uid}),TEXT("Pod replay rejected")); + Check(static_cast(S.addLoot("cells",3,"ue-source2")),TEXT("Second loot commit"));auto SafeUid=S.state().run.cargo.front().uid;Check(static_cast(S.moveItem(SafeUid,gf::Hold::Cargo,gf::Hold::SafeCase)),TEXT("Secure case unique transfer")); + Check(static_cast(S.finish(true)),TEXT("Two-phase death finalization"));FIronsSessionHost B(Folder/TEXT("campaign-v1.json"));Check(static_cast(B.Session.load()),TEXT("Reload real disk profile"));Check(gf::total(B.Session.state().warehouse,"alloy")==4&&gf::total(B.Session.state().warehouse,"cells")==3,TEXT("Recovered ownership survives restart")); + FString Broken=TEXT("{broken-primary");FFileHelper::SaveStringToFile(Broken,*A.Storage.Path,FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM);FIronsSessionHost C(A.Storage.Path);Check(static_cast(C.Session.load()),TEXT("Recover valid atomic backup")); + std::string Backup;Check(ReadValidated(A.Storage.Path+TEXT(".bak"),Backup),TEXT("Backup remains valid"));Check(static_cast(C.Session.state().run.destructionPending?C.Session.finish(true):C.Session.save()),TEXT("Finalize recovered pending death while preserving backup"));Check(ReadValidated(A.Storage.Path+TEXT(".bak"),Backup),TEXT("Repair did not replace backup with corrupt data"));Check(gf::total(C.Session.state().warehouse,"cells")==3,TEXT("Pending backup recovery banks secure cargo exactly once")); + FFileHelper::SaveStringToFile(Broken,*A.Storage.Path,FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM);FFileHelper::SaveStringToFile(Broken,*(A.Storage.Path+TEXT(".bak")),FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM); + FIronsSessionHost D(A.Storage.Path);Check(!D.Session.load(),TEXT("Both damaged files rejected"));auto Commit=D.Storage.commitAtomically(gf::encode(B.Session.state()));Check(!Commit,TEXT("Corrupt profile cannot be overwritten"));FString Preserved;FFileHelper::LoadFileToString(Preserved,*A.Storage.Path);Check(Preserved==Broken,TEXT("Corrupt original preserved")); + FIronsSessionHost Trial(Folder/TEXT("Training/profile-v1.json"));Check(static_cast(Trial.Session.load()),TEXT("Training store independent"));Check(Trial.Session.state().sorties==0&&B.Session.state().sorties==1,TEXT("Training does not mutate campaign")); + Report+=FString::Printf(TEXT("%d passed, %d failed\n"),Passed,Failed);FFileHelper::SaveStringToFile(Report,*(Folder/TEXT("host-results.txt")),FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM);return Failed==0; +} diff --git a/Source/UEAdapter/Private/IronsPerformanceSubsystem.cpp b/Source/UEAdapter/Private/IronsPerformanceSubsystem.cpp new file mode 100644 index 0000000..ab5e0fc --- /dev/null +++ b/Source/UEAdapter/Private/IronsPerformanceSubsystem.cpp @@ -0,0 +1,76 @@ +#include "IronsPerformanceSubsystem.h" +#include "Engine/GameInstance.h" +#include "Engine/World.h" +#include "CoreGlobals.h" +#include "HAL/PlatformTime.h" +#include "RenderTimer.h" +#include "RHI.h" +#include "DynamicRHI.h" +#include "RHICommandList.h" + +namespace { +bool Valid(double Value) { return FMath::IsFinite(Value)&&Value>=0; } +double CyclesMs(uint32 Cycles) { return Cycles>0?FPlatformTime::ToMilliseconds(Cycles):-1; } +} + +void FIronsPerformanceWindow::Reset() { Samples.Reset(); } +void FIronsPerformanceWindow::Add(double End,double Seconds,double Game,double Render,double Rhi,double Gpu) +{ + if(!FMath::IsFinite(End)||!FMath::IsFinite(Seconds)||Seconds<=0)return; + // Reject backwards clocks rather than mixing two timelines. + if(!Samples.IsEmpty()&&End=2048)Samples.RemoveAt(0,1,EAllowShrinking::No); + Samples.Add({End,Seconds,Game,Render,Rhi,Gpu}); +} +FIronsPerformanceSnapshot FIronsPerformanceWindow::Read(double Now) const +{ + FIronsPerformanceSnapshot Result; + double Seconds=0,Sums[4]={};int32 Counts[4]={}; + for(const auto& S:Samples){ + if(S.End<=Now-1.0||S.End>Now)continue; + Seconds+=S.Seconds;++Result.Samples; + const double Values[]={S.Game,S.Render,S.Rhi,S.Gpu}; + for(int32 I=0;I<4;++I)if(Valid(Values[I])){Sums[I]+=Values[I];++Counts[I];} + } + if(Result.Samples>0&&Seconds>0){Result.FrameMs=Seconds*1000/Result.Samples;Result.Fps=Result.Samples/Seconds;} + double* Values[]={&Result.GameMs,&Result.RenderMs,&Result.RhiMs,&Result.GpuMs}; + for(int32 I=0;I<4;++I)if(Counts[I]>0)*Values[I]=Sums[I]/Counts[I]; + // Do not keep showing an old GPU/RHI value after the source becomes unavailable. + if(!Samples.IsEmpty()){ + const auto& Last=Samples.Last(); + if(!Valid(Last.Game))Result.GameMs=-1;if(!Valid(Last.Render))Result.RenderMs=-1; + if(!Valid(Last.Rhi))Result.RhiMs=-1;if(!Valid(Last.Gpu))Result.GpuMs=-1; + } + return Result; +} + +void UIronsPerformanceSubsystem::Initialize(FSubsystemCollectionBase& Collection) +{ + Super::Initialize(Collection);bInitialized=true; +} +void UIronsPerformanceSubsystem::Deinitialize() +{ + bInitialized=false;Window.Reset();SampleWorld.Reset();Super::Deinitialize(); +} +bool UIronsPerformanceSubsystem::IsTickable() const { return bInitialized&&!IsTemplate(); } +UWorld* UIronsPerformanceSubsystem::GetTickableGameObjectWorld() const { return GetGameInstance()?GetGameInstance()->GetWorld():nullptr; } +TStatId UIronsPerformanceSubsystem::GetStatId() const { RETURN_QUICK_DECLARE_CYCLE_STAT(IronsPerformance,STATGROUP_Tickables); } +void UIronsPerformanceSubsystem::Tick(float DeltaTime) +{ + if(LastFrame==GFrameCounter)return;LastFrame=GFrameCounter; + UWorld* World=GetTickableGameObjectWorld();const double Now=FPlatformTime::Seconds(); + if(!World||SampleWorld.Get()!=World){ + SampleWorld=World;Window.Reset();LastSampleTime=Now;LastPublishTime=Now; + const uint64 Revision=Snapshot.Revision+1;Snapshot={};Snapshot.Revision=Revision;Raw={};return; + } + const double Seconds=Now-LastSampleTime;LastSampleTime=Now; + Raw.FrameMs=Seconds*1000;Raw.Fps=Seconds>0?1/Seconds:-1; + Raw.GameMs=CyclesMs(GGameThreadTime);Raw.RenderMs=CyclesMs(GRenderThreadTime); + Raw.RhiMs=IsRunningRHIInSeparateThread()?CyclesMs(GRHIThreadTime):-1; + Raw.GpuMs=GDynamicRHI&&GSupportsTimestampRenderQueries?CyclesMs(RHIGetGPUFrameCycles()):-1; + Window.Add(Now,Seconds,Raw.GameMs,Raw.RenderMs,Raw.RhiMs,Raw.GpuMs); + if(Now-LastPublishTime>=.25){ + const uint64 Revision=Snapshot.Revision+1;Snapshot=Window.Read(Now);Snapshot.Revision=Revision;LastPublishTime=Now; + } +} diff --git a/Source/UEAdapter/Private/Tests/IronsPerformanceTests.cpp b/Source/UEAdapter/Private/Tests/IronsPerformanceTests.cpp new file mode 100644 index 0000000..0599882 --- /dev/null +++ b/Source/UEAdapter/Private/Tests/IronsPerformanceTests.cpp @@ -0,0 +1,37 @@ +#include "IronsPerformanceSubsystem.h" +#include "Misc/AutomationTest.h" +#include + +#if WITH_DEV_AUTOMATION_TESTS +IMPLEMENT_SIMPLE_AUTOMATION_TEST(FIronsPerformanceWindowTest,"IRONSPORE.Performance.RollingWindow",EAutomationTestFlags::EditorContext|EAutomationTestFlags::EngineFilter) +bool FIronsPerformanceWindowTest::RunTest(const FString& Parameters) +{ + FIronsPerformanceWindow W; + W.Add(10,.01,1,2,3,4);W.Add(10.03,.03,3,4,5,6); + auto S=W.Read(10.03); + TestEqual(TEXT("FPS uses total elapsed time, not average reciprocal"),S.Fps,50.0); + TestEqual(TEXT("Frame time in milliseconds"),S.FrameMs,20.0); + TestEqual(TEXT("CPU samples averaged independently"),S.GameMs,2.0); + W.Add(11.2,.02,5,6,-1,-1);S=W.Read(11.2); + TestEqual(TEXT("Expired samples removed"),S.Samples,1); + TestTrue(TEXT("Unavailable GPU does not retain last valid number"),S.GpuMs<0); + TestTrue(TEXT("Disabled RHI thread is unavailable"),S.RhiMs<0); + TestEqual(TEXT("An idle CPU may legitimately measure zero"),[&](){W.Add(11.22,.02,0,0,0,0);return W.Read(11.22).GpuMs;}(),0.0); + TestTrue(TEXT("A stale window produces no FPS"),W.Read(13).Fps<0); + return true; +} +IMPLEMENT_SIMPLE_AUTOMATION_TEST(FIronsPerformanceClockTest,"IRONSPORE.Performance.ClockAndValidity",EAutomationTestFlags::EditorContext|EAutomationTestFlags::EngineFilter) +bool FIronsPerformanceClockTest::RunTest(const FString& Parameters) +{ + FIronsPerformanceWindow W;const double Nan=std::numeric_limits::quiet_NaN(); + W.Add(1,0,1,2,3,4);W.Add(1,Nan,1,2,3,4); + TestEqual(TEXT("Invalid frame intervals rejected"),W.Read(1).Samples,0); + W.Add(2,.02,Nan,2,-1,Nan); + TestTrue(TEXT("Invalid timings unavailable"),W.Read(2).GameMs<0&&W.Read(2).GpuMs<0); + W.Add(1,.5,1,2,3,4); + TestEqual(TEXT("Backwards clock starts a new timeline"),W.Read(1).Samples,1); + TestEqual(TEXT("Wall clock remains meaningful during gameplay pause"),W.Read(1).Fps,2.0); + W.Reset();TestTrue(TEXT("Travel reset clears old numbers"),W.Read(1).Fps<0); + return true; +} +#endif diff --git a/Source/UEAdapter/Public/IronsHost.h b/Source/UEAdapter/Public/IronsHost.h new file mode 100644 index 0000000..2d15867 --- /dev/null +++ b/Source/UEAdapter/Public/IronsHost.h @@ -0,0 +1,52 @@ +#pragma once +#include "CoreMinimal.h" +#include "Engine/GameInstance.h" +#include "irons/gameFramework/Game.hpp" +#include "IronsHost.generated.h" + +class FIronsSessionHost; +struct FIronsHostDeleter {void operator()(FIronsSessionHost* Ptr)const;}; +namespace gf = irons::gameFramework; +UEADAPTER_API bool RunIronsHostAcceptance(const FString& Root,FString& Report); + +UCLASS() +class UEADAPTER_API UIronsGameInstance : public UGameInstance +{ + GENERATED_BODY() + TUniquePtr Campaign; + TUniquePtr Training; + TMap Chinese, English; + FString PreferencesIni; + bool bTraining=false; +public: + UIronsGameInstance(); + virtual ~UIronsGameInstance() override; + virtual void Init() override; + virtual void Shutdown() override; + gf::Session& Core(); + const gf::Session& Core()const; + gf::Combat Combat; + bool bReady=false; + bool bEnglish=false; + float Sensitivity=1,AimSensitivity=1, MasterVolume=.65f; + bool bInvertY=false; + bool bShowPerformance=true; + int32 PerformanceCorner=0; // 0 right top, 1 left top, 2 right bottom, 3 left bottom. + TArray Keys; + FString StatusKey, StatusDetail; + double StatusUntil=0; + FString DataRoot; + int AcceptanceStep=0; + FText Text(const FString& Key)const; + FString Label(const FString& Key)const; + FString ItemName(std::string_view Id)const; + void SetLanguage(bool EnglishLanguage); + void SavePreferences(); + void EnterTraining(); + void LeaveTraining(); + bool IsTraining()const {return bTraining;} + bool Accept(const gf::Result& Result); + bool BeginSortie(); + bool FinishSortie(bool Destroyed); + void Notice(const FString& Key,const FString& Detail=TEXT("")); +}; diff --git a/Source/UEAdapter/Public/IronsPerformanceSubsystem.h b/Source/UEAdapter/Public/IronsPerformanceSubsystem.h new file mode 100644 index 0000000..3630429 --- /dev/null +++ b/Source/UEAdapter/Public/IronsPerformanceSubsystem.h @@ -0,0 +1,46 @@ +#pragma once + +#include "CoreMinimal.h" +#include "Subsystems/GameInstanceSubsystem.h" +#include "Tickable.h" +#include "IronsPerformanceSubsystem.generated.h" + +// UE telemetry only. A negative value means that no valid measurement is available. +struct UEADAPTER_API FIronsPerformanceSnapshot +{ + double Fps=-1,FrameMs=-1,GameMs=-1,RenderMs=-1,RhiMs=-1,GpuMs=-1; + int32 Samples=0; + uint64 Revision=0; +}; + +class UEADAPTER_API FIronsPerformanceWindow +{ + struct FSample { double End=0,Seconds=0,Game=-1,Render=-1,Rhi=-1,Gpu=-1; }; + TArray Samples; +public: + void Reset(); + void Add(double End,double Seconds,double Game,double Render,double Rhi,double Gpu); + FIronsPerformanceSnapshot Read(double Now) const; +}; + +UCLASS() +class UEADAPTER_API UIronsPerformanceSubsystem : public UGameInstanceSubsystem, public FTickableGameObject +{ + GENERATED_BODY() + FIronsPerformanceWindow Window; + FIronsPerformanceSnapshot Snapshot,Raw; + TWeakObjectPtr SampleWorld; + double LastSampleTime=0,LastPublishTime=0; + uint64 LastFrame=MAX_uint64; + bool bInitialized=false; +public: + virtual void Initialize(FSubsystemCollectionBase& Collection) override; + virtual void Deinitialize() override; + virtual void Tick(float DeltaTime) override; + virtual TStatId GetStatId() const override; + virtual bool IsTickable() const override; + virtual bool IsTickableWhenPaused() const override { return true; } + virtual UWorld* GetTickableGameObjectWorld() const override; + const FIronsPerformanceSnapshot& GetSnapshot() const { return Snapshot; } + const FIronsPerformanceSnapshot& GetRawSample() const { return Raw; } +}; diff --git a/Source/UEAdapter/UEAdapter.Build.cs b/Source/UEAdapter/UEAdapter.Build.cs new file mode 100644 index 0000000..32ac517 --- /dev/null +++ b/Source/UEAdapter/UEAdapter.Build.cs @@ -0,0 +1,10 @@ +using UnrealBuildTool; +public class UEAdapter : ModuleRules +{ + public UEAdapter(ReadOnlyTargetRules Target) : base(Target) + { + PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs; + PublicDependencyModuleNames.AddRange(new[] { "Core", "CoreUObject", "Engine", "GameFrameworkExternal" }); + PrivateDependencyModuleNames.AddRange(new[] { "Json", "JsonUtilities", "RHI", "RenderCore" }); + } +} diff --git a/Source/ueRouge.Target.cs b/Source/ueRouge.Target.cs new file mode 100644 index 0000000..47062fa --- /dev/null +++ b/Source/ueRouge.Target.cs @@ -0,0 +1,12 @@ +using UnrealBuildTool; +public class ueRougeTarget : TargetRules +{ + public ueRougeTarget(TargetInfo Target) : base(Target) + { + Type = TargetType.Game; + DefaultBuildSettings = BuildSettingsVersion.V7; + IncludeOrderVersion = EngineIncludeOrderVersion.Unreal5_8; + CppStandard = CppStandardVersion.Cpp20; + ExtraModuleNames.AddRange(new[] { "ueRouge", "UEAdapter" }); + } +} diff --git a/Source/ueRouge/Private/IronsAcceptance.cpp b/Source/ueRouge/Private/IronsAcceptance.cpp new file mode 100644 index 0000000..6e0224c --- /dev/null +++ b/Source/ueRouge/Private/IronsAcceptance.cpp @@ -0,0 +1,55 @@ +#include "IronsGame.h" +#include "Misc/CommandLine.h" +#include "Misc/Parse.h" +#include "Misc/Paths.h" +#include "Misc/FileHelper.h" +#include "HighResScreenshot.h" +#include "Engine/GameViewportClient.h" +#include "Kismet/KismetSystemLibrary.h" +#include "Serialization/JsonSerializer.h" +#include "HAL/FileManager.h" +#include "GameFramework/CharacterMovementComponent.h" +#include "EnhancedInputComponent.h" + +void AIronsController::TickAcceptance(float Dt){ +#if !UE_BUILD_SHIPPING + FString Scenario;if(!FParse::Value(FCommandLine::Get(),TEXT("IronsporeQAScenario="),Scenario))return; + auto H=Host();if(!H||!H->bReady||!H->DataRoot.Contains(TEXT("/QA/")))return; + FString Folder;if(!FParse::Value(FCommandLine::Get(),TEXT("IronsporeQAEvidence="),Folder)||!Folder.Contains(TEXT("/Docs/QA/")))return; + if(Scenario.StartsWith(TEXT("performance"))){TickPerformanceAcceptance(Dt);return;} + IFileManager::Get().MakeDirectory(*Folder,true); + AcceptanceClock+=Dt;int Step=H->AcceptanceStep; + auto Shot=[&](const TCHAR* Name){FString Path=Folder/TEXT("")+Name+FString::Printf(TEXT("-%d.png"),Step);FScreenshotRequest::RequestScreenshot(Path,true,false);}; + auto Record=[&](const TCHAR* Name){FFileHelper::SaveStringToFile(UTF8_TO_TCHAR(gf::encode(H->Core().state()).c_str()),*(Folder/(FString(Name)+TEXT(".json"))),FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM);}; + auto Advance=[&](){H->AcceptanceStep++;AcceptanceClock=0;}; + if(Scenario==TEXT("manual")){ + if(Mech()&&AcceptanceClock>.1f){auto M=Mech()->GetCharacterMovement();auto L=Mech()->GetActorLocation();auto V=Mech()->GetVelocity();auto E=Cast(InputComponent);bool Mapped=E&&Actions.Num()>0&&E->GetBoundActionValue(Actions[0]).Get();FString Line=FString::Printf(TEXT("%.3f,%.3f,%.3f,%.3f,%.3f,%d,%d,%d,%d,%d\n"),GetWorld()->GetTimeSeconds(),L.X,L.Y,L.Z,V.Size(),static_cast(M->MovementMode),M->IsComponentTickEnabled()?1:0,IsMoveInputIgnored()?1:0,IsInputKeyDown(FKey(*H->Keys[0]))?1:0,Mapped?1:0);FFileHelper::SaveStringToFile(Line,*(Folder/TEXT("movement.csv")),FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM,&IFileManager::Get(),FILEWRITE_Append);AcceptanceClock=0;} + return; + } + if(Scenario==TEXT("ui")){ + if(AcceptanceClock<3)return; + if(Step==0){H->Core().seedAcceptanceStock();H->SetLanguage(false);SetPage(TEXT("home"));Shot(TEXT("home-zh"));} + else if(Step==1){SetPage(TEXT("hangar"));Shot(TEXT("hangar-zh"));} + else if(Step==2){H->SetLanguage(true);Shot(TEXT("hangar-en"));} + else if(Step==3){SetPage(TEXT("settings"));Shot(TEXT("settings-en"));} + else if(Step==4){H->SetLanguage(false);SetPage(TEXT("research"));Shot(TEXT("research-zh"));} + else{Record(TEXT("ui-profile"));UKismetSystemLibrary::QuitGame(this,this,EQuitPreference::Quit,false);return;} + Advance();return; + } + if(Scenario!=TEXT("cycle"))return; + if(Playing()&&Mech()&&Step<9){auto& R=H->Core().state().run;H->Core().vitals(H->Core().catalog().chassis[R.chassis].armor,R.energy,R.elapsed,R.escortArmor);if(AcceptanceClock>2&&Step==2)FrameSamples.Add(Dt);} + if(AcceptanceClock<3)return; + if(Step==0){H->SetLanguage(false);Shot(TEXT("cycle-home"));Record(TEXT("before"));Advance();} + else if(Step==1){Advance();Launch();} + else if(Step==2){if(AcceptanceClock<7)return;Shot(TEXT("shoulder"));Record(TEXT("deployed"));FString Stats;float Sum=0,Max=0;for(float T:FrameSamples){Sum+=T;Max=FMath::Max(Max,T);}Stats=FString::Printf(TEXT("{\"samples\":%d,\"seconds\":%.3f,\"averageFps\":%.3f,\"worstFrameMs\":%.3f,\"environment\":\"Packaged Win64, short battle sampling; shader warmup excluded\"}\n"),FrameSamples.Num(),Sum,Sum>0?FrameSamples.Num()/Sum:0,Max*1000);FFileHelper::SaveStringToFile(Stats,*(Folder/TEXT("frame-sample.json")),FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM);H->Core().addLoot("alloy",4,"qa-pod-source");H->Core().recoverPod({H->Core().state().run.cargo.front().uid});H->Core().addLoot("cells",2,"qa-safe-source");auto Uid=H->Core().state().run.cargo.front().uid;H->Core().moveItem(Uid,gf::Hold::Cargo,gf::Hold::SafeCase);Advance();} + else if(Step==3){SetPage(TEXT("inventory"));Shot(TEXT("cargo-recovery"));Record(TEXT("pod-recovered"));Advance();} + else if(Step==4){SetPage(TEXT("play"));if(Mech())Mech()->SwitchView();Shot(TEXT("cockpit"));Advance();} + else if(Step==5){if(auto M=GetWorld()->GetAuthGameMode()){if(Mech()&&M->Extraction)Mech()->SetActorLocation(M->Extraction->GetActorLocation()+FVector(0,0,180));M->Interact();}Advance();} + else if(Step==6){auto M=GetWorld()->GetAuthGameMode();if(!M||!M->bExtractionReady)return;Record(TEXT("ready-still-active"));Shot(TEXT("ready-to-board"));Advance();M->Interact();} + else if(Step==7){Record(TEXT("returned"));Shot(TEXT("after-extraction"));Advance();} + else if(Step==8){Advance();Launch();} + else if(Step==9){H->Core().addLoot("alloy",5,"qa-loss-source");H->Core().addLoot("cells",3,"qa-survival-source");auto& Items=H->Core().state().run.cargo;for(auto& I:Items)if(I.itemId=="cells"){auto Uid=I.uid;H->Core().moveItem(Uid,gf::Hold::Cargo,gf::Hold::SafeCase);break;}Advance();if(Mech())Mech()->ReceiveHit(100000);} + else if(Step==10){Record(TEXT("destroyed"));Shot(TEXT("after-destruction"));Advance();} + else{Record(TEXT("final"));UKismetSystemLibrary::QuitGame(this,this,EQuitPreference::Quit,false);} +#endif +} diff --git a/Source/ueRouge/Private/IronsCombat.cpp b/Source/ueRouge/Private/IronsCombat.cpp new file mode 100644 index 0000000..9cc7ff7 --- /dev/null +++ b/Source/ueRouge/Private/IronsCombat.cpp @@ -0,0 +1,78 @@ +#include "IronsGame.h" +#include "Camera/CameraComponent.h" +#include "GameFramework/SpringArmComponent.h" +#include "GameFramework/CharacterMovementComponent.h" +#include "Components/CapsuleComponent.h" +#include "Components/StaticMeshComponent.h" +#include "Kismet/GameplayStatics.h" +#include "EngineUtils.h" +#include "DrawDebugHelpers.h" +#include "Sound/SoundBase.h" + +namespace { +void Sound(UWorld* World,const TCHAR* Name,float Volume){FString P=FString::Printf(TEXT("/Game/IRONSPORE/Audio/%s.%s"),Name,Name);if(auto S=LoadObject(nullptr,*P))UGameplayStatics::PlaySound2D(World,S,Volume);} +void Blast(UWorld* W,FVector P,float Damage,float Radius){for(TActorIterator I(W);I;++I){float D=FVector::Dist(I->GetActorLocation(),P);float Amount=gf::radialDamage(Damage,D*.01f,Radius*.01f);if(Amount>0)I->Hit(Amount);}DrawDebugSphere(W,P,Radius,20,FColor::Orange,false,.35f,0,3);} +} +AIronsCharacter::AIronsCharacter(){ + PrimaryActorTick.bCanEverTick=true; + GetCapsuleComponent()->InitCapsuleSize(65,160); + Body=CreateDefaultSubobject(TEXT("MechBody"));Body->SetupAttachment(GetRootComponent());Body->SetRelativeLocation(FVector(0,0,-160));Body->SetCollisionEnabled(ECollisionEnabled::NoCollision); + Arm=CreateDefaultSubobject(TEXT("ShoulderArm"));Arm->SetupAttachment(GetRootComponent());Arm->TargetArmLength=600;Arm->SocketOffset=FVector(0,125,90);Arm->bUsePawnControlRotation=true; + Camera=CreateDefaultSubobject(TEXT("DualViewCamera"));Camera->SetupAttachment(Arm);Camera->FieldOfView=70; + bUseControllerRotationYaw=true;GetCharacterMovement()->bOrientRotationToMovement=false;GetCharacterMovement()->JumpZVelocity=700;GetCharacterMovement()->GravityScale=2.04f;GetCharacterMovement()->MaxStepHeight=65;GetCharacterMovement()->AirControl=.5f; +} +UIronsGameInstance* AIronsCharacter::Host()const{return Cast(GetGameInstance());} +void AIronsCharacter::BeginPlay(){Super::BeginPlay();Body->SetStaticMesh(LoadObject(nullptr,TEXT("/Game/IRONSPORE/Art/Mechs/SM_MechScout.SM_MechScout")));Body->SetRelativeRotation(FRotator(0,-90,0));if(auto H=Host()){int Chassis=H->Core().state().run.chassis;Body->SetRelativeScale3D(Chassis==0?FVector(.85f,.85f,.92f):Chassis==2?FVector(1.15f,1.13f,1.05f):FVector::OneVector);}} +void AIronsCharacter::SwitchView(){bCockpit=!bCockpit;Arm->TargetArmLength=bCockpit?0:600;Arm->SocketOffset=bCockpit?FVector(25,0,125):FVector(0,125,90);Body->SetVisibility(!bCockpit,true);} +void AIronsCharacter::Tick(float Dt){ + Super::Tick(Dt);auto H=Host();auto PC=Cast(GetController());if(!H||!H->bReady||!PC||!PC->Playing())return; + auto& S=H->Core();auto& R=S.state().run; + GetCharacterMovement()->MaxWalkSpeed=(H->Combat.boost>0?S.catalog().chassis[R.chassis].boostSpeed:S.moveSpeed())*100; + Camera->SetFieldOfView(FMath::FInterpTo(Camera->FieldOfView,bAim?50.f:bCockpit?78.f:70.f,Dt,12)); + if(H->Combat.boost>0){FVector D=GetLastMovementInputVector();if(D.IsNearlyZero())D=GetActorForwardVector();GetCharacterMovement()->Velocity=D.GetSafeNormal()*GetCharacterMovement()->MaxWalkSpeed+FVector(0,0,GetVelocity().Z);} + if(PC->IsInputKeyDown(FKey(*H->Keys[4]))&&!GetCharacterMovement()->IsMovingOnGround()){float Lift=S.lift(Dt);if(Lift>0)GetCharacterMovement()->Velocity.Z=Lift*100;} + if(R.armor<=0&&H->FinishSortie(true))PC->ReturnHome(); +} +void AIronsCharacter::ReceiveHit(float Damage){auto H=Host();if(!H||!H->Core().state().run.active)return;H->Core().takeDamage(H->Combat,Damage);DamageTime=GetWorld()->GetTimeSeconds();Sound(GetWorld(),TEXT("S_Impact"),H->MasterVolume*.6f);} +void AIronsCharacter::Shoot(bool Secondary){ + auto H=Host();if(!H||!H->Core().fire(H->Combat,Secondary))return; + auto& S=H->Core();auto P=S.catalog().part(Secondary?1:0,S.state().run.equipment[Secondary?1:0]);if(!P)return; + FVector Eye=Camera->GetComponentLocation(),Dir=Camera->GetForwardVector(),End=Eye+Dir*18000; + FCollisionQueryParams Q(SCENE_QUERY_STAT(IronsAim),true,this);FHitResult Aim;GetWorld()->LineTraceSingleByChannel(Aim,Eye,End,ECC_Visibility,Q);FVector TargetPoint=Aim.bBlockingHit?Aim.ImpactPoint:End; + FVector Muzzle=GetActorLocation()+GetActorForwardVector()*125+GetActorRightVector()*75+FVector(0,0,45); + float Damage=S.shotDamage(Secondary),Radius=S.blastRadius(Secondary)*100; + if(Secondary&&P->guided){ + FActorSpawnParameters Params;Params.Owner=this;auto Missile=GetWorld()->SpawnActor(Muzzle,(TargetPoint-Muzzle).Rotation(),Params);Missile->Damage=Damage;Missile->Radius=Radius;Missile->Velocity=(TargetPoint-Muzzle).GetSafeNormal()*2200; + float Best=.96f;for(TActorIterator I(GetWorld());I;++I){float Dot=FVector::DotProduct(Dir,(I->GetActorLocation()-Eye).GetSafeNormal());if(Dot>Best){Best=Dot;Missile->Target=*I;}} + }else if(Secondary){Blast(GetWorld(),Muzzle+Dir*700,Damage,Radius);} + else { + FHitResult Hit;GetWorld()->LineTraceSingleByChannel(Hit,Muzzle,TargetPoint+Dir*15,ECC_Visibility,Q); + FVector Impact=Hit.bBlockingHit?Hit.ImpactPoint:TargetPoint; + if(Radius>0)Blast(GetWorld(),Impact,Damage,Radius);else if(auto E=Cast(Hit.GetActor()))E->Hit(Damage); + DrawDebugLine(GetWorld(),Muzzle,Impact,P->id=="coil"?FColor::Cyan:FColor(255,185,75),false,.07f,0,P->id=="coil"?5:2); + if(Hit.bBlockingHit)DrawDebugSphere(GetWorld(),Impact,15,8,FColor::Orange,false,.12f); + } + FireFlash=.1f;Sound(GetWorld(),Secondary?TEXT("S_Secondary"):TEXT("S_Shot"),H->MasterVolume*.55f); +} +AIronsEnemy::AIronsEnemy(){PrimaryActorTick.bCanEverTick=true;Body=CreateDefaultSubobject(TEXT("EnemyBody"));SetRootComponent(Body);Body->SetCollisionProfileName(TEXT("BlockAllDynamic"));} +void AIronsEnemy::BeginPlay(){Super::BeginPlay();if(EntityId.IsEmpty())EntityId=GetName();if(auto H=Cast(GetGameInstance()))H->Core().registerEnemy(TCHAR_TO_UTF8(*EntityId),Type,bTerminal); + Body->SetStaticMesh(LoadObject(nullptr,Type==1?TEXT("/Engine/BasicShapes/Sphere.Sphere"):Type==2||Type>=4?TEXT("/Engine/BasicShapes/Cube.Cube"):TEXT("/Game/IRONSPORE/Art/Mechs/SM_MechScout.SM_MechScout"))); + SetActorScale3D(FVector(Type==3?1.5f:Type==1?1.1f:Type==2?1.7f:.8f)); + if(Type>=4)SetActorScale3D(FVector(2.1f,.35f,2.3f)); +} +void AIronsEnemy::Hit(float Damage){auto H=Cast(GetGameInstance());if(!H)return;bool Dead=H->Core().damageEnemy(TCHAR_TO_UTF8(*EntityId),Damage);if(Type>=4)DrawDebugSphere(GetWorld(),GetActorLocation(),90,10,FColor::Cyan,false,.15f,0,5);if(Dead){ + if(Type!=1){FActorSpawnParameters Params;auto Loot=GetWorld()->SpawnActor(GetActorLocation()+FVector(0,0,50),FRotator::ZeroRotator,Params);Loot->Kind=TEXT("loot");Loot->ItemId=Type==3?TEXT("reactive_guard"):Type==2?TEXT("cells"):TEXT("alloy");Loot->Quantity=Type==2?3:Type==3?1:4;Loot->SourceKey=TEXT("wreck-")+EntityId;} + SetActorHiddenInGame(true);SetActorEnableCollision(false);SetActorTickEnabled(bTerminal&&!H->Core().state().run.routeCompleted);DrawDebugSphere(GetWorld(),GetActorLocation()+FVector(0,0,120),150,12,FColor::Orange,false,.6f,0,8); +}} +void AIronsEnemy::Tick(float Dt){ + Super::Tick(Dt);auto PC=Cast(UGameplayStatics::GetPlayerController(this,0));auto H=Cast(GetGameInstance());if(!PC||!PC->Playing()||!H||!PC->Mech())return; + if(Type>=4){if(Type==5){FVector L=GetActorLocation();L.X=-3500+FMath::Sin(GetWorld()->GetTimeSeconds())*450;SetActorLocation(L);}return;} + auto P=PC->Mech();FVector Origin=GetActorLocation()+FVector(0,0,Type==1?0:160),Target=P->GetActorLocation();FHitResult Hit;FCollisionQueryParams Q(SCENE_QUERY_STAT(IronsAI),true,this);GetWorld()->LineTraceSingleByChannel(Hit,Origin,Target,ECC_Visibility,Q); + auto D=H->Core().stepEnemy(TCHAR_TO_UTF8(*EntityId),Dt,FVector::Dist(Origin,Target)*.01f,!Hit.bBlockingHit||Hit.GetActor()==P); + if(D.fire){P->ReceiveHit(D.damage);DrawDebugLine(GetWorld(),Origin,Target,FColor::Red,false,.12f,0,2);} + if(D.moveSpeed>0){FVector Delta=(Target-Origin).GetSafeNormal2D()*D.moveSpeed*100*Dt;SetActorLocation(GetActorLocation()+Delta,true);SetActorRotation(FRotator(0,(Target-Origin).Rotation().Yaw,0));} + if(D.fly){FVector L=GetActorLocation();L.Z=350+FMath::Sin(GetWorld()->GetTimeSeconds()*1.7f)*60;SetActorLocation(L);} +} +AIronsProjectile::AIronsProjectile(){PrimaryActorTick.bCanEverTick=true;Body=CreateDefaultSubobject(TEXT("Missile"));SetRootComponent(Body);Body->SetCollisionEnabled(ECollisionEnabled::NoCollision);Body->SetStaticMesh(LoadObject(nullptr,TEXT("/Engine/BasicShapes/Sphere.Sphere")));SetActorScale3D(FVector(.2f));InitialLifeSpan=8;} +void AIronsProjectile::Tick(float Dt){Super::Tick(Dt);if(auto PC=Cast(UGameplayStatics::GetPlayerController(this,0));!PC||!PC->Playing())return;if(IsValid(Target)&&!Target->IsHidden()){FVector Desired=(Target->GetActorLocation()+FVector(0,0,110)-GetActorLocation()).GetSafeNormal()*2200;Velocity=FMath::VInterpTo(Velocity,Desired,Dt,4);} + FVector Start=GetActorLocation(),End=Start+Velocity*Dt;FHitResult Hit;FCollisionQueryParams Q(SCENE_QUERY_STAT(IronsMissile),true,GetOwner());GetWorld()->LineTraceSingleByChannel(Hit,Start,End,ECC_Visibility,Q);if(Hit.bBlockingHit){Blast(GetWorld(),Hit.ImpactPoint,Damage,Radius);Destroy();}else SetActorLocation(End);} diff --git a/Source/ueRouge/Private/IronsInput.cpp b/Source/ueRouge/Private/IronsInput.cpp new file mode 100644 index 0000000..88ebade --- /dev/null +++ b/Source/ueRouge/Private/IronsInput.cpp @@ -0,0 +1,93 @@ +#include "IronsGame.h" +#include "IronsPerformanceOverlay.h" +#include "EnhancedInputComponent.h" +#include "EnhancedInputSubsystems.h" +#include "InputAction.h" +#include "InputMappingContext.h" +#include "Engine/LocalPlayer.h" +#include "GameFramework/CharacterMovementComponent.h" +#include "Kismet/GameplayStatics.h" +#include "Blueprint/WidgetBlueprintLibrary.h" +#include "Engine/Engine.h" +#include "Engine/GameViewportClient.h" +#include "Widgets/SWindow.h" + +AIronsController::AIronsController(){PrimaryActorTick.bCanEverTick=true;} +UIronsGameInstance* AIronsController::Host()const{return Cast(GetGameInstance());} +AIronsCharacter* AIronsController::Mech()const{return Cast(GetPawn());} +void AIronsController::BeginPlay(){ + Super::BeginPlay();RebuildInput();UClass* Class=LoadClass(nullptr,TEXT("/Game/IRONSPORE/UI/WBP_IronsRoot.WBP_IronsRoot_C"));if(!Class)Class=UIronsScreen::StaticClass();Screen=CreateWidget(this,Class);Screen->AddToViewport(10);SetPage(GetWorld()->GetMapName().Contains(TEXT("L_Hangar"))?TEXT("home"):TEXT("play")); + PerformanceOverlay=CreateWidget(this);PerformanceOverlay->AddToViewport(30); + if(GEngine&&GEngine->GameViewport)if(auto Window=GEngine->GameViewport->GetWindow())Window->ShowWindow(); +} +void AIronsController::EndPlay(const EEndPlayReason::Type Reason){ + if(PerformanceOverlay)PerformanceOverlay->RemoveFromParent();if(Screen)Screen->RemoveFromParent();Super::EndPlay(Reason); +} +void AIronsController::RebuildInput(){ + auto H=Host();auto Local=GetLocalPlayer();auto Sub=Local?Local->GetSubsystem():nullptr;if(!H||!Sub)return; + if(Context)Sub->RemoveMappingContext(Context);Context=NewObject(this);Actions.Empty(); + auto E=Cast(InputComponent);if(E)E->ClearActionValueBindings(); + for(int I=0;IKeys.Num();I++){auto A=NewObject(this);A->ValueType=EInputActionValueType::Boolean;Context->MapKey(A,FKey(*H->Keys[I]));Actions.Add(A);if(E)E->BindActionValue(A);} + Sub->AddMappingContext(Context,0); +} +void AIronsController::SetPage(const FString& NewPage){ + if(Page!=NewPage)PreviousPage=Page;Page=NewPage;bTrigger=false;bPodHeld=false;if(auto H=Host())H->Combat.freshTrigger=true; + bool Play=Playing();bShowMouseCursor=!Play;ResetIgnoreLookInput();ResetIgnoreMoveInput();SetIgnoreLookInput(!Play);SetIgnoreMoveInput(!Play); + if(Mech()){Mech()->GetCharacterMovement()->SetComponentTickEnabled(Play);if(!Play)Mech()->GetCharacterMovement()->StopMovementImmediately();} + if(Play){FInputModeGameOnly Mode;SetInputMode(Mode);}else{FInputModeGameAndUI Mode;Mode.SetHideCursorDuringCapture(false);Mode.SetLockMouseToViewportBehavior(EMouseLockMode::DoNotLock);if(Screen)Mode.SetWidgetToFocus(Screen->TakeWidget());SetInputMode(Mode);} + if(Screen)Screen->ClearSelection(); +} +void AIronsController::Launch(bool Training){ + auto H=Host();if(!H||!H->bReady)return;if(Training)H->EnterTraining();else if(H->IsTraining())H->LeaveTraining(); + if(!H->IsTraining()&&!H->BeginSortie())return; + UGameplayStatics::OpenLevel(this,Training?TEXT("/Game/IRONSPORE/Maps/L_TrainingRange"):TEXT("/Game/IRONSPORE/Maps/L_Warzone")); +} +void AIronsController::ReturnHome(){if(auto H=Host()){if(H->IsTraining())H->LeaveTraining();else if(H->Core().state().run.active)H->Accept(H->Core().save());}UGameplayStatics::OpenLevel(this,TEXT("/Game/IRONSPORE/Maps/L_Hangar"));} +void AIronsController::Menu(){if(Playing())SetPage(TEXT("pause"));else if(Mech())SetPage(TEXT("play"));else SetPage(TEXT("home"));} +void AIronsController::Interact(){if(Playing())if(auto M=GetWorld()->GetAuthGameMode())M->Interact();} +void AIronsController::FirePressed(){if(Playing()){bTrigger=true;if(Mech())Mech()->Shoot();}} +void AIronsController::FireReleased(){bTrigger=false;} +void AIronsController::ProcessKey(FKey Key,bool Pressed){ + auto H=Host();if(!H)return; + if(Rebind>=0&&Pressed){if(Key==EKeys::Escape){Rebind=-1;return;}if(Key.IsGamepadKey())return; + FString Name=Key.GetFName().ToString();for(int I=0;IKeys.Num();I++)if(I!=Rebind&&H->Keys[I]==Name){H->Notice(TEXT("ue.keyConflict"));return;}H->Keys[Rebind]=Name;Rebind=-1;H->SavePreferences();RebuildInput();return;} + if(Key==EKeys::Escape&&Pressed){Menu();return;} + if(!Playing())return; + if(Key==FKey(*H->Keys[13])){if(Pressed)FirePressed();else FireReleased();return;} + if(Key==FKey(*H->Keys[14])){if(Mech())Mech()->bAim=Pressed;return;} + int Index=H->Keys.IndexOfByKey(Key.GetFName().ToString());if(Index<0)return; + if(Index==11){bPodHeld=Pressed;return;}if(Index==4&&!Pressed){if(Mech())Mech()->StopJumping();return;}if(!Pressed)return; + if(Index==4){if(Mech())Mech()->Jump();} + else if(Index==5)H->Core().boost(H->Combat); + else if(Index==6)H->Core().guard(H->Combat); + else if(Index==7)H->Core().reload(H->Combat); + else if(Index==8){if(Mech())Mech()->SwitchView();} + else if(Index==9)Interact(); + else if(Index==10)SetPage(TEXT("inventory")); + else if(Index==12){if(Mech())Mech()->Shoot(true);} +} +bool AIronsController::InputKey(const FInputKeyEventArgs& Params){if(Params.Event==IE_Pressed||Params.Event==IE_Released)ProcessKey(Params.Key,Params.Event==IE_Pressed);return Super::InputKey(Params);} +void AIronsController::PlayerTick(float Dt){ + Super::PlayerTick(Dt);TickAcceptance(Dt);auto H=Host();auto P=Mech();if(!H||!H->bReady||!Playing()||!P)return; + auto& S=H->Core();S.tick(Dt,H->Combat,!IsInputKeyDown(FKey(*H->Keys[13]))); + auto E=Cast(InputComponent);auto Down=[&](int I){return E&&Actions.IsValidIndex(I)?E->GetBoundActionValue(Actions[I]).Get():IsInputKeyDown(FKey(*H->Keys[I]));}; + FRotator Yaw(0,GetControlRotation().Yaw,0);FVector Forward=Yaw.Vector(),Right=FRotationMatrix(Yaw).GetUnitAxis(EAxis::Y); + P->AddMovementInput(Forward,(Down(0)?1.f:0.f)-(Down(1)?1.f:0.f));P->AddMovementInput(Right,(Down(3)?1.f:0.f)-(Down(2)?1.f:0.f)); + float X=0,Y=0;GetInputMouseDelta(X,Y);float MouseScale=H->Sensitivity*(P->bAim?H->AimSensitivity:1);AddYawInput(X*MouseScale);AddPitchInput(Y*MouseScale*(H->bInvertY?1:-1)); + if(bTrigger)P->Shoot(); + static float SaveClock=0;SaveClock+=Dt;if(SaveClock>30){SaveClock=0;H->Accept(S.save());} +} +void AIronsController::IronsUi(const FString& NewPage){ +#if !UE_BUILD_SHIPPING + if(FParse::Param(FCommandLine::Get(),TEXT("IronsporeQA")))SetPage(NewPage); +#endif +} +void AIronsController::IronsQA(const FString& Command){ +#if !UE_BUILD_SHIPPING + if(!FParse::Param(FCommandLine::Get(),TEXT("IronsporeQA")))return; + auto H=Host();if(!H)return;if(Command==TEXT("stock"))H->Accept(H->Core().seedAcceptanceStock()); + else if(Command==TEXT("return")){if(H->FinishSortie(false))ReturnHome();} + else if(Command==TEXT("destroy")){if(H->FinishSortie(true))ReturnHome();} + else if(Command==TEXT("loot"))H->Accept(H->Core().addLoot("alloy",4,"qa-crate")); +#endif +} diff --git a/Source/ueRouge/Private/IronsPerformanceAcceptance.cpp b/Source/ueRouge/Private/IronsPerformanceAcceptance.cpp new file mode 100644 index 0000000..75975a2 --- /dev/null +++ b/Source/ueRouge/Private/IronsPerformanceAcceptance.cpp @@ -0,0 +1,77 @@ +#include "IronsGame.h" +#include "IronsPerformanceSubsystem.h" +#include "IronsPerformanceOverlay.h" +#include "Misc/CommandLine.h" +#include "Misc/Parse.h" +#include "Misc/FileHelper.h" +#include "Misc/Paths.h" +#include "HAL/PlatformTime.h" +#include "HighResScreenshot.h" +#include "Engine/GameViewportClient.h" +#include "UnrealClient.h" +#include "Kismet/KismetSystemLibrary.h" +#include "Serialization/JsonSerializer.h" +#include "UObject/UObjectIterator.h" +#include "Containers/Ticker.h" + +void AIronsController::TickPerformanceAcceptance(float Dt) +{ +#if !UE_BUILD_SHIPPING + auto H=Host();if(!H||!H->bReady||!H->DataRoot.Contains(TEXT("/QA/")))return; + FString Scenario,Folder; + if(!FParse::Value(FCommandLine::Get(),TEXT("IronsporeQAScenario="),Scenario)||!Scenario.StartsWith(TEXT("performance"))|| + !FParse::Value(FCommandLine::Get(),TEXT("IronsporeQAEvidence="),Folder)||!Folder.Contains(TEXT("/Docs/QA/")))return; + const double Now=FPlatformTime::Seconds();if(PerformanceLastClock>0)AcceptanceClock+=Now-PerformanceLastClock;PerformanceLastClock=Now; + if(AcceptanceClock<2.5f)return; + const int32 Step=H->AcceptanceStep;auto Metrics=H->GetSubsystem(); + auto Advance=[&](){++H->AcceptanceStep;AcceptanceClock=0;}; + auto Action=[&](const TCHAR* Cmd){FIronsHitRegion Hit;Hit.Command=Cmd;Screen->Dispatch(Hit);}; + auto Quit=[&](){UKismetSystemLibrary::QuitGame(this,this,EQuitPreference::Quit,false);}; + auto Capture=[&](const TCHAR* Name){ + auto O=MakeShared();O->SetStringField(TEXT("name"),Name);O->SetStringField(TEXT("page"),Page);O->SetStringField(TEXT("map"),GetWorld()->GetMapName()); + O->SetBoolField(TEXT("enabled"),H->bShowPerformance);O->SetBoolField(TEXT("english"),H->bEnglish);O->SetNumberField(TEXT("corner"),H->PerformanceCorner); + O->SetBoolField(TEXT("worldPaused"),GetWorld()->IsPaused());O->SetNumberField(TEXT("saveVersion"),H->Core().state().version); + int32 VX=0,VY=0;GetViewportSize(VX,VY);O->SetNumberField(TEXT("width"),VX);O->SetNumberField(TEXT("height"),VY); + int32 Overlays=0;for(TObjectIterator It;It;++It)if(It->GetWorld()==GetWorld()&&It->IsInViewport())++Overlays;O->SetNumberField(TEXT("overlayInstances"),Overlays); + auto Numbers=[](const FIronsPerformanceSnapshot& S){auto J=MakeShared();J->SetNumberField(TEXT("fps"),S.Fps);J->SetNumberField(TEXT("frameMs"),S.FrameMs);J->SetNumberField(TEXT("gameMs"),S.GameMs);J->SetNumberField(TEXT("renderMs"),S.RenderMs);J->SetNumberField(TEXT("rhiMs"),S.RhiMs);J->SetNumberField(TEXT("gpuMs"),S.GpuMs);J->SetNumberField(TEXT("samples"),S.Samples);J->SetNumberField(TEXT("revision"),static_cast(S.Revision));return J;}; + if(Metrics){O->SetObjectField(TEXT("metrics"),Numbers(Metrics->GetSnapshot()));O->SetObjectField(TEXT("raw"),Numbers(Metrics->GetRawSample()));} + if(auto V=GetWorld()->GetGameViewport();V&&V->GetStatUnitData()){const auto* U=V->GetStatUnitData();auto J=MakeShared();J->SetNumberField(TEXT("frameMs"),U->RawFrameTime);J->SetNumberField(TEXT("gameMs"),U->RawGameThreadTime);J->SetNumberField(TEXT("renderMs"),U->RawRenderThreadTime);J->SetNumberField(TEXT("rhiMs"),U->RawRHITTime);J->SetNumberField(TEXT("gpuMs"),U->RawGPUFrameTime[0]);O->SetObjectField(TEXT("nativeStatUnitRaw"),J);} + FString Json;FJsonSerializer::Serialize(O,TJsonWriterFactory<>::Create(&Json));FFileHelper::SaveStringToFile(Json,*(Folder/(FString(Name)+TEXT(".json"))),FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM); + // Allow new-language glyph uploads and camera history to settle before capture. + const FString Path=Folder/(FString(Name)+TEXT(".png")); + FTSTicker::GetCoreTicker().AddTicker(FTickerDelegate::CreateLambda([Path](float){FScreenshotRequest::RequestScreenshot(Path,true,false);return false;}),.25f); + }; + if(Scenario==TEXT("performance-ui")){ + if(Step==0){H->SetLanguage(false);Capture(TEXT("home-zh"));} + else if(Step==1){SetPage(TEXT("hangar"));Capture(TEXT("hangar-zh"));} + else if(Step==2){SetPage(TEXT("settings"));Capture(TEXT("settings-zh"));} + else if(Step==3){H->SetLanguage(true);Capture(TEXT("settings-en"));} + else {Quit();return;} + Advance();return; + } + if(Scenario==TEXT("performance-restart")){ + if(Step==0){SetPage(TEXT("settings"));Capture(TEXT("restart-disabled"));} + else if(Step==1){Action(TEXT("performance"));Action(TEXT("performanceCorner"));H->SetLanguage(false);Capture(TEXT("restart-enabled"));} + else if(Step==2){ConsoleCommand(TEXT("stat unit"),true);} + else if(Step==3){Capture(TEXT("native-comparison"));} + else {ConsoleCommand(TEXT("stat unit"),true);Quit();return;} + Advance();return; + } + if(Scenario!=TEXT("performance"))return; + if(Step==0){Capture(TEXT("home-default-zh"));} + else if(Step==1){SetPage(TEXT("hangar"));Capture(TEXT("hangar-default-zh"));} + else if(Step==2){SetPage(TEXT("settings"));Action(TEXT("performance"));Capture(TEXT("disabled-zh"));} + else if(Step==3){Action(TEXT("performance"));Action(TEXT("performanceCorner"));H->SetLanguage(true);Capture(TEXT("top-left-en"));} + else if(Step==4){Action(TEXT("performanceCorner"));SetPage(TEXT("home"));Capture(TEXT("bottom-right-en"));} + else if(Step==5){Action(TEXT("performanceCorner"));SetPage(TEXT("settings"));Capture(TEXT("bottom-left-en"));} + else if(Step==6){Action(TEXT("performanceCorner"));H->SetLanguage(false);Advance();Launch(true);return;} + else if(Step==7){Capture(TEXT("training-shoulder"));} + else if(Step==8){if(Mech())Mech()->SwitchView();Capture(TEXT("training-cockpit"));} + else if(Step==9){SetPage(TEXT("pause"));PrimaryActorTick.bTickEvenWhenPaused=true;bShouldPerformFullTickWhenPaused=true;SetPause(true);Capture(TEXT("pause-begin"));} + else if(Step==10){Capture(TEXT("pause-updating"));SetPause(false);bShouldPerformFullTickWhenPaused=false;PrimaryActorTick.bTickEvenWhenPaused=false;} + else if(Step==11){Advance();ReturnHome();return;} + else if(Step==12){Capture(TEXT("after-travel"));Action(TEXT("performance"));H->PerformanceCorner=3;H->SetLanguage(true);H->SavePreferences();} + else {Quit();return;} + Advance(); +#endif +} diff --git a/Source/ueRouge/Private/IronsPerformanceOverlay.cpp b/Source/ueRouge/Private/IronsPerformanceOverlay.cpp new file mode 100644 index 0000000..c6ed2f6 --- /dev/null +++ b/Source/ueRouge/Private/IronsPerformanceOverlay.cpp @@ -0,0 +1,86 @@ +#include "IronsPerformanceOverlay.h" +#include "IronsHost.h" +#include "IronsPerformanceSubsystem.h" +#include "Blueprint/WidgetTree.h" +#include "Components/CanvasPanel.h" +#include "Rendering/DrawElements.h" +#include "Styling/CoreStyle.h" +#include "Fonts/CompositeFont.h" +#include "Fonts/FontMeasure.h" +#include "Framework/Application/SlateApplication.h" +#include "Misc/Paths.h" + +namespace { +const TSharedPtr& PerformanceFont(){ + static TSharedPtr Font=MakeShared(TEXT("Regular"),FPaths::ProjectContentDir()/TEXT("IRONSPORE/Raw/Fonts/IronsSansSC-Regular.ttf"),EFontHinting::Default,EFontLoadingPolicy::LazyLoad);return Font; +} +} + +FIronsPerformanceLayout FIronsPerformanceLayout::Make(FVector2D Size,bool Enabled,int32 Corner) +{ + FIronsPerformanceLayout L; + Size.X=FMath::Max(1.0,Size.X);Size.Y=FMath::Max(1.0,Size.Y); + L.PanelScale=FMath::Min(Size.X/1920.f,Size.Y/1080.f); + Corner=FMath::Clamp(Corner,0,3); + const bool Left=Corner==1||Corner==3,Bottom=Corner>=2; + const float S=L.PanelScale,Width=660*S,Height=72*S,Margin=16*S; + const float X=Left?Margin:Size.X-Margin-Width,Y=Bottom?Size.Y-Margin-Height:Margin; + L.Panel=FSlateRect(X,Y,X+Width,Y+Height); + const float Band=Enabled?104*S:0; + L.ContentScale=FMath::Min(Size.X/1920.f,(Size.Y-Band)/1080.f); + L.ContentOrigin=(Size-FVector2D(1920,1080)*L.ContentScale-FVector2D(0,Band))*.5f; + if(!Bottom)L.ContentOrigin.Y+=Band; + return L; +} +void UIronsPerformanceOverlay::NativeConstruct() +{ + Super::NativeConstruct();SetIsFocusable(false);SetVisibility(ESlateVisibility::HitTestInvisible); + if(WidgetTree&&!WidgetTree->RootWidget)WidgetTree->RootWidget=WidgetTree->ConstructWidget(); +} +void UIronsPerformanceOverlay::NativeTick(const FGeometry& Geometry,float DeltaTime) +{ + Super::NativeTick(Geometry,DeltaTime); + auto H=GetGameInstance();auto Metrics=GetGameInstance()?GetGameInstance()->GetSubsystem():nullptr; + if(!H||!Metrics)return;const auto& S=Metrics->GetSnapshot(); + const float Scale=FIronsPerformanceLayout::Make(Geometry.GetLocalSize(),true,H->PerformanceCorner).PanelScale; + if(CachedRevision==S.Revision&&bCachedEnglish==H->bEnglish&&FMath::IsNearlyEqual(CachedScale,Scale))return; + CachedRevision=S.Revision;bCachedEnglish=H->bEnglish;CachedScale=Scale; + auto Fit=[&](const FString& Text,float Width,int32 Size){ + FSlateFontInfo Info(PerformanceFont(),FMath::Max(1,FMath::RoundToInt(Size*Scale))); + const auto Measure=FSlateApplication::Get().GetRenderer()->GetFontMeasureService(); + const float Measured=Measure->Measure(Text,Info).X; + if(Measured>Width*Scale)Info.Size=FMath::Max(7,FMath::FloorToInt(Info.Size*Width*Scale/Measured)); + Measure->Measure(Text,Info); // Prepare the actual glyph size before either widget paints. + return Info.Size; + }; + const TCHAR* Keys[]={TEXT("ue.perfFps"),TEXT("ue.perfFrame"),TEXT("ue.perfGpu"),TEXT("ue.perfGame"),TEXT("ue.perfRender"),TEXT("ue.perfRhi")}; + const double Numbers[]={S.Fps,S.FrameMs,S.GpuMs,S.GameMs,S.RenderMs,S.RhiMs}; + for(int32 I=0;I<6;++I){ + Labels[I]=H->Label(Keys[I]);const double N=Numbers[I]; + Values[I]=N<0||!FMath::IsFinite(N)?TEXT("—"):I==0?FString::Printf(TEXT("%.1f"),N):FString::Printf(TEXT("%.2f ms"),N); + LabelSizes[I]=Fit(Labels[I],96,14);ValueSizes[I]=Fit(Values[I],106,16); + Colors[I]=FLinearColor(.4f,.55f,.6f); + if(N>=0&&FMath::IsFinite(N)){double Ms=I==0?1000/FMath::Max(N,.001):N; + Colors[I]=Ms>33.3334?FLinearColor(1,.34f,.28f):Ms>16.6667?FLinearColor(.98f,.73f,.3f):FLinearColor(.35f,.88f,.83f);} + } +} +int32 UIronsPerformanceOverlay::NativePaint(const FPaintArgs& Args,const FGeometry& G,const FSlateRect& Cull,FSlateWindowElementList& Out,int32 Layer,const FWidgetStyle& Style,bool Enabled)const +{ + int32 Z=Super::NativePaint(Args,G,Cull,Out,Layer,Style,Enabled); + auto H=GetGameInstance();if(!H||!H->bShowPerformance)return Z; + const auto L=FIronsPerformanceLayout::Make(G.GetLocalSize(),true,H->PerformanceCorner);const float S=L.PanelScale; + auto Geo=[&](float X,float Y,float W,float Height){return G.ToPaintGeometry(FVector2D(W,Height)*S,FSlateLayoutTransform(FVector2D(L.Panel.Left,L.Panel.Top)+FVector2D(X,Y)*S));}; + FSlateDrawElement::MakeBox(Out,++Z,Geo(0,0,660,72),FCoreStyle::Get().GetBrush(TEXT("WhiteBrush")),ESlateDrawEffect::None,FLinearColor(.012f,.025f,.033f,.94f)); + FSlateDrawElement::MakeBox(Out,++Z,Geo(0,0,3,72),FCoreStyle::Get().GetBrush(TEXT("WhiteBrush")),ESlateDrawEffect::None,FLinearColor(.25f,.7f,.7f)); + auto DrawText=[&](float X,float Y,float Width,const FString& Text,int32 Size,FLinearColor Color){ + FSlateFontInfo Info(PerformanceFont(),FMath::Max(1,Size)); + Out.PushClip(FSlateClippingZone(Geo(X,Y,Width,25))); + FSlateDrawElement::MakeText(Out,++Z,Geo(X,Y,Width,25),Text,Info,ESlateDrawEffect::None,Color); + Out.PopClip(); + }; + for(int32 I=0;I<6;++I){float X=14+(I%3)*216,Y=9+(I/3)*32; + DrawText(X,Y,96,Labels[I],LabelSizes[I],FLinearColor(.7f,.8f,.83f)); + DrawText(X+102,Y-1,106,Values[I],ValueSizes[I],Colors[I]); + } + return Z; +} diff --git a/Source/ueRouge/Private/IronsScreen.cpp b/Source/ueRouge/Private/IronsScreen.cpp new file mode 100644 index 0000000..673f252 --- /dev/null +++ b/Source/ueRouge/Private/IronsScreen.cpp @@ -0,0 +1,217 @@ +#include "IronsGame.h" +#include "IronsPerformanceOverlay.h" +#include "Blueprint/WidgetTree.h" +#include "Components/CanvasPanel.h" +#include "Components/StaticMeshComponent.h" +#include "Rendering/DrawElements.h" +#include "Styling/CoreStyle.h" +#include "Framework/Application/SlateApplication.h" +#include "Fonts/FontMeasure.h" +#include "Fonts/CompositeFont.h" +#include "Engine/Texture2D.h" +#include "GameFramework/GameUserSettings.h" +#include "Kismet/KismetSystemLibrary.h" +#include "Misc/Paths.h" +#include "EngineUtils.h" + +namespace { +FString W(std::string_view S){return FString(UTF8_TO_TCHAR(std::string(S).c_str()));} +std::string U(const FString& S){return std::string(TCHAR_TO_UTF8(*S));} +const FLinearColor Ink(.79f,.87f,.9f),Muted(.39f,.5f,.56f),Teal(.28f,.82f,.8f),Amber(.96f,.68f,.26f),Panel(.025f,.041f,.052f,.94f),Line(.13f,.23f,.28f,.9f); +struct FPainter { + const FGeometry& G;FSlateWindowElementList& Out;int Layer;float Scale;FVector2D Origin;FString FontPath; + FPainter(const FGeometry& Geo,FSlateWindowElementList& O,int L,const UIronsGameInstance* H):G(Geo),Out(O),Layer(L){auto Layout=FIronsPerformanceLayout::Make(G.GetLocalSize(),H&&H->bShowPerformance,H?H->PerformanceCorner:0);Scale=Layout.ContentScale;Origin=Layout.ContentOrigin;FontPath=FPaths::ProjectContentDir()/TEXT("IRONSPORE/Raw/Fonts/IronsSansSC-Regular.ttf");} + FPaintGeometry Geo(float X,float Y,float Width,float Height)const{return G.ToPaintGeometry(FVector2D(Width,Height)*Scale,FSlateLayoutTransform(FVector2D(X,Y)*Scale+Origin));} + void Rect(float X,float Y,float Width,float Height,FLinearColor C){FSlateDrawElement::MakeBox(Out,Layer++,Geo(X,Y,Width,Height),FCoreStyle::Get().GetBrush(TEXT("WhiteBrush")),ESlateDrawEffect::None,C);} + void Frame(float X,float Y,float Width,float Height,FLinearColor C=Line){Rect(X,Y,Width,1,C);Rect(X,Y+Height-1,Width,1,C);Rect(X,Y,1,Height,C);Rect(X+Width-1,Y,1,Height,C);} + void Text(float X,float Y,const FString& String,float Size=20,FLinearColor C=Ink,float MaxWidth=0){ + static TSharedPtr Composite=MakeShared(TEXT("Regular"),FontPath,EFontHinting::Default,EFontLoadingPolicy::LazyLoad); + FSlateFontInfo Font(Composite,Size*Scale,TEXT("Regular"));FString S=String; + if(MaxWidth>0){auto Measure=FSlateApplication::Get().GetRenderer()->GetFontMeasureService();while(S.Len()>1&&Measure->Measure(S,Font).X>MaxWidth*Scale)S.LeftChopInline(1);if(S!=String&&S.Len()>2)S=S.LeftChop(2)+TEXT("…");} + FSlateDrawElement::MakeText(Out,Layer++,Geo(X,Y,1,1),S,Font,ESlateDrawEffect::None,C); + } + void Image(float X,float Y,float Width,float Height,UTexture2D* Texture,FLinearColor Tint=FLinearColor::White){if(!Texture)return;FSlateBrush Brush;Brush.SetResourceObject(Texture);Brush.ImageSize=FVector2D(Texture->GetSizeX(),Texture->GetSizeY());Brush.DrawAs=ESlateBrushDrawType::Image;float Ratio=FMath::Min(Width/Texture->GetSizeX(),Height/Texture->GetSizeY());FVector2D D(Texture->GetSizeX()*Ratio,Texture->GetSizeY()*Ratio);FSlateDrawElement::MakeBox(Out,Layer++,Geo(X+(Width-D.X)*.5f,Y+(Height-D.Y)*.5f,D.X,D.Y),&Brush,ESlateDrawEffect::None,Tint);} +}; +} +AIronsController* UIronsScreen::Controller()const{return Cast(GetOwningPlayer());} +UIronsGameInstance* UIronsScreen::Host()const{return Cast(GetGameInstance());} +void UIronsScreen::NativeConstruct(){Super::NativeConstruct();SetIsFocusable(true);SetVisibility(ESlateVisibility::Visible);if(!WidgetTree->RootWidget)WidgetTree->RootWidget=WidgetTree->ConstructWidget();WidgetTree->RootWidget->SetVisibility(ESlateVisibility::Visible); + for(const auto& E:gf::Catalog::defaults().equipment){FString Id=W(E.id);Icons.Add(Id,LoadObject(nullptr,*(TEXT("/Game/IRONSPORE/UI/Icons/T_")+Id+TEXT(".T_")+Id)));} + Icons.Add(TEXT("silhouette"),LoadObject(nullptr,TEXT("/Game/IRONSPORE/UI/Icons/T_MechFrontSilhouette.T_MechFrontSilhouette"))); +} +void UIronsScreen::ClearSelection(){Selected.Empty();DragUid.Empty();bDragging=false;SelectedSlot=-1;Scroll=0;} +std::vector UIronsScreen::SelectedPodItems()const{std::vector V;for(const auto& S:PodSelection)V.push_back(U(S));return V;} +int32 UIronsScreen::NativePaint(const FPaintArgs& Args,const FGeometry& G,const FSlateRect& Cull,FSlateWindowElementList& Out,int32 Layer,const FWidgetStyle& Style,bool Enabled)const{ + int32 Base=Super::NativePaint(Args,G,Cull,Out,Layer,Style,Enabled);auto H=Host();auto C=Controller();if(!H||!C)return Base; + Hits.Empty();Grids.Empty();FPainter P(G,Out,Base+1,H);auto L=[&](const TCHAR* Key){return H->Label(Key);};auto& S=H->Core();const auto& State=S.state();const auto& Run=State.run; + auto Hit=[&](float X,float Y,float Width,float Height,FString Cmd,FString Arg=TEXT(""),int A=0,int B=0,int D=0){Hits.Add({FSlateRect(X,Y,X+Width,Y+Height),MoveTemp(Cmd),MoveTemp(Arg),A,B,D});}; + auto Button=[&](float X,float Y,float Width,float Height,const FString& Text,const FString& Cmd,const FString& Arg=TEXT(""),int A=0,bool Active=false){bool Hover=FSlateRect(X,Y,X+Width,Y+Height).ContainsPoint(Pointer);P.Rect(X,Y,Width,Height,Active?FLinearColor(.08f,.27f,.29f,.98f):Hover?FLinearColor(.10f,.17f,.21f,.98f):Panel);P.Frame(X,Y,Width,Height,Active?Teal:Line);P.Rect(X,Y,3,Height,Active||Hover?Teal:Muted);P.Text(X+16,Y+Height*.5f-15,Text,20,Active?Teal:Ink,Width-30);Hit(X,Y,Width,Height,Cmd,Arg,A);}; + auto Section=[&](float X,float Y,float Width,float Height,const FString& Title,const FString& Sub){P.Rect(X,Y,Width,Height,Panel);P.Frame(X,Y,Width,Height);P.Rect(X,Y,Width,3,Teal*.7f);P.Text(X+22,Y+17,Title,25);P.Text(X+22,Y+56,Sub,15,Muted,Width-35);P.Rect(X+20,Y+91,Width-40,1,Line);}; + auto DrawItem=[&](float X,float Y,float Width,float Height,const gf::Item& I,gf::Hold Hold,bool Reserved=false){ + FString Id=W(I.itemId),Uid=W(I.uid);bool Sel=Selected==Uid,Pod=PodSelection.Contains(Uid);P.Rect(X+2,Y+2,Width-4,Height-4,Sel?FLinearColor(.09f,.28f,.31f):FLinearColor(.065f,.105f,.13f));P.Frame(X+2,Y+2,Width-4,Height-4,Pod?Amber:Sel?Teal:Line); + if(const auto* T=Icons.Find(Id);T&&*T)P.Image(X+7,Y+12,Width-14,Height-27,*T);else{P.Text(X+5,Y+Height*.35f-4,Id==TEXT("alloy")?TEXT("Fe"):Id==TEXT("cells")?TEXT("+"):TEXT("BP"),Width<70?16:24,Teal,Width-10);} + if(Width>=70&&Height>=70)P.Text(X+7,Y+4,H->ItemName(I.itemId),12,Ink,Width-13);if(I.count>1)P.Text(X+Width-23,Y+Height-19,FString::FromInt(I.count),12,Teal); + if(Reserved)P.Text(X+7,Y+Height-23,L(TEXT("hangar.reserved")),11,Amber,Width-10);if(I.borrowed)P.Text(X+7,Y+Height-23,L(TEXT("ue.borrowed")),11,Amber,Width-10); + Hit(X,Y,Width,Height,TEXT("item"),Uid,static_cast(Hold)); + }; + auto Grid=[&](float X,float Y,int Width,int Height,float Cell,gf::Hold Hold,const std::vector& Items,int StartRow=0,int VisibleRows=0){ + int Rows=VisibleRows?VisibleRows:Height;P.Rect(X,Y,Width*Cell,Rows*Cell,FLinearColor(.018f,.03f,.039f));for(int I=0;I<=Width;I++)P.Rect(X+I*Cell,Y,1,Rows*Cell,Line);for(int I=0;I<=Rows;I++)P.Rect(X,Y+I*Cell,Width*Cell,1,Line); + Grids.Add({FSlateRect(X,Y,X+Width*Cell,Y+Rows*Cell),Hold,Cell,Width,Height}); + Out.PushClip(FSlateClippingZone(P.Geo(X,Y,Width*Cell,Rows*Cell))); + for(const auto& I:Items){auto Size=gf::Grid::size(I.itemId,I.rotated);if(I.y+Size.second<=StartRow||I.y>=StartRow+Rows)continue;DrawItem(X+I.x*Cell,Y+(I.y-StartRow)*Cell,Size.first*Cell,Size.second*Cell,I,Hold,Hold==gf::Hold::Warehouse&&!Run.active&&S.reserved(I.uid));} + Out.PopClip(); + }; + bool Play=C->Playing();FString Page=C->Page; + if(!Play){P.Rect(0,0,1920,1080,FLinearColor(.005f,.015f,.025f,Page==TEXT("home")?.18f:.7f));P.Rect(0,0,1920,112,FLinearColor(.015f,.026f,.035f,.93f));P.Rect(40,110,1840,1,Line);P.Text(56,28,TEXT("IRONSPORE"),35);P.Text(320,44,TEXT("// ")+L(TEXT("ue.subtitle")),16,Muted);P.Text(1440,28,FString::Printf(TEXT("%s %03d / %s %03d"),*L(TEXT("ue.sorties")),State.sorties,*L(TEXT("ue.returns")),State.extractions),16,Teal,430);P.Text(1440,62,Run.active?L(TEXT("ue.activeRun")):L(TEXT("ue.standby")),14,Muted);} + if(Page==TEXT("home")){ + P.Rect(40,150,480,770,FLinearColor(.02f,.034f,.046f,.82f));P.Text(75,176,L(TEXT("ue.baseTitle")),32);P.Text(78,225,TEXT("01 / OPERATIONS"),15,Teal); + Button(76,282,405,65,L(TEXT("ue.enterHangar")),TEXT("page"),TEXT("hangar"),0,true); + Button(76,365,405,65,L(Run.active?TEXT("ue.resume"):TEXT("ue.deploy")),TEXT("launch")); + Button(76,448,405,65,L(TEXT("ue.training")),TEXT("training"));Button(76,531,405,65,L(TEXT("ue.research")),TEXT("page"),TEXT("research"));Button(76,614,405,65,L(TEXT("ue.settings")),TEXT("page"),TEXT("settings"));Button(76,697,405,65,L(TEXT("ue.quit")),TEXT("quit")); + P.Text(78,832,L(TEXT("ue.baseHint")),16,Muted,405); + Section(1470,168,410,390,L(TEXT("ue.pilot")),TEXT("RCI / IRON-01"));P.Text(1494,282,L(TEXT("ue.warehouse")),18,Muted);P.Text(1494,321,FString::Printf(TEXT("%d / %d"),gf::Grid::occupied(State.warehouse),State.warehouseWidth*State.warehouseHeight),37,Teal);P.Text(1494,391,L(TEXT("ue.pending")),17,Muted);P.Text(1494,428,FString::FromInt(State.recoveryInbox.size()),34,State.recoveryInbox.empty()?Ink:Amber);P.Text(1494,500,L(TEXT("ue.completed"))+TEXT(" ")+FString::FromInt(State.completed),14,Muted); + }else if(Page==TEXT("hangar")||Page==TEXT("inventory")){ + bool Field=Page==TEXT("inventory");Section(40,135,520,790,L(TEXT("hangar.equipment")),L(Field?TEXT("ue.fieldEquipment"):Run.active?TEXT("hangar.readOnly"):TEXT("hangar.draft")));Section(580,135,500,790,L(TEXT("hangar.carry")),L(TEXT("ue.carryHint")));Section(1100,135,780,790,L(Field?TEXT("ue.fieldSystems"):TEXT("ue.warehouse")),L(Field?TEXT("ue.fieldHint"):TEXT("ue.warehouseHint"))); + if(const auto* T=Icons.Find(TEXT("silhouette"));T&&*T)P.Image(175,250,250,450,*T,FLinearColor(.35f,.47f,.55f,.4f)); + auto Parts=Run.active?Run.equipment:S.draft().equipment;int Chassis=Run.active?Run.chassis:S.draft().chassis; + for(int SlotIndex=0;SlotIndex<4;SlotIndex++){float X=SlotIndex%2?351:63,Y=245+(SlotIndex/2)*230;auto E=S.catalog().part(SlotIndex,Parts[SlotIndex]);gf::Item Item;Item.itemId=E?E->id:"";Item.uid="slot"+std::to_string(SlotIndex);if(Run.active)for(const auto& I:Run.installed)if(gf::Grid::slot(I.itemId)==SlotIndex)Item=I; + P.Rect(X,Y,184,200,FLinearColor(.03f,.065f,.08f,.8f));P.Frame(X,Y,184,200,SelectedSlot==SlotIndex?Teal:Line);if(E){if(const auto* T=Icons.Find(W(E->id));T&&*T)P.Image(X+20,Y+25,144,115,*T);P.Text(X+10,Y+151,H->ItemName(E->id),16,Ink,164);}else P.Text(X+20,Y+90,L(TEXT("hangar.emptySlot")),17,Muted); + FString SlotKey=SlotIndex==0?TEXT("ue.primary"):SlotIndex==1?TEXT("ue.secondary"):SlotIndex==2?TEXT("ue.booster"):TEXT("ue.guard");P.Text(X+8,Y+5,H->Label(SlotKey),13,Muted);Hit(X,Y,184,200,TEXT("slot"),TEXT(""),SlotIndex);} + P.Text(65,714,H->Label(FString::Printf(TEXT("chassis.chassis_%d.name"),Chassis)),23,Teal);float Mass=0,Power=0;for(int SlotIndex=0;SlotIndex<4;SlotIndex++)if(auto E=S.catalog().part(SlotIndex,Parts[SlotIndex])){Mass+=E->mass;Power+=E->power;}const auto& Ch=S.catalog().chassis[Chassis];P.Text(65,757,FString::Printf(TEXT("%s %.0f / %.0f %s %.0f / %.0f"),*L(TEXT("ue.mass")),Mass,Ch.massCapacity,*L(TEXT("ue.power")),Power,Ch.powerCapacity),17,S.catalog().fits(Chassis,Parts)?Ink:Amber,460); + for(int I=0;I<3;I++)Button(65+I*157,817,146,55,H->Label(FString::Printf(TEXT("chassis.chassis_%d.name"),I)),TEXT("chassis"),TEXT(""),I,Chassis==I); + std::vector Cargo=Run.cargo,Safe=Run.safeCase;if(!Run.active){Cargo.clear();Safe.clear();for(const auto& I:S.draft().items)if(auto Item=gf::find(State.warehouse,I.uid)){auto Copy=*Item;Copy.count=I.count;Copy.x=I.x;Copy.y=I.y;Copy.rotated=I.rotated;(I.hold==gf::Hold::Cargo?Cargo:Safe).push_back(Copy);}} + P.Text(612,245,L(TEXT("ue.cargo"))+TEXT(" 4 × 4"),18,Muted);Grid(612,283,4,4,101,gf::Hold::Cargo,Cargo);P.Text(612,710,L(TEXT("ue.safeCase")),18,Teal);Grid(612,746,2,2,64,gf::Hold::SafeCase,Safe);P.Text(763,753,L(TEXT("hangar.protected")),14,Muted,290);Button(765,807,266,53,L(TEXT("hangar.details")),TEXT("details")); + if(!Field){P.Text(1125,244,FString::Printf(TEXT("10 × 24 / %d %s"),gf::Grid::occupied(State.warehouse),*L(TEXT("ue.cellsUsed"))),15,Muted);Grid(1125,282,10,24,44,gf::Hold::Warehouse,State.warehouse,Scroll,12);Button(1125,837,110,43,TEXT("↑"),TEXT("scroll"),TEXT(""),-4);Button(1245,837,110,43,TEXT("↓"),TEXT("scroll"),TEXT(""),4);Button(1365,837,198,43,L(TEXT("ue.sort")),TEXT("sort"));P.Text(1125,890,FString::Printf(TEXT("%02d–%02d / 24"),Scroll+1,Scroll+12),14,Muted);} + float DX=Field?1125:1590,DW=Field?710:266;float DY=Field?245:245; + if(Field){Button(DX,DY,DW,55,L(TEXT("ue.podSelect")),TEXT("podMode"),TEXT(""),0,bPodSelect);P.Text(DX,DY+77,L(TEXT("ue.podRule")),16,Amber,DW);P.Text(DX,DY+110,L(TEXT("ue.holdPod"))+TEXT(" [")+H->Keys[11]+TEXT("]"),19,Teal,DW);P.Text(DX,DY+160,FString::Printf(TEXT("%s: %d / 4"),*L(TEXT("ue.selected")),PodSelection.Num()),19);DY+=220;} + if(bDetails){const gf::Item* Item=nullptr;if(SelectedHold==gf::Hold::Warehouse)Item=gf::find(State.warehouse,U(Selected));else if(Run.active)Item=gf::find(SelectedHold==gf::Hold::Cargo?Cargo:Safe,U(Selected));else Item=gf::find(State.warehouse,U(Selected)); + if(SelectedSlot>=0){int SlotIndex=SelectedSlot;auto E=S.catalog().part(SlotIndex,Parts[SlotIndex]);P.Text(DX,DY,E?H->ItemName(E->id):L(TEXT("hangar.emptySlot")),20,Teal,DW);if(Field)Button(DX,DY+55,DW,48,L(TEXT("ue.uninstall")),TEXT("uninstall"),TEXT(""),SlotIndex);else if(!Run.active)Button(DX,DY+55,DW,48,L(TEXT("ue.borrow")),TEXT("borrow"),TEXT(""),SlotIndex);} + else if(Item){P.Text(DX,DY,H->ItemName(Item->itemId),20,Teal,DW);P.Text(DX,DY+35,TEXT("UID ")+W(Item->uid).Left(10),12,Muted,DW);P.Text(DX,DY+64,FString::Printf(TEXT("%s %d / %d"),*L(TEXT("warehouse.quantity")),Quantity,Item->count),17);Button(DX,DY+100,55,40,TEXT("−"),TEXT("quantity"),TEXT(""),-1);Button(DX+65,DY+100,55,40,TEXT("+"),TEXT("quantity"),TEXT(""),1);float AY=DY+155; + if(!Run.active){if(gf::Grid::slot(Item->itemId)>=0){Button(DX,AY,DW,44,L(TEXT("ue.equip")),TEXT("equip"));AY+=54;}Button(DX,AY,DW,44,L(TEXT("ue.toCargo")),TEXT("prepare"),TEXT(""),0);AY+=54;Button(DX,AY,DW,44,L(TEXT("ue.toSafe")),TEXT("prepare"),TEXT(""),1);AY+=54;Button(DX,AY,DW,44,L(TEXT("ue.unprepare")),TEXT("unprepare"));AY+=54;} + else{if(gf::Grid::slot(Item->itemId)>=0){Button(DX,AY,DW,44,L(TEXT("ue.install")),TEXT("install"));AY+=54;}Button(DX,AY,DW,44,L(TEXT("ue.transfer")),TEXT("transfer"));AY+=54;} + if(!Run.active&&gf::Grid::blueprint(Item->itemId)){Button(DX,AY,DW,44,L(TEXT("ue.research")),TEXT("research"),W(Item->itemId));AY+=54;Button(DX,AY,DW,44,L(TEXT("ue.recycle")),TEXT("recycle"));} + P.Text(DX,880,L(TEXT("ue.dragHint")),13,Muted,DW); + }else{P.Text(DX,DY,L(TEXT("ue.selectItem")),18,Muted,DW);P.Text(DX,DY+45,L(TEXT("ue.dragHint")),14,Muted,DW);} + } + if(!Field&&State.recoveryInbox.size()>0){Button(1125,938,438,50,L(TEXT("ue.pending"))+TEXT(" ")+FString::FromInt(State.recoveryInbox.size()),TEXT("page"),TEXT("inbox"),0,true);} + Button(40,950,220,55,L(Field?TEXT("ue.resume"):TEXT("ue.back")),TEXT("page"),Field?TEXT("play"):TEXT("home"));if(!Field)Button(765,950,315,55,L(Run.active?TEXT("ue.resume"):TEXT("ue.deploy")),TEXT("launch"),TEXT(""),0,true); + }else if(Page==TEXT("research")){ + Section(90,160,1740,720,L(TEXT("ue.research")),L(TEXT("ue.researchHint")));int I=0;for(const auto* Id:{"blueprint_coil_capacitor","blueprint_boost_efficient","blueprint_reactive_guard"}){auto R=gf::Grid::recipe(Id);bool Unlocked=std::find(State.unlocked.begin(),State.unlocked.end(),std::string(Id))!=State.unlocked.end();float X=130+I*565;P.Rect(X,300,525,470,FLinearColor(.04f,.08f,.10f));P.Frame(X,300,525,470);P.Text(X+20,330,H->ItemName(Id),25,Teal,485);if(auto T=Icons.Find(W(R.item));T&&*T)P.Image(X+180,390,160,180,*T);P.Text(X+20,600,FString::Printf(TEXT("%s %d / %s %d"),*H->ItemName("alloy"),R.alloy,*H->ItemName("cells"),R.cells),18);Button(X+20,660,485,60,L(Unlocked?TEXT("ue.manufacture"):TEXT("ue.research")),Unlocked?TEXT("manufacture"):TEXT("research"),W(Id),0,Unlocked);I++;}P.Text(130,810,FString::Printf(TEXT("%s: %d / %s: %d"),*H->ItemName("alloy"),S.available("alloy"),*H->ItemName("cells"),S.available("cells")),21,Muted);Button(90,925,250,60,L(TEXT("ue.back")),TEXT("page"),TEXT("home")); + }else if(Page==TEXT("inbox")){ + Section(90,160,1740,720,L(TEXT("ue.pending")),L(TEXT("ue.inboxHint")));int I=0;for(const auto& Item:State.recoveryInbox){if(I>=10)break;float Y=285+I*50;P.Text(130,Y,H->ItemName(Item.itemId)+TEXT(" × ")+FString::FromInt(Item.count),20);Button(1050,Y,250,42,L(TEXT("ue.claim")),TEXT("claim"),W(Item.uid));Button(1330,Y,250,42,L(TEXT("ue.discard")),TEXT("discardInbox"),W(Item.uid));I++;}Button(130,810,400,48,L(TEXT("ue.claimAll")),TEXT("claim"));Button(90,925,250,60,L(TEXT("ue.back")),TEXT("page"),TEXT("hangar")); + }else if(Page==TEXT("settings")){ + Section(80,150,1760,760,L(TEXT("ue.settings")),L(TEXT("ue.settingsHint")));static const TCHAR* Corners[]={TEXT("ue.perfTopRight"),TEXT("ue.perfTopLeft"),TEXT("ue.perfBottomRight"),TEXT("ue.perfBottomLeft")};Button(120,265,390,48,L(TEXT("ue.performance"))+TEXT(" ")+L(H->bShowPerformance?TEXT("ue.perfOn"):TEXT("ue.perfOff")),TEXT("performance"),TEXT(""),0,H->bShowPerformance);Button(540,265,460,48,L(TEXT("ue.perfPosition"))+TEXT(" ")+L(Corners[FMath::Clamp(H->PerformanceCorner,0,3)]),TEXT("performanceCorner"));P.Text(1030,279,L(TEXT("ue.perfHint")),14,Muted,740);P.Text(120,333,L(TEXT("ue.display")),23,Teal);int I=0;for(auto Resolution:{FIntPoint(1280,720),FIntPoint(1920,1080),FIntPoint(2560,1440),FIntPoint(3440,1440)}){Button(120,375+I*64,350,50,FString::Printf(TEXT("%d × %d"),Resolution.X,Resolution.Y),TEXT("resolution"),FString::Printf(TEXT("%dx%d"),Resolution.X,Resolution.Y));I++;} + Button(120,649,350,50,L(TEXT("ue.fullscreen")),TEXT("fullscreen"));P.Text(120,718,FString::Printf(TEXT("%s %.1f"),*L(TEXT("ue.sensitivity")),H->Sensitivity),18,Ink,235);Button(365,708,45,43,TEXT("−"),TEXT("sensitivity"),TEXT(""),-1);Button(425,708,45,43,TEXT("+"),TEXT("sensitivity"),TEXT(""),1);P.Text(120,770,FString::Printf(TEXT("%s %.1f"),*L(TEXT("ue.aimSensitivity")),H->AimSensitivity),18,Ink,235);Button(365,760,45,43,TEXT("−"),TEXT("aimSensitivity"),TEXT(""),-1);Button(425,760,45,43,TEXT("+"),TEXT("aimSensitivity"),TEXT(""),1);Button(120,825,350,48,L(TEXT("ue.invertY")),TEXT("invert"),TEXT(""),0,H->bInvertY); + P.Text(560,333,L(TEXT("ue.controls")),23,Teal);static const TCHAR* Names[]={TEXT("ue.forward"),TEXT("ue.backward"),TEXT("ue.left"),TEXT("ue.right"),TEXT("ue.jump"),TEXT("ue.boost"),TEXT("ue.guard"),TEXT("ue.reload"),TEXT("ue.view"),TEXT("ue.interact"),TEXT("ue.inventory"),TEXT("ue.pod"),TEXT("ue.secondary"),TEXT("ue.fire"),TEXT("ue.aim")};for(int J=0;JKeys.Num();J++){float X=560+(J/8)*600,Y=375+(J%8)*64;P.Text(X,Y+10,L(Names[J]),18,Ink,225);Button(X+240,Y,300,48,C->Rebind==J?L(TEXT("ue.pressKey")):H->Keys[J],TEXT("rebind"),TEXT(""),J,C->Rebind==J);} + Button(80,945,270,58,L(TEXT("ue.back")),TEXT("page"),C->Mech()?TEXT("pause"):TEXT("home"));Button(1200,945,250,58,FString::Printf(TEXT("%s %d%%"),*L(TEXT("ue.volume")),FMath::RoundToInt(H->MasterVolume*100)),TEXT("volume"));Button(1480,945,360,58,L(TEXT("ue.quality")),TEXT("quality")); + }else if(Page==TEXT("pause")||Page==TEXT("upgrade")){ + Section(580,220,760,630,L(Page==TEXT("pause")?TEXT("ue.paused"):TEXT("ue.upgrade")),L(TEXT("ue.pauseHint"))); + if(Page==TEXT("pause")){Button(625,355,670,65,L(TEXT("ue.resume")),TEXT("page"),TEXT("play"),0,true);Button(625,440,670,65,L(TEXT("ue.inventory")),TEXT("page"),TEXT("inventory"));Button(625,525,670,65,L(TEXT("ue.settings")),TEXT("page"),TEXT("settings"));Button(625,610,670,65,L(TEXT("ue.saveAndHome")),TEXT("home"));} + else{for(int I=0;I<3;I++)Button(625,345+I*98,670,70,L(I==0?TEXT("ue.upgradeDamage"):I==1?TEXT("ue.upgradeRepair"):TEXT("ue.upgradeEnergy")),TEXT("upgrade"),TEXT(""),I);Button(625,690,670,55,L(TEXT("ue.back")),TEXT("page"),TEXT("play"));} + }else if(Play){ + P.Rect(35,32,515,145,Panel);P.Rect(35,32,4,145,Teal);P.Text(60,50,H->Label(FString::Printf(TEXT("chassis.chassis_%d.name"),Run.chassis)),23,Teal);P.Text(60,90,FString::Printf(TEXT("%s %.0f / %s %.0f"),*L(TEXT("ue.armor")),Run.armor,*L(TEXT("ue.energy")),Run.energy),22);P.Rect(60,140,460,8,Line);P.Rect(60,140,460*FMath::Clamp(Run.armor/S.catalog().chassis[Run.chassis].armor,0.f,1.f),8,Teal); + P.Rect(1390,32,495,145,Panel);P.Text(1415,48,FString::Printf(TEXT("%s %d / 5 · %s %d"),*L(TEXT("ue.stage")),Run.stage+1,*L(TEXT("ue.salvage")),static_cast(Run.cargo.size())),20,Teal);P.Text(1415,90,L(Run.routeCompleted?TEXT("ue.routeDone"):Run.objectiveDone?TEXT("ue.objectiveDone"):TEXT("ue.findObjective")),19,Ink,445);P.Text(1415,133,L(Run.podUsed?TEXT("ue.podUsed"):TEXT("ue.podAvailable")),15,Run.podUsed?Muted:Amber); + P.Rect(35,932,480,105,Panel);P.Text(60,951,FString::Printf(TEXT("%02d / %s"),H->Combat.rounds,*L(H->Combat.reload>0?TEXT("ue.reloading"):TEXT("ue.primary"))),27,Teal);P.Text(60,997,H->Keys[8]+TEXT(" ")+L(TEXT("ue.view"))+TEXT(" · ")+H->Keys[10]+TEXT(" ")+L(TEXT("ue.inventory")),14,Muted); + const FVector2D Aim=(G.GetLocalSize()*.5f-P.Origin)/P.Scale;P.Rect(Aim.X-2,Aim.Y-10,4,20,Teal);P.Rect(Aim.X-10,Aim.Y-2,20,4,Teal);if(auto M=GetWorld()->GetAuthGameMode()){P.Rect(550,955,800,65,Panel);P.Text(580,972,M->Prompt(),19,Ink,740);if(M->ExtractionWait>=0&&!M->bExtractionReady)P.Text(740,900,L(TEXT("ue.wait"))+FString::Printf(TEXT(" %.0fs"),M->ExtractionWait),25,Amber);if(M->PodProgress>0){P.Rect(650,850,620,9,Line);P.Rect(650,850,620*M->PodProgress/3,9,Amber);P.Text(650,810,L(TEXT("ue.podHolding")),20,Amber);}if(M->Escort)P.Text(650,210,FString::Printf(TEXT("%s %.0f / %.0f%%"),*L(TEXT("ue.escort")),Run.escortArmor,M->EscortProgress/45*100),19,Teal);} + if(H->IsTraining())P.Text(650,185,L(TEXT("ue.rangeStats"))+FString::Printf(TEXT(" %d / %d / %.1fs"),S.range().hits,S.range().gates,S.range().seconds),18,Teal,670); + int VX=0,VY=0;C->GetViewportSize(VX,VY);if(VX>0&&VY>0&&C->Mech())for(TActorIterator It(GetWorld());It;++It){bool Course=It->Kind.StartsWith(TEXT("rangeGate"));if(It->IsHidden()&&!Course)continue;float Distance=FVector::Dist(It->GetActorLocation(),C->Mech()->GetActorLocation())*.01f;if(It->Kind==TEXT("loot")&&Distance>4.5f)continue;if(Course&&FCString::Atoi(*It->Kind.Right(1))!=S.range().gates)continue;if(It->Kind.StartsWith(TEXT("gate"))&&!Run.objectiveDone)continue;if(It->Kind==TEXT("objective")&&(Run.objectiveDone||(!H->IsTraining()&&Run.stage==4)))continue;if((It->Kind==TEXT("upgrade")||It->Kind==TEXT("rangeReset"))&&Distance>5)continue;FVector2D Screen;if(!C->ProjectWorldLocationToScreen(It->GetActorLocation()+FVector(0,0,180),Screen,true))continue; + FVector2D Point=(FVector2D(Screen.X*G.GetLocalSize().X/VX,Screen.Y*G.GetLocalSize().Y/VY)-P.Origin)/P.Scale;if(Point.X<20||Point.X>1900||Point.Y<190||Point.Y>890)continue; + FString Key=Course?TEXT("ue.courseGate"):It->Kind==TEXT("extract")?TEXT("ue.extraction"):It->Kind==TEXT("loot")?TEXT("ue.search"):It->Kind.StartsWith(TEXT("gate"))?(Run.stage==1?(It->Kind.EndsWith(TEXT("1"))?TEXT("ue.routeEscort"):TEXT("ue.routeBoss")):TEXT("ue.nextStage")):It->Kind==TEXT("objective")?TEXT("ue.activate"):It->Kind==TEXT("rangeReset")?TEXT("ue.rangeResetLabel"):TEXT("ue.upgrade"); + P.Text(Point.X-90,Point.Y,H->Label(Key)+FString::Printf(TEXT(" %.0fm"),Distance),14,It->Kind==TEXT("loot")?Ink:Amber,210); + } + if(auto Mech=C->Mech();Mech&&GetWorld()->GetTimeSeconds()-Mech->DamageTime<.2f)P.Frame(0,0,1920,1080,FLinearColor(1,.15f,.1f)); + } + if(!Play){P.Rect(0,1030,1920,50,FLinearColor(.015f,.026f,.035f,.97f));P.Text(42,1043,L(TEXT("ue.footer")),14,Muted);Button(1660,1035,220,39,TEXT("中文 / EN"),TEXT("language"));} + if(!H->bReady||(!H->StatusKey.IsEmpty()&&FPlatformTime::Seconds()StatusUntil)){P.Rect(540,114,840,48,FLinearColor(.025f,.09f,.11f,.97f));FString Detail=H->StatusKey==TEXT("ue.loot")?H->ItemName(U(H->StatusDetail)):H->StatusDetail;P.Text(560,126,H->Label(H->StatusKey)+(Detail.IsEmpty()?TEXT(""):TEXT(" · ")+Detail),16,H->bReady?Teal:Amber,800);} + if(bDragging){const gf::Item* Item=gf::find(State.warehouse,U(DragUid));if(!Item)Item=gf::find(DragHold==gf::Hold::Cargo?Run.cargo:Run.safeCase,U(DragUid));if(Item){auto Size=gf::Grid::size(Item->itemId,bRotated);P.Rect(Pointer.X-Size.first*28,Pointer.Y-Size.second*28,Size.first*56,Size.second*56,FLinearColor(.16f,.5f,.5f,.7f));if(const auto* T=Icons.Find(W(Item->itemId));T&&*T)P.Image(Pointer.X-Size.first*28,Pointer.Y-Size.second*28,Size.first*56,Size.second*56,*T);}} + return P.Layer; +} + +namespace { +FVector2D UiPoint(const FGeometry& G,FVector2D Absolute,const UIronsGameInstance* H){auto Layout=FIronsPerformanceLayout::Make(G.GetLocalSize(),H&&H->bShowPerformance,H?H->PerformanceCorner:0);return (G.AbsoluteToLocal(Absolute)-Layout.ContentOrigin)/Layout.ContentScale;} +} +void UIronsScreen::Dispatch(const FIronsHitRegion& Hit){ + auto H=Host();auto C=Controller();if(!H||!C)return;auto& S=H->Core();auto Cmd=Hit.Command;auto Uid=U(Selected); + if(Cmd==TEXT("page")){C->SetPage(Hit.Argument);return;} + if(Cmd==TEXT("language")){H->SetLanguage(!H->bEnglish);return;} + if(Cmd==TEXT("performance")){H->bShowPerformance=!H->bShowPerformance;H->SavePreferences();return;} + if(Cmd==TEXT("performanceCorner")){H->PerformanceCorner=(H->PerformanceCorner+1)%4;H->SavePreferences();return;} + if(Cmd==TEXT("launch")){C->Launch();return;} + if(Cmd==TEXT("training")){C->Launch(true);return;} + if(Cmd==TEXT("quit")){UKismetSystemLibrary::QuitGame(this,C,EQuitPreference::Quit,false);return;} + if(Cmd==TEXT("home")){C->ReturnHome();return;} + if(Cmd==TEXT("scroll")){Scroll=FMath::Clamp(Scroll+Hit.A,0,12);return;} + if(Cmd==TEXT("details")){bDetails=!bDetails;return;} + if(Cmd==TEXT("podMode")){bPodSelect=!bPodSelect;return;} + if(Cmd==TEXT("quantity")){const gf::Item* I=gf::find(S.state().warehouse,Uid);if(!I)I=gf::find(SelectedHold==gf::Hold::Cargo?S.state().run.cargo:S.state().run.safeCase,Uid);Quantity=FMath::Clamp(Quantity+Hit.A,1,I?I->count:1);return;} + if(Cmd==TEXT("slot")){Selected.Empty();SelectedSlot=Hit.A;return;} + if(Cmd==TEXT("item")){Selected=Hit.Argument;SelectedSlot=-1;SelectedHold=static_cast(Hit.A);const gf::Item* I=gf::find(S.state().warehouse,U(Selected));if(!I)I=gf::find(SelectedHold==gf::Hold::Cargo?S.state().run.cargo:S.state().run.safeCase,U(Selected));Quantity=I?I->count:1;bRotated=I&&I->rotated; + if(bPodSelect&&SelectedHold==gf::Hold::Cargo){if(PodSelection.Contains(Selected))PodSelection.Remove(Selected);else if(PodSelection.Num()<4)PodSelection.Add(Selected);}return;} + if(Cmd==TEXT("settings")){C->SetPage(TEXT("settings"));return;} + if(Cmd==TEXT("rebind")){C->Rebind=Hit.A;return;} + if(Cmd==TEXT("sensitivity")){H->Sensitivity=FMath::Clamp(H->Sensitivity+.1f*Hit.A,.1f,5.f);H->SavePreferences();return;} + if(Cmd==TEXT("aimSensitivity")){H->AimSensitivity=FMath::Clamp(H->AimSensitivity+.1f*Hit.A,.1f,2.f);H->SavePreferences();return;} + if(Cmd==TEXT("invert")){H->bInvertY=!H->bInvertY;H->SavePreferences();return;} + if(Cmd==TEXT("volume")){H->MasterVolume+=.1f;if(H->MasterVolume>1.01f)H->MasterVolume=0;H->SavePreferences();return;} + auto Settings=UGameUserSettings::GetGameUserSettings(); + if(Cmd==TEXT("resolution")){FString X,Y;Hit.Argument.Split(TEXT("x"),&X,&Y);Settings->SetScreenResolution(FIntPoint(FCString::Atoi(*X),FCString::Atoi(*Y)));Settings->SetFullscreenMode(EWindowMode::Windowed);Settings->ApplySettings(false);return;} + if(Cmd==TEXT("fullscreen")){Settings->SetFullscreenMode(Settings->GetFullscreenMode()==EWindowMode::Windowed?EWindowMode::WindowedFullscreen:EWindowMode::Windowed);Settings->ApplySettings(false);return;} + if(Cmd==TEXT("quality")){int Quality=(Settings->GetOverallScalabilityLevel()+1)%4;Settings->SetOverallScalabilityLevel(Quality);Settings->ApplySettings(false);H->Notice(TEXT("ue.quality"),FString::FromInt(Quality));return;} + if(!H->bReady)return; + if(C->Page==TEXT("hangar")&&S.state().run.active){H->Notice(TEXT("ue.locked"));return;} + gf::Result Result=gf::Result::fail(gf::Code::Invalid); + if(Cmd==TEXT("chassis"))Result=S.selectChassis(Hit.A); + else if(Cmd==TEXT("borrow"))Result=S.borrow(Hit.A); + else if(Cmd==TEXT("equip"))Result=S.equip(Uid); + else if(Cmd==TEXT("install"))Result=S.install(Uid,SelectedHold); + else if(Cmd==TEXT("uninstall")){ + auto SlotIndex=Hit.A;auto Part=S.catalog().part(SlotIndex,S.state().run.equipment[SlotIndex]);if(Part){auto Item=gf::Item{};Item.itemId=Part->id;auto Grid=S.state().run.cargo;bool Found=false;for(int Y=0;Y<4&&!Found;Y++)for(int X=0;X<4&&!Found;X++)if(gf::Grid::fits(Grid,4,4,Item,X,Y,false)){Result=S.uninstall(SlotIndex,gf::Hold::Cargo,X,Y);Found=true;}if(!Found)Result=gf::Result::fail(gf::Code::NoSpace);} + } + else if(Cmd==TEXT("prepare"))Result=S.prepare(Uid,Hit.A?gf::Hold::SafeCase:gf::Hold::Cargo,bRotated,-1,-1,Quantity); + else if(Cmd==TEXT("unprepare"))Result=S.unprepare(Uid); + else if(Cmd==TEXT("transfer"))Result=S.moveItem(Uid,SelectedHold,SelectedHold==gf::Hold::Cargo?gf::Hold::SafeCase:gf::Hold::Cargo,-1,-1,bRotated,Quantity); + else if(Cmd==TEXT("sort"))Result=S.sortWarehouse(); + else if(Cmd==TEXT("claim"))Result=S.claim(U(Hit.Argument)); + else if(Cmd==TEXT("discardInbox")){if(Selected==Hit.Argument){Result=S.discard(U(Hit.Argument),true);Selected.Empty();}else{Selected=Hit.Argument;H->Notice(TEXT("ue.confirmDiscard"));return;}} + else if(Cmd==TEXT("research"))Result=S.research(U(Hit.Argument)); + else if(Cmd==TEXT("manufacture"))Result=S.manufacture(U(Hit.Argument)); + else if(Cmd==TEXT("recycle"))Result=S.recycle(Uid); + else if(Cmd==TEXT("upgrade")){Result=S.upgrade(Hit.A);if(Result)C->SetPage(TEXT("play"));} + H->Accept(Result); +} +FReply UIronsScreen::NativeOnMouseButtonDown(const FGeometry& G,const FPointerEvent& E){ + Pointer=UiPoint(G,E.GetScreenSpacePosition(),Host());if(Controller()->Rebind>=0){Controller()->ProcessKey(E.GetEffectingButton(),true);return FReply::Handled();}if(E.GetEffectingButton()!=EKeys::LeftMouseButton)return FReply::Handled(); + for(int I=Hits.Num()-1;I>=0;I--)if(Hits[I].Rect.ContainsPoint(Pointer)){auto Hit=Hits[I];if(Hit.Command==TEXT("item")){Dispatch(Hit);DragUid=Hit.Argument;DragHold=static_cast(Hit.A);DragStart=Pointer;bDragging=false;}else Dispatch(Hit);return FReply::Handled().SetUserFocus(TakeWidget(),EFocusCause::Mouse).CaptureMouse(TakeWidget());} + return FReply::Handled().SetUserFocus(TakeWidget(),EFocusCause::Mouse); +} +FReply UIronsScreen::NativeOnMouseMove(const FGeometry& G,const FPointerEvent& E){Pointer=UiPoint(G,E.GetScreenSpacePosition(),Host());if(!DragUid.IsEmpty()&&(Pointer-DragStart).Size()>8&&!bPodSelect)bDragging=true;return FReply::Handled();} +FReply UIronsScreen::NativeOnMouseButtonUp(const FGeometry& G,const FPointerEvent& E){ + Pointer=UiPoint(G,E.GetScreenSpacePosition(),Host());auto H=Host();auto C=Controller();if(H&&C&&bDragging&&!DragUid.IsEmpty()){ + auto& S=H->Core();gf::Result R=gf::Result::fail(gf::Code::Invalid);bool Applied=false; + if(C->Page==TEXT("hangar")&&S.state().run.active){H->Notice(TEXT("ue.locked"));} + else{ + for(const auto& Hit:Hits)if(Hit.Command==TEXT("slot")&&Hit.Rect.ContainsPoint(Pointer)){R=S.state().run.active?S.install(U(DragUid),DragHold):S.equip(U(DragUid));Applied=true;break;} + for(const auto& Grid:Grids)if(!Applied&&Grid.Rect.ContainsPoint(Pointer)){ + int X=FMath::FloorToInt((Pointer.X-Grid.Rect.Left)/Grid.Cell),Y=FMath::FloorToInt((Pointer.Y-Grid.Rect.Top)/Grid.Cell)+(Grid.Hold==gf::Hold::Warehouse?Scroll:0); + const gf::Item* Target=nullptr;const auto& Items=Grid.Hold==gf::Hold::Warehouse?S.state().warehouse:Grid.Hold==gf::Hold::Cargo?S.state().run.cargo:S.state().run.safeCase; + for(const auto& I:Items){auto Size=gf::Grid::size(I.itemId,I.rotated);if(I.uid!=U(DragUid)&&X>=I.x&&X=I.y&&Yuid,Quantity); + else if(Target&&S.state().run.active&&Grid.Hold!=gf::Hold::Warehouse){const auto* Source=gf::find(DragHold==gf::Hold::Cargo?S.state().run.cargo:S.state().run.safeCase,U(DragUid));R=Source&&Source->itemId==Target->itemId&&gf::Grid::material(Source->itemId)?S.mergeItem(U(DragUid),DragHold,Target->uid,Grid.Hold,Quantity):S.swapItems(U(DragUid),DragHold,Target->uid,Grid.Hold,bRotated);} + else if(Grid.Hold==gf::Hold::Warehouse)R=DragHold==gf::Hold::Warehouse?S.moveWarehouse(U(DragUid),X,Y,bRotated,Quantity):S.unprepare(U(DragUid)); + else if(!S.state().run.active)R=S.prepare(U(DragUid),Grid.Hold,bRotated,X,Y,Quantity); + else R=S.moveItem(U(DragUid),DragHold,Grid.Hold,X,Y,bRotated,Quantity); + Applied=true; + } + if(Applied)H->Accept(R); + } + } + bDragging=false;DragUid.Empty();return FReply::Handled().ReleaseMouseCapture(); +} +FReply UIronsScreen::NativeOnMouseWheel(const FGeometry& G,const FPointerEvent& E){Pointer=UiPoint(G,E.GetScreenSpacePosition(),Host());if(Pointer.X>1100&&Controller()->Page==TEXT("hangar"))Scroll=FMath::Clamp(Scroll-FMath::RoundToInt(E.GetWheelDelta()*3),0,12);return FReply::Handled();} +FReply UIronsScreen::NativeOnKeyDown(const FGeometry& G,const FKeyEvent& E){ + auto C=Controller();if(!C)return FReply::Unhandled();if(C->Rebind>=0){C->ProcessKey(E.GetKey(),true);return FReply::Handled();} + if(bDragging&&E.GetKey()==EKeys::R){bRotated=!bRotated;return FReply::Handled();} + if(E.GetKey()==EKeys::Escape){if(bDragging){bDragging=false;DragUid.Empty();}else C->Menu();return FReply::Handled();} + if(C->Page==TEXT("inventory")&&E.GetKey()==FKey(*Host()->Keys[10])){C->SetPage(TEXT("play"));return FReply::Handled();} + return Super::NativeOnKeyDown(G,E); +} diff --git a/Source/ueRouge/Private/IronsWorld.cpp b/Source/ueRouge/Private/IronsWorld.cpp new file mode 100644 index 0000000..1059671 --- /dev/null +++ b/Source/ueRouge/Private/IronsWorld.cpp @@ -0,0 +1,100 @@ +#include "IronsGame.h" +#include "Components/StaticMeshComponent.h" +#include "Components/DirectionalLightComponent.h" +#include "Components/PointLightComponent.h" +#include "Components/SkyLightComponent.h" +#include "Components/ExponentialHeightFogComponent.h" +#include "Engine/DirectionalLight.h" +#include "Engine/PointLight.h" +#include "Engine/SkyLight.h" +#include "Engine/ExponentialHeightFog.h" +#include "Components/SkyAtmosphereComponent.h" +#include "Engine/StaticMeshActor.h" +#include "Engine/World.h" +#include "EngineUtils.h" +#include "Camera/CameraActor.h" +#include "Kismet/GameplayStatics.h" +#include "GameFramework/CharacterMovementComponent.h" +#include "Materials/MaterialInstanceDynamic.h" +#include "Materials/Material.h" +#include "DrawDebugHelpers.h" + +namespace { +UStaticMeshComponent* Block(UWorld* W,FVector P,FVector Size,FLinearColor Color,bool Collision=true){ + auto A=W->SpawnActor(P,FRotator::ZeroRotator);auto C=A->GetStaticMeshComponent();C->SetMobility(EComponentMobility::Movable);C->SetStaticMesh(LoadObject(nullptr,TEXT("/Engine/BasicShapes/Cube.Cube")));C->SetWorldScale3D(Size/100);C->SetCollisionEnabled(Collision?ECollisionEnabled::QueryAndPhysics:ECollisionEnabled::NoCollision); + auto M=LoadObject(nullptr,TEXT("/Game/IRONSPORE/Art/Materials/M_Industrial.M_Industrial"));if(!M)M=LoadObject(nullptr,TEXT("/Engine/BasicShapes/BasicShapeMaterial.BasicShapeMaterial"));auto D=UMaterialInstanceDynamic::Create(M,C);D->SetVectorParameterValue(TEXT("BaseColor"),Color);D->SetVectorParameterValue(TEXT("Color"),Color);C->SetMaterial(0,D);return C; +} +void Light(UWorld* W,FVector P,FLinearColor Color,float Power,float Radius){auto L=W->SpawnActor(P,FRotator::ZeroRotator);L->SetMobility(EComponentMobility::Movable);auto C=L->PointLightComponent;C->SetLightColor(Color);C->SetIntensity(Power);C->SetAttenuationRadius(Radius);} +AIronsMarker* Marker(UWorld* W,FVector P,const FString& Kind,const FString& Id=TEXT(""),int Count=1,const FString& Source=TEXT("")){ + auto M=W->SpawnActor(P,FRotator::ZeroRotator);M->Kind=Kind;M->ItemId=Id;M->Quantity=Count;M->SourceKey=Source; + auto Material=LoadObject(nullptr,TEXT("/Game/IRONSPORE/Art/Materials/M_Industrial.M_Industrial"));if(Material){auto D=UMaterialInstanceDynamic::Create(Material,M);D->SetVectorParameterValue(TEXT("BaseColor"),Kind==TEXT("extract")?FLinearColor(.04f,.6f,.3f):Kind==TEXT("loot")?FLinearColor(.25f,.34f,.37f):FLinearColor(.8f,.4f,.05f));M->Body->SetMaterial(0,D);}return M; +} +} +AIronsMarker::AIronsMarker(){Body=CreateDefaultSubobject(TEXT("MarkerBody"));SetRootComponent(Body);Body->SetStaticMesh(LoadObject(nullptr,TEXT("/Engine/BasicShapes/Cube.Cube")));Body->SetCollisionProfileName(TEXT("BlockAllDynamic"));SetActorScale3D(FVector(1.1f,1.1f,.75f));} +AIronsGameMode::AIronsGameMode(){PrimaryActorTick.bCanEverTick=true;DefaultPawnClass=nullptr;PlayerControllerClass=AIronsController::StaticClass();} +void AIronsGameMode::BeginPlay(){Super::BeginPlay();auto H=Cast(GetGameInstance()); + if(GetWorld()->GetMapName().Contains(TEXT("L_Hangar")))BuildHangar();else{BuildBattle();if(H&&H->bReady){auto PC=Cast(UGameplayStatics::GetPlayerController(this,0));if(PC){FActorSpawnParameters Params;auto Mech=GetWorld()->SpawnActor(FVector(-2300,-2300,175),FRotator(0,45,0),Params);PC->Possess(Mech);PC->SetPage(TEXT("play"));}}} +} +void AIronsGameMode::BuildHangar(){ + auto W=GetWorld();Block(W,FVector(0,0,-35),FVector(4500,4000,70),FLinearColor(.08f,.10f,.12f)); + auto A=W->SpawnActor(FVector(0,0,0),FRotator::ZeroRotator);auto C=A->GetStaticMeshComponent();C->SetMobility(EComponentMobility::Movable);C->SetStaticMesh(LoadObject(nullptr,TEXT("/Game/IRONSPORE/Art/Environment/SM_HangarBay.SM_HangarBay"))); + auto Mech=W->SpawnActor(FVector(200,150,60),FRotator(0,-130,0));auto B=Mech->GetStaticMeshComponent();B->SetMobility(EComponentMobility::Movable);B->SetStaticMesh(LoadObject(nullptr,TEXT("/Game/IRONSPORE/Art/Mechs/SM_MechScout.SM_MechScout")));B->SetWorldScale3D(FVector(2.0f)); + Block(W,FVector(200,150,10),FVector(800,800,35),FLinearColor(.12f,.16f,.18f)); + Light(W,FVector(300,-500,700),FLinearColor(.5f,.8f,1),120000,1700);Light(W,FVector(-600,200,400),FLinearColor(1,.45f,.16f),80000,1400);Light(W,FVector(0,0,850),FLinearColor(.8f,.88f,1),100000,1800); + for(int N=0;N<6;N++){Block(W,FVector(-1000+N*400,450,380),FVector(15,20,700),FLinearColor(.12f,.16f,.19f));Light(W,FVector(-1000+N*400,450,730),FLinearColor(.2f,.8f,1),3000,400);} + auto Cam=W->SpawnActor(FVector(780,-660,480),FRotator::ZeroRotator);Cam->SetActorRotation((FVector(200,150,340)-Cam->GetActorLocation()).Rotation());auto PC=UGameplayStatics::GetPlayerController(this,0);if(PC)PC->SetViewTarget(Cam); +} +void AIronsGameMode::BuildBattle(){ + auto H=Cast(GetGameInstance());if(!H||!H->bReady)return;auto W=GetWorld();auto& S=H->Core();S.clearEnemies();S.resetField();auto R=S.state().run; + int Biome=R.biome;FLinearColor Ground=Biome==0?FLinearColor(.16f,.21f,.22f):Biome==1?FLinearColor(.25f,.20f,.12f):FLinearColor(.12f,.16f,.22f); + Block(W,FVector(0,0,-50),FVector(13000,13000,100),Ground); + auto Sun=W->SpawnActor(FVector(0,0,3000),FRotator(-45,-35,0));Sun->SetMobility(EComponentMobility::Movable);Sun->GetLightComponent()->SetIntensity(3.5f); + W->SpawnActor(); + auto Sky=W->SpawnActor();Sky->GetLightComponent()->SetMobility(EComponentMobility::Movable);Sky->GetLightComponent()->SetIntensity(1.2f);Sky->GetLightComponent()->bRealTimeCapture=true; + auto Fog=W->SpawnActor();Fog->GetComponent()->SetFogDensity(.007f);Fog->GetComponent()->SetFogInscatteringColor(Biome==1?FLinearColor(.35f,.27f,.18f):FLinearColor(.15f,.23f,.3f)); + FRandomStream Rand(R.seed+R.stage*871);for(int I=0;I<30;I++){FVector P(Rand.FRandRange(-5300,5300),Rand.FRandRange(-5300,5300),0);if(P.Size2D()<1000||FVector::Dist2D(P,FVector(-2300,-2300,0))<800)continue;float Height=Rand.FRandRange(180,600);P.Z=Height/2;Block(W,P,FVector(Rand.FRandRange(250,600),Rand.FRandRange(200,500),Height),Ground*.7f);} + for(int I=0;I<4;I++){FVector P(I%2?6200:-6200,I<2?0:0,350);if(I>1)P=FVector(0,I==2?6200:-6200,350);Block(W,P,I<2?FVector(100,12400,700):FVector(12400,100,700),Ground*.5f);} + Extraction=Marker(W,FVector(-2700,-2300,60),TEXT("extract"));Extraction->SetActorScale3D(FVector(4,4,.2f)); + Light(W,Extraction->GetActorLocation()+FVector(0,0,250),FLinearColor(.2f,1,.8f),50000,1100); + static const TCHAR* LootIds[]={TEXT("alloy"),TEXT("cells"),TEXT("coil"),TEXT("vector"),TEXT("blueprint_coil_capacitor"),TEXT("blueprint_boost_efficient"),TEXT("pulse"),TEXT("reactive_guard"),TEXT("blueprint_reactive_guard"),TEXT("siege")}; + for(int I=0;I<10;I++){FVector P(-1700+(I%5)*800,-1300+(I/5)*1800,70);auto M=Marker(W,P,TEXT("loot"),LootIds[I],I<2?5:1,FString::Printf(TEXT("stage%d-crate%d"),R.stage,I));if(std::find(R.searched.begin(),R.searched.end(),std::string(TCHAR_TO_UTF8(*M->SourceKey)))!=R.searched.end()){M->SetActorHiddenInGame(true);M->SetActorEnableCollision(false);}} + Marker(W,FVector(1800,1700,100),TEXT("objective"));Marker(W,FVector(2700,2500,100),TEXT("gate0"));Marker(W,FVector(3500,2500,100),TEXT("gate1")); + Marker(W,FVector(700,200,70),TEXT("upgrade")); + int Count=H->IsTraining()?2:6+R.stage*2;for(int I=0;IIsTraining()?FVector(2800+I*1200,3800+I*1000,0):FVector(-800+(I%4)*1100,900+(I/4)*1100,0);int T=I%3;if(T==1)P.Z=350;FTransform Transform(FRotator(0,-120,0),P);auto E=W->SpawnActorDeferred(AIronsEnemy::StaticClass(),Transform);E->EntityId=FString::Printf(TEXT("s%d-e%d"),R.stage,I);E->Type=T;UGameplayStatics::FinishSpawningActor(E,Transform);} + if(R.stage==4&&R.branch==0){FTransform T(FVector(2800,4000,0));auto Boss=W->SpawnActorDeferred(AIronsEnemy::StaticClass(),T);Boss->Type=3;Boss->bTerminal=true;Boss->EntityId=TEXT("boss");UGameplayStatics::FinishSpawningActor(Boss,T);} + if(R.stage==4&&R.branch==1){Escort=Marker(W,FVector(-1500,0,110),TEXT("escort"));Escort->SetActorScale3D(FVector(2.5f,3,1));} + if(H->IsTraining()){ + S.rangeReset();Marker(W,FVector(-1800,-2100,60),TEXT("rangeReset"));Marker(W,FVector(-1300,-2100,60),TEXT("objective")); + auto Ramp=Block(W,FVector(4200,-1300,210),FVector(1000,2700,70),FLinearColor(.12f,.2f,.24f));Ramp->SetWorldRotation(FRotator(0,0,9));Block(W,FVector(4200,1300,400),FVector(1000,2500,80),FLinearColor(.12f,.2f,.24f)); + const FVector Points[]={FVector(4200,-3000,150),FVector(4200,100,590),FVector(4200,2000,590)}; + for(int I=0;I<3;I++){auto Gate=Marker(W,Points[I],FString::Printf(TEXT("rangeGate%d"),I));Gate->Body->SetCollisionEnabled(ECollisionEnabled::NoCollision);Gate->SetActorHiddenInGame(true);for(int Side:{-1,1})Block(W,Points[I]+FVector(Side*350,0,100),FVector(40,60,500),FLinearColor(.1f,.4f,.36f));Block(W,Points[I]+FVector(0,0,350),FVector(740,60,35),FLinearColor(.1f,.7f,.5f),false);} + for(int I=0;I<2;I++){FTransform T(FVector(I?-3500:-2000,1500,180));auto Target=W->SpawnActorDeferred(AIronsEnemy::StaticClass(),T);Target->Type=4+I;Target->EntityId=FString::Printf(TEXT("range-target-%d"),I);UGameplayStatics::FinishSpawningActor(Target,T);} + } +} +void AIronsGameMode::Interact(){ + auto H=Cast(GetGameInstance());auto PC=Cast(UGameplayStatics::GetPlayerController(this,0));if(!H||!PC||!PC->Mech())return; + AIronsMarker* Nearest=nullptr;float Distance=500;for(TActorIterator I(GetWorld());I;++I)if(!I->IsHidden()){float D=FVector::Dist2D(I->GetActorLocation(),PC->Mech()->GetActorLocation());if(DNotice(TEXT("ue.nothingNear"));return;}auto& S=H->Core();FString K=Nearest->Kind; + if(K==TEXT("loot")){if(H->Accept(S.addLoot(TCHAR_TO_UTF8(*Nearest->ItemId),Nearest->Quantity,TCHAR_TO_UTF8(*Nearest->SourceKey)))){Nearest->SetActorHiddenInGame(true);Nearest->SetActorEnableCollision(false);H->Notice(TEXT("ue.loot"),Nearest->ItemId);}} + else if(K==TEXT("extract")){if(S.field().ready){if(H->Accept(S.boardExtraction())){H->Notice(TEXT("ue.extracted"));PC->ReturnHome();}}else if(S.field().extraction<0){if(H->Accept(S.callExtraction()))H->Notice(TEXT("ue.called"));}} + else if(K==TEXT("objective")){if(S.state().run.stage<4||H->IsTraining()){if(H->Accept(S.objective()))H->Notice(TEXT("ue.objectiveDone"));}else H->Notice(TEXT("ue.finalObjective"));} + else if(K.StartsWith(TEXT("gate"))){int Branch=K.EndsWith(TEXT("1"))?1:0;if(H->Accept(S.nextStage(Branch)))UGameplayStatics::OpenLevel(this,TEXT("/Game/IRONSPORE/Maps/L_Warzone"));} + else if(K==TEXT("upgrade")){PC->SetPage(TEXT("upgrade"));} + else if(K==TEXT("rangeReset")){S.rangeReset();H->Notice(TEXT("ue.rangeReset"));} + else if(K==TEXT("escort"))H->Notice(TEXT("ue.escort")); +} +FString AIronsGameMode::Prompt()const{ + auto PC=Cast(UGameplayStatics::GetPlayerController(this,0));auto H=Cast(GetGameInstance());if(!PC||!PC->Mech()||!H)return TEXT(""); + AIronsMarker* Best=nullptr;float D=500;for(TActorIterator I(GetWorld());I;++I)if(!I->IsHidden()){float T=FVector::Dist2D(I->GetActorLocation(),PC->Mech()->GetActorLocation());if(TLabel(TEXT("ue.explore"));if(Best->Kind==TEXT("loot"))return H->Keys[9]+TEXT(" ")+H->Label(TEXT("ue.search"))+TEXT(" ")+H->ItemName(TCHAR_TO_UTF8(*Best->ItemId)); + FString Key=Best->Kind==TEXT("extract")?(bExtractionReady?TEXT("ue.board"):ExtractionWait>=0?TEXT("ue.wait"):TEXT("ue.call")):Best->Kind.StartsWith(TEXT("gate"))?TEXT("ue.nextStage"):Best->Kind==TEXT("objective")?TEXT("ue.activate"):Best->Kind==TEXT("upgrade")?TEXT("ue.upgrade"):TEXT("ue.escort");return H->Keys[9]+TEXT(" ")+H->Label(Key); +} +void AIronsGameMode::Tick(float Dt){ + Super::Tick(Dt);auto H=Cast(GetGameInstance());auto PC=Cast(UGameplayStatics::GetPlayerController(this,0));if(!H||!PC||!PC->Playing()||!PC->Mech())return; + auto P=PC->Mech();bool WasReady=bExtractionReady;ExtractionWait=H->Core().field().extraction;bExtractionReady=H->Core().field().ready;if(!WasReady&&bExtractionReady)H->Notice(TEXT("ue.readyToBoard")); + if(H->IsTraining()){int Inside=-1;for(TActorIterator I(GetWorld());I;++I)if(I->Kind.StartsWith(TEXT("rangeGate"))&&FVector::Dist2D(I->GetActorLocation(),P->GetActorLocation())<280&&FMath::Abs(I->GetActorLocation().Z-P->GetActorLocation().Z)<210){Inside=FCString::Atoi(*I->Kind.Right(1));break;}if(Inside>=0&&Inside!=LastCourseGate)H->Core().rangeGate(Inside);LastCourseGate=Inside;} + if(!H->Core().state().run.podUsed){bool Stable=P->GetCharacterMovement()->IsMovingOnGround()&&P->GetVelocity().Size()<20&&GetWorld()->GetTimeSeconds()-P->DamageTime>1;auto Result=H->Core().advanceRecovery(Dt,PC->bPodHeld,Stable,PC->Screen->SelectedPodItems());if(!Result){if(Result.detail=="interrupted")H->Notice(TEXT("ue.podInterrupted"));else{H->Accept(Result);PC->bPodHeld=false;}}if(H->Core().state().run.podUsed){H->Notice(TEXT("ue.podSent"));PC->bPodHeld=false;}} + PodProgress=H->Core().field().podProgress; + if(Escort&&!H->Core().state().run.objectiveDone&&FVector::Dist2D(Escort->GetActorLocation(),P->GetActorLocation())<1200){EscortProgress+=Dt;Escort->AddActorWorldOffset(FVector(75*Dt,0,0));int Threats=0;for(TActorIterator I(GetWorld());I;++I)if(!I->IsHidden()&&FVector::Dist2D(I->GetActorLocation(),Escort->GetActorLocation())<1500)Threats++; + H->Core().damageEscort(Threats*1.5f*Dt);if(H->Core().state().run.escortArmor<=0){H->Notice(TEXT("ue.escortLost"));if(H->FinishSortie(true))PC->ReturnHome();}else if(EscortProgress>=45){H->Accept(H->Core().objective());if(H->Core().state().run.stage==4)H->Accept(H->Core().completeRoute());}} +} diff --git a/Source/ueRouge/Private/Tests/IronsPerformanceLayoutTests.cpp b/Source/ueRouge/Private/Tests/IronsPerformanceLayoutTests.cpp new file mode 100644 index 0000000..ed654a2 --- /dev/null +++ b/Source/ueRouge/Private/Tests/IronsPerformanceLayoutTests.cpp @@ -0,0 +1,24 @@ +#include "IronsPerformanceOverlay.h" +#include "Misc/AutomationTest.h" + +#if WITH_DEV_AUTOMATION_TESTS +IMPLEMENT_SIMPLE_AUTOMATION_TEST(FIronsPerformanceLayoutTest,"IRONSPORE.Performance.ViewportLayout",EAutomationTestFlags::EditorContext|EAutomationTestFlags::EngineFilter) +bool FIronsPerformanceLayoutTest::RunTest(const FString& Parameters) +{ + for(const auto Size:{FVector2D(1280,720),FVector2D(1920,1080),FVector2D(2560,1440),FVector2D(3440,1440)})for(int32 Corner=0;Corner<4;++Corner){ + const auto L=FIronsPerformanceLayout::Make(Size,true,Corner); + const FSlateRect Content(L.ContentOrigin.X,L.ContentOrigin.Y,L.ContentOrigin.X+1920*L.ContentScale,L.ContentOrigin.Y+1080*L.ContentScale); + TestTrue(TEXT("Panel remains in the actual viewport"),L.Panel.Left>=0&&L.Panel.Top>=0&&L.Panel.Right<=Size.X&&L.Panel.Bottom<=Size.Y); + TestTrue(TEXT("Existing content never intersects the panel band"),Content.Bottom<=L.Panel.Top||Content.Top>=L.Panel.Bottom); + const FVector2D Source(1730,610),Screen=Source*L.ContentScale+L.ContentOrigin; + TestTrue(TEXT("Grid pointer coordinates survive safe-band projection"),((Screen-L.ContentOrigin)/L.ContentScale).Equals(Source,.001)); + const FVector2D Aim=(Size*.5-L.ContentOrigin)/L.ContentScale; + TestTrue(TEXT("Crosshair remains at actual viewport center"),(Aim*L.ContentScale+L.ContentOrigin).Equals(Size*.5,.001)); + const float Margin=16*L.PanelScale; + TestTrue(TEXT("Wide-screen right anchor uses real viewport edge"),Corner==1||Corner==3||FMath::IsNearlyEqual(L.Panel.Right,static_cast(Size.X)-Margin,.01f)); + } + const auto Hidden=FIronsPerformanceLayout::Make(FVector2D(1920,1080),false,0); + TestTrue(TEXT("Hidden panel releases the reserved band"),Hidden.ContentOrigin.IsNearlyZero()&&FMath::IsNearlyEqual(Hidden.ContentScale,1.f)); + return true; +} +#endif diff --git a/Source/ueRouge/Private/ueRouge.cpp b/Source/ueRouge/Private/ueRouge.cpp new file mode 100644 index 0000000..f1800c2 --- /dev/null +++ b/Source/ueRouge/Private/ueRouge.cpp @@ -0,0 +1,2 @@ +#include "Modules/ModuleManager.h" +IMPLEMENT_PRIMARY_GAME_MODULE(FDefaultGameModuleImpl, ueRouge, "ueRouge"); diff --git a/Source/ueRouge/Public/IronsGame.h b/Source/ueRouge/Public/IronsGame.h new file mode 100644 index 0000000..6a1c740 --- /dev/null +++ b/Source/ueRouge/Public/IronsGame.h @@ -0,0 +1,168 @@ +#pragma once +#include "CoreMinimal.h" +#include "GameFramework/GameModeBase.h" +#include "GameFramework/PlayerController.h" +#include "GameFramework/Character.h" +#include "Blueprint/UserWidget.h" +#include "IronsHost.h" +#include "IronsGame.generated.h" + +class UCameraComponent; +class USpringArmComponent; +class UStaticMeshComponent; +class UInputMappingContext; +class UInputAction; +class UIronsScreen; +class UIronsPerformanceOverlay; +class APointLight; + +UCLASS() +class UEROUGE_API AIronsProjectile : public AActor +{ + GENERATED_BODY() +public: + AIronsProjectile(); + UPROPERTY() TObjectPtr Body; + UPROPERTY() TObjectPtr Target; + FVector Velocity; + float Damage=65; + float Radius=600; + virtual void Tick(float Dt)override; +}; + +UCLASS() +class UEROUGE_API AIronsCharacter : public ACharacter +{ + GENERATED_BODY() +public: + AIronsCharacter(); + UPROPERTY(VisibleAnywhere,BlueprintReadOnly) TObjectPtr Body; + UPROPERTY(VisibleAnywhere,BlueprintReadOnly) TObjectPtr Arm; + UPROPERTY(VisibleAnywhere,BlueprintReadOnly) TObjectPtr Camera; + bool bCockpit=false,bAim=false; + float FireFlash=0,DamageTime=-100; + virtual void BeginPlay()override; + virtual void Tick(float Dt)override; + void SwitchView(); + void Shoot(bool Secondary=false); + void ReceiveHit(float Damage); + UIronsGameInstance* Host()const; +}; + +UCLASS() +class UEROUGE_API AIronsEnemy : public AActor +{ + GENERATED_BODY() +public: + AIronsEnemy(); + UPROPERTY(VisibleAnywhere) TObjectPtr Body; + FString EntityId; + int Type=0; + bool bTerminal=false; + virtual void BeginPlay()override; + virtual void Tick(float Dt)override; + void Hit(float Damage); +}; + +UCLASS() +class UEROUGE_API AIronsMarker : public AActor +{ + GENERATED_BODY() +public: + AIronsMarker(); + UPROPERTY(VisibleAnywhere) TObjectPtr Body; + FString Kind,SourceKey,ItemId; + int Quantity=1; +}; + +UCLASS() +class UEROUGE_API AIronsGameMode : public AGameModeBase +{ + GENERATED_BODY() +public: + AIronsGameMode(); + virtual void BeginPlay()override; + virtual void Tick(float Dt)override; + void BuildHangar(); + void BuildBattle(); + void Interact(); + FString Prompt()const; + float ExtractionWait=-1,PodProgress=0; + float EscortProgress=0; + int LastCourseGate=-1; + bool bExtractionReady=false; + UPROPERTY() TObjectPtr Extraction; + UPROPERTY() TObjectPtr Escort; +}; + +UCLASS() +class UEROUGE_API AIronsController : public APlayerController +{ + GENERATED_BODY() + UPROPERTY() TObjectPtr Context; + UPROPERTY() TArray> Actions; +public: + AIronsController(); + UPROPERTY() TObjectPtr Screen; + UPROPERTY() TObjectPtr PerformanceOverlay; + FString Page=TEXT("home"),PreviousPage=TEXT("home"); + int Rebind=-1; + bool bTrigger=false,bPodHeld=false; + virtual void BeginPlay()override; + virtual void EndPlay(const EEndPlayReason::Type Reason)override; + virtual void PlayerTick(float Dt)override; + virtual bool InputKey(const FInputKeyEventArgs& Params)override; + void ProcessKey(FKey Key,bool Pressed); + void RebuildInput(); + void SetPage(const FString& NewPage); + void Launch(bool Training=false); + void ReturnHome(); + void Menu(); + void Interact(); + void FirePressed();void FireReleased(); + void TickAcceptance(float Dt); + void TickPerformanceAcceptance(float Dt); + float AcceptanceClock=0; + double PerformanceLastClock=0; + TArray FrameSamples; + UFUNCTION(Exec) void IronsUi(const FString& NewPage); + UFUNCTION(Exec) void IronsQA(const FString& Command); + UIronsGameInstance* Host()const; + AIronsCharacter* Mech()const; + bool Playing()const{return Page==TEXT("play");} +}; + +struct FIronsHitRegion { + FSlateRect Rect; + FString Command,Argument; + int A=0,B=0,C=0; +}; +struct FIronsGridRegion {FSlateRect Rect;gf::Hold Hold;float Cell=0;int Width=0,Height=0;}; + +UCLASS(Blueprintable) +class UEROUGE_API UIronsScreen : public UUserWidget +{ + GENERATED_BODY() + mutable TArray Hits; + mutable TArray Grids; + UPROPERTY() TMap> Icons; + FString Selected,DragUid; + gf::Hold SelectedHold=gf::Hold::Warehouse,DragHold=gf::Hold::Warehouse; + int SelectedSlot=-1,Scroll=0,Quantity=1; + bool bDragging=false,bRotated=false,bPodSelect=false,bDetails=true; + TArray PodSelection; + FVector2D Pointer,DragStart; +public: + virtual void NativeConstruct()override; + virtual int32 NativePaint(const FPaintArgs&,const FGeometry&,const FSlateRect&,FSlateWindowElementList&,int32,const FWidgetStyle&,bool)const override; + virtual FReply NativeOnMouseButtonDown(const FGeometry&,const FPointerEvent&)override; + virtual FReply NativeOnMouseButtonUp(const FGeometry&,const FPointerEvent&)override; + virtual FReply NativeOnMouseMove(const FGeometry&,const FPointerEvent&)override; + virtual FReply NativeOnMouseWheel(const FGeometry&,const FPointerEvent&)override; + virtual FReply NativeOnKeyDown(const FGeometry&,const FKeyEvent&)override; + void Dispatch(const FIronsHitRegion& Hit); + void ClearSelection(); + std::vector SelectedPodItems()const; + AIronsController* Controller()const; + UIronsGameInstance* Host()const; +}; diff --git a/Source/ueRouge/Public/IronsPerformanceOverlay.h b/Source/ueRouge/Public/IronsPerformanceOverlay.h new file mode 100644 index 0000000..d4abeb1 --- /dev/null +++ b/Source/ueRouge/Public/IronsPerformanceOverlay.h @@ -0,0 +1,30 @@ +#pragma once + +#include "CoreMinimal.h" +#include "Blueprint/UserWidget.h" +#include "IronsPerformanceOverlay.generated.h" + +// The panel uses the actual viewport; the existing 16:9 UI fits the remaining band. +struct UEROUGE_API FIronsPerformanceLayout +{ + FVector2D ContentOrigin=FVector2D::ZeroVector; + float ContentScale=1,PanelScale=1; + FSlateRect Panel; + static FIronsPerformanceLayout Make(FVector2D ViewSize,bool Enabled,int32 Corner); +}; + +UCLASS() +class UEROUGE_API UIronsPerformanceOverlay : public UUserWidget +{ + GENERATED_BODY() + FString Labels[6],Values[6]; + FLinearColor Colors[6]; + int32 LabelSizes[6]={},ValueSizes[6]={}; + float CachedScale=-1; + uint64 CachedRevision=MAX_uint64; + bool bCachedEnglish=false; +public: + virtual void NativeConstruct() override; + virtual void NativeTick(const FGeometry& Geometry,float DeltaTime) override; + virtual int32 NativePaint(const FPaintArgs&,const FGeometry&,const FSlateRect&,FSlateWindowElementList&,int32,const FWidgetStyle&,bool)const override; +}; diff --git a/Source/ueRouge/ueRouge.Build.cs b/Source/ueRouge/ueRouge.Build.cs new file mode 100644 index 0000000..c3fbe1f --- /dev/null +++ b/Source/ueRouge/ueRouge.Build.cs @@ -0,0 +1,12 @@ +using System.IO; +using UnrealBuildTool; +public class ueRouge : ModuleRules +{ + public ueRouge(ReadOnlyTargetRules Target) : base(Target) + { + PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs; + PublicDependencyModuleNames.AddRange(new[] { "Core", "CoreUObject", "Engine", "UEAdapter", "GameFrameworkExternal", "InputCore", "EnhancedInput", "UMG", "Slate", "SlateCore" }); + PrivateDependencyModuleNames.AddRange(new[] { "Json", "RenderCore" }); + RuntimeDependencies.Add("$(ProjectDir)/Content/IRONSPORE/Raw/...", StagedFileType.NonUFS); + } +} diff --git a/Source/ueRougeEditor.Target.cs b/Source/ueRougeEditor.Target.cs new file mode 100644 index 0000000..d0ac1c9 --- /dev/null +++ b/Source/ueRougeEditor.Target.cs @@ -0,0 +1,12 @@ +using UnrealBuildTool; +public class ueRougeEditorTarget : TargetRules +{ + public ueRougeEditorTarget(TargetInfo Target) : base(Target) + { + Type = TargetType.Editor; + DefaultBuildSettings = BuildSettingsVersion.V7; + IncludeOrderVersion = EngineIncludeOrderVersion.Unreal5_8; + CppStandard = CppStandardVersion.Cpp20; + ExtraModuleNames.AddRange(new[] { "ueRouge", "UEAdapter", "IronsEditor" }); + } +} diff --git a/SourceAssets/Audio/Generated/S_Impact.wav b/SourceAssets/Audio/Generated/S_Impact.wav new file mode 100644 index 0000000..55d3cdd Binary files /dev/null and b/SourceAssets/Audio/Generated/S_Impact.wav differ diff --git a/SourceAssets/Audio/Generated/S_Secondary.wav b/SourceAssets/Audio/Generated/S_Secondary.wav new file mode 100644 index 0000000..d0cb683 Binary files /dev/null and b/SourceAssets/Audio/Generated/S_Secondary.wav differ diff --git a/SourceAssets/Audio/Generated/S_Shot.wav b/SourceAssets/Audio/Generated/S_Shot.wav new file mode 100644 index 0000000..e9d600e Binary files /dev/null and b/SourceAssets/Audio/Generated/S_Shot.wav differ diff --git a/SourceAssets/Audio/Generated/S_UI.wav b/SourceAssets/Audio/Generated/S_UI.wav new file mode 100644 index 0000000..ff84f65 Binary files /dev/null and b/SourceAssets/Audio/Generated/S_UI.wav differ diff --git a/SourceAssets/Blender/Environment/HangarBay.blend b/SourceAssets/Blender/Environment/HangarBay.blend new file mode 100644 index 0000000..840d23a Binary files /dev/null and b/SourceAssets/Blender/Environment/HangarBay.blend differ diff --git a/SourceAssets/Blender/Mechs/Mech_Scout_A.blend b/SourceAssets/Blender/Mechs/Mech_Scout_A.blend new file mode 100644 index 0000000..069723b Binary files /dev/null and b/SourceAssets/Blender/Mechs/Mech_Scout_A.blend differ diff --git a/SourceAssets/Derived/UI/T_MechFrontSilhouette.png b/SourceAssets/Derived/UI/T_MechFrontSilhouette.png new file mode 100644 index 0000000..2932d55 Binary files /dev/null and b/SourceAssets/Derived/UI/T_MechFrontSilhouette.png differ diff --git a/SourceAssets/Derived/UI/T_coil.png b/SourceAssets/Derived/UI/T_coil.png new file mode 100644 index 0000000..e3e03c3 Binary files /dev/null and b/SourceAssets/Derived/UI/T_coil.png differ diff --git a/SourceAssets/Derived/UI/T_composite_guard.png b/SourceAssets/Derived/UI/T_composite_guard.png new file mode 100644 index 0000000..d03dff2 Binary files /dev/null and b/SourceAssets/Derived/UI/T_composite_guard.png differ diff --git a/SourceAssets/Derived/UI/T_efficient.png b/SourceAssets/Derived/UI/T_efficient.png new file mode 100644 index 0000000..58149a5 Binary files /dev/null and b/SourceAssets/Derived/UI/T_efficient.png differ diff --git a/SourceAssets/Derived/UI/T_guided.png b/SourceAssets/Derived/UI/T_guided.png new file mode 100644 index 0000000..fe394c8 Binary files /dev/null and b/SourceAssets/Derived/UI/T_guided.png differ diff --git a/SourceAssets/Derived/UI/T_pulse.png b/SourceAssets/Derived/UI/T_pulse.png new file mode 100644 index 0000000..9723460 Binary files /dev/null and b/SourceAssets/Derived/UI/T_pulse.png differ diff --git a/SourceAssets/Derived/UI/T_rapid.png b/SourceAssets/Derived/UI/T_rapid.png new file mode 100644 index 0000000..3cff46e Binary files /dev/null and b/SourceAssets/Derived/UI/T_rapid.png differ diff --git a/SourceAssets/Derived/UI/T_reactive_guard.png b/SourceAssets/Derived/UI/T_reactive_guard.png new file mode 100644 index 0000000..5771262 Binary files /dev/null and b/SourceAssets/Derived/UI/T_reactive_guard.png differ diff --git a/SourceAssets/Derived/UI/T_siege.png b/SourceAssets/Derived/UI/T_siege.png new file mode 100644 index 0000000..a1c81d5 Binary files /dev/null and b/SourceAssets/Derived/UI/T_siege.png differ diff --git a/SourceAssets/Derived/UI/T_vector.png b/SourceAssets/Derived/UI/T_vector.png new file mode 100644 index 0000000..70b518a Binary files /dev/null and b/SourceAssets/Derived/UI/T_vector.png differ diff --git a/SourceAssets/Exports/Unity/Environment/HangarBay.fbx b/SourceAssets/Exports/Unity/Environment/HangarBay.fbx new file mode 100644 index 0000000..79fc352 Binary files /dev/null and b/SourceAssets/Exports/Unity/Environment/HangarBay.fbx differ diff --git a/SourceAssets/Exports/Unity/Mechs/Mech_Scout_A.fbx b/SourceAssets/Exports/Unity/Mechs/Mech_Scout_A.fbx new file mode 100644 index 0000000..b4e75f9 Binary files /dev/null and b/SourceAssets/Exports/Unity/Mechs/Mech_Scout_A.fbx differ diff --git a/SourceAssets/Localization/Tables/en.json b/SourceAssets/Localization/Tables/en.json new file mode 100644 index 0000000..944a0b1 --- /dev/null +++ b/SourceAssets/Localization/Tables/en.json @@ -0,0 +1,1316 @@ +{ + "entries": [ + { + "key": "range.choose", + "value": "Left: targets. Right: timed gates. Beyond the targets: salvage." + }, + { + "key": "range.start", + "value": "01 / START" + }, + { + "key": "range.upper", + "value": "02 / RAMP" + }, + { + "key": "range.landing", + "value": "BRAKE HERE" + }, + { + "key": "range.gallery", + "value": "TARGET RANGE" + }, + { + "key": "range.salvage", + "value": "SALVAGE BAY" + }, + { + "key": "range.material", + "value": "MATERIALS" + }, + { + "key": "range.parts", + "value": "FIELD PARTS" + }, + { + "key": "range.targetGuide", + "value": "Hit both targets. Hits: {0}. Use the reset desk to retry." + }, + { + "key": "range.brake", + "value": "Stop inside the stripes at the far end of the platform." + }, + { + "key": "range.finish", + "value": "Course time: {0:F1} s. Cross the start gate again to retry." + }, + { + "key": "range.recoveryGuide", + "value": "Search a case [{0}], select cargo [{1}], then stop and hold [{2}] to send." + }, + { + "key": "range.recoverySent", + "value": "Cargo sent. Call the transport at the orange pad [{0}]." + }, + { + "key": "range.board", + "value": "Transport ready. Board at the orange pad [{0}]." + }, + { + "key": "range.reset", + "value": "Reset drills [{0}]" + }, + { + "key": "range.gate", + "value": "Gate cleared. Follow the lane." + }, + { + "key": "range.hit", + "value": "Target hits: {0}" + }, + { + "key": "range.ready", + "value": "Training equipment reset." + }, + { + "key": "ui.home.subtitle", + "value": "MECH RECOVERY OPERATIONS" + }, + { + "key": "ui.home.loadout", + "value": "STARTING LOADOUT" + }, + { + "key": "ui.slot.0", + "value": "PRIMARY" + }, + { + "key": "ui.slot.1", + "value": "SECONDARY" + }, + { + "key": "ui.slot.2", + "value": "PROPULSION" + }, + { + "key": "ui.slot.3", + "value": "DEFENSE" + }, + { + "key": "ui.home.part", + "value": "{0} / {1} [{2} M / {3} P]" + }, + { + "key": "ui.home.budget", + "value": "MASS {0} / {1} POWER {2} / {3}" + }, + { + "key": "ui.home.deploy", + "value": "DEPLOY SORTIE" + }, + { + "key": "ui.home.range", + "value": "DRIVING RANGE" + }, + { + "key": "ui.home.resume", + "value": "RESUME SORTIE" + }, + { + "key": "ui.home.locked", + "value": "Selected gear is out of stock. Standard slots can use loaners. All models are available in the range." + }, + { + "key": "ui.home.moveHint", + "value": "{0} move / mouse aim / {1} perspective / {2} boost / {3} jump & lift" + }, + { + "key": "ui.home.fireHint", + "value": "{0} primary / {1} precision aim / {2} secondary / {3} shield / {4} interact" + }, + { + "key": "ui.home.inventory", + "value": "HANGAR INVENTORY" + }, + { + "key": "ui.home.stock", + "value": "ALLOY {0} CELLS {1} SCHEMATICS {2} EXTRACTIONS {3} ROUTES CLEARED {4}" + }, + { + "key": "ui.home.researched", + "value": "{0} / RESEARCHED" + }, + { + "key": "ui.home.research", + "value": "{0} / RESEARCH: 2 ALLOY + 1 CELL" + }, + { + "key": "ui.home.quit", + "value": "QUIT" + }, + { + "key": "ui.home.language", + "value": "语言 / Language · {0}" + }, + { + "key": "ui.home.languageHelp", + "value": "Choose language" + }, + { + "key": "ui.language.current", + "value": "{0} / Selected" + }, + { + "key": "ui.hud.operation", + "value": "CURRENT OPERATION" + }, + { + "key": "ui.hud.armor", + "value": "ARMOR {0}" + }, + { + "key": "ui.hud.energy", + "value": "ENERGY {0}" + }, + { + "key": "ui.hud.shield", + "value": "SHIELD ACTIVE" + }, + { + "key": "ui.hud.podUsed", + "value": "POD USED / 1 OF 1" + }, + { + "key": "ui.hud.podPreparing", + "value": "PREPARING POD..." + }, + { + "key": "ui.hud.podReady", + "value": "POD READY / {0} SELECT CARGO" + }, + { + "key": "ui.hud.hint", + "value": "{0} VIEW {1} BOOST {2} LIFT {3} SECONDARY {4} SHIELD {5} INTERACT {6} CARGO HOLD {7} SEND POD {8} PAUSE" + }, + { + "key": "ui.hud.zone", + "value": "ZONE {0} / 5" + }, + { + "key": "ui.hud.status", + "value": "{0} {1}" + }, + { + "key": "ui.hud.complete", + "value": "MISSION COMPLETE / bring your cargo to extraction" + }, + { + "key": "ui.hud.boss", + "value": "Defeat the Overseer" + }, + { + "key": "ui.hud.escort", + "value": "Escort the engineering crawler to the far gate" + }, + { + "key": "ui.hud.routeOpen", + "value": "Route unlocked / search, extract or proceed" + }, + { + "key": "ui.hud.objective", + "value": "Recover navigation data at the relay" + }, + { + "key": "ui.hud.cockpit", + "value": "COCKPIT" + }, + { + "key": "ui.hud.shoulder", + "value": "SHOULDER" + }, + { + "key": "ui.hud.telemetry", + "value": "{0}\n{1} / {2}\nCARGO {3} / 16\n{4}" + }, + { + "key": "ui.hud.transportReady", + "value": "TRANSPORT READY / BOARD AT PAD" + }, + { + "key": "ui.hud.transportEta", + "value": "TRANSPORT ETA {0}s" + }, + { + "key": "ui.hud.extractionAvailable", + "value": "EXTRACTION AVAILABLE / ORANGE PAD" + }, + { + "key": "ui.cargo.title", + "value": "Cargo and ejection safe case" + }, + { + "key": "ui.cargo.used", + "value": "Pod launched. On destruction, only items in the safe case return." + }, + { + "key": "ui.cargo.help", + "value": "Drag an item or select it and click a cell. Press R to rotate. The safe case returns on destruction. Mark cargo, close this panel and hold {0} while stationary for three seconds to launch the pod." + }, + { + "key": "ui.cargo.stack", + "value": "{0} {1} ×{2}" + }, + { + "key": "ui.cargo.selected", + "value": "[SELECTED]" + }, + { + "key": "ui.cargo.capacity", + "value": "Hold {0}/16 cells Pod {1}/4 cells" + }, + { + "key": "ui.return", + "value": "RETURN TO PILOT" + }, + { + "key": "ui.refit.title", + "value": "FIELD REFIT" + }, + { + "key": "ui.refit.help", + "value": "Recovered a replacement part. Compare mass and power before installing." + }, + { + "key": "ui.refit.equipped", + "value": "EQUIPPED / {0} {1} M / {2} P" + }, + { + "key": "ui.refit.recovered", + "value": "RECOVERED / {0} {1} M / {2} P" + }, + { + "key": "ui.refit.install", + "value": "INSTALL RECOVERED PART" + }, + { + "key": "ui.refit.keep", + "value": "KEEP CURRENT LOADOUT" + }, + { + "key": "ui.routes.split", + "value": "COMMIT TO AN ENDPOINT" + }, + { + "key": "ui.routes.title", + "value": "CHOOSE THE NEXT WARZONE" + }, + { + "key": "ui.routes.splitHelp", + "value": "Your loadout has had two zones to develop. Choose the mission your mech is ready to tackle." + }, + { + "key": "ui.routes.help", + "value": "Objectives and resource categories are known. Specific salvage is discovered inside the zone." + }, + { + "key": "ui.routes.boss", + "value": "OVERSEER ROUTE / assault relay / weapon salvage" + }, + { + "key": "ui.routes.escort", + "value": "ENGINEERING ESCORT / restore supply link / propulsion salvage" + }, + { + "key": "ui.routes.freight", + "value": "FREIGHT RECLAMATION / recover navigation data / alloy-rich" + }, + { + "key": "ui.routes.power", + "value": "POWER SUBSTATION / reactivate relay / cell-rich" + }, + { + "key": "ui.routes.stay", + "value": "STAY IN THIS WARZONE" + }, + { + "key": "ui.pause.title", + "value": "PAUSED" + }, + { + "key": "ui.pause.help", + "value": "Sortie progress is saved at checkpoints and every successful recovery." + }, + { + "key": "ui.pause.resume", + "value": "RESUME" + }, + { + "key": "ui.pause.range", + "value": "RETURN FROM RANGE" + }, + { + "key": "ui.pause.quit", + "value": "SAVE & QUIT" + }, + { + "key": "report.ready", + "value": "Choose a chassis and configure your sortie." + }, + { + "key": "report.range", + "value": "Driving range complete. Your sortie inventory is ready in the hangar." + }, + { + "key": "report.destroyed", + "value": "Mech destroyed. Lost {0} equipped items or cargo stacks. The safe case returned {1} items or stacks." + }, + { + "key": "report.complete", + "value": "MISSION COMPLETE / all carried materials and schematics secured." + }, + { + "key": "report.extracted", + "value": "Extracted. Equipped gear, cargo and safe case contents recovered." + }, + { + "key": "notice.unfinished", + "value": "An unfinished sortie is available. Resume it first." + }, + { + "key": "notice.budget", + "value": "Loadout exceeds chassis mass or power budget." + }, + { + "key": "notice.researchFirst", + "value": "Selected equipment is unavailable. Choose an owned item or manufacture one." + }, + { + "key": "notice.saveFailed", + "value": "Could not save. Please try again." + }, + { + "key": "notice.range", + "value": "DRIVING RANGE / {0} switches perspective. {1} operates nearby devices." + }, + { + "key": "notice.enter", + "value": "Search the zone. Complete the objective to open the onward route." + }, + { + "key": "notice.podSecured", + "value": "CARGO SECURED / banked immediately. Sortie continues." + }, + { + "key": "notice.cargoUnavailable", + "value": "Cargo selection is no longer available." + }, + { + "key": "notice.twoSlots", + "value": "The marked items do not fit in the 2×2 pod." + }, + { + "key": "notice.holdFull", + "value": "Cargo hold full. Send two stacks or extract." + }, + { + "key": "notice.recovered", + "value": "RECOVERED / {0} ×{1}" + }, + { + "key": "notice.partBudget", + "value": "That part exceeds the chassis budget." + }, + { + "key": "notice.refit", + "value": "FIELD REFIT / {0}" + }, + { + "key": "notice.routeOpen", + "value": "ROUTE OPEN / continue searching, extract, or use the onward gate." + }, + { + "key": "notice.complete", + "value": "MISSION COMPLETE / return to the extraction pad with your cargo." + }, + { + "key": "notice.objectiveFirst", + "value": "Complete the zone objective to open the route." + }, + { + "key": "notice.rangeComplete", + "value": "Range objective complete. Extract or keep testing your chassis." + }, + { + "key": "notice.extract", + "value": "Use the extraction pad to bring your cargo home." + }, + { + "key": "notice.researched", + "value": "Research complete. The recipe is unlocked; use materials to manufacture equipment." + }, + { + "key": "notice.researchCost", + "value": "Research needs a recovered schematic, 2 alloy and 1 cell." + }, + { + "key": "notice.transportInbound", + "value": "TRANSPORT INBOUND / you can move and fight while waiting." + }, + { + "key": "notice.extractionCalled", + "value": "EXTRACTION CALLED / hold for 15 seconds, then return to board." + }, + { + "key": "notice.transportReady", + "value": "TRANSPORT READY / return to the orange pad and press {0} to board." + }, + { + "key": "notice.escortLost", + "value": "ENGINEERING CRAWLER LOST / extraction remains available." + }, + { + "key": "prompt.search", + "value": "{0} SEARCH SALVAGE" + }, + { + "key": "prompt.relayOnline", + "value": "RELAY ONLINE" + }, + { + "key": "prompt.objective", + "value": "{0} RECOVER NAVIGATION DATA" + }, + { + "key": "prompt.route", + "value": "{0} CHOOSE ONWARD ROUTE" + }, + { + "key": "prompt.routeLocked", + "value": "ROUTE LOCKED / COMPLETE OBJECTIVE" + }, + { + "key": "prompt.board", + "value": "{0} BOARD / BANK CARGO" + }, + { + "key": "prompt.inbound", + "value": "TRANSPORT INBOUND" + }, + { + "key": "prompt.call", + "value": "{0} CALL EXTRACTION / 15s" + }, + { + "key": "world.extraction", + "value": "EXTRACTION" + }, + { + "key": "world.relay", + "value": "RELAY / ONWARD ROUTE" + }, + { + "key": "world.overseer", + "value": "OVERSEER" + }, + { + "key": "world.bulwark", + "value": "BULWARK" + }, + { + "key": "world.skimmer", + "value": "SKIMMER" + }, + { + "key": "world.sentinel", + "value": "SENTINEL" + }, + { + "key": "world.escort", + "value": "ENGINEERING CRAWLER" + }, + { + "key": "chassis.chassis_0.name", + "value": "Kestrel / Light" + }, + { + "key": "chassis.chassis_0.description", + "value": "Aerial mobility" + }, + { + "key": "chassis.chassis_1.name", + "value": "Warden / Medium" + }, + { + "key": "chassis.chassis_1.description", + "value": "Balanced assault" + }, + { + "key": "chassis.chassis_2.name", + "value": "Bastion / Heavy" + }, + { + "key": "chassis.chassis_2.description", + "value": "Armored advance" + }, + { + "key": "equipment.rapid.name", + "value": "Hail / rapid cannon" + }, + { + "key": "equipment.coil.name", + "value": "Lance / coil rifle" + }, + { + "key": "equipment.siege.name", + "value": "Maul / heavy cannon" + }, + { + "key": "equipment.guided.name", + "value": "Wisp / guided salvo" + }, + { + "key": "equipment.pulse.name", + "value": "Breaker / pulse burst" + }, + { + "key": "equipment.efficient.name", + "value": "Cruise / efficient drive" + }, + { + "key": "equipment.vector.name", + "value": "Vector / overdrive" + }, + { + "key": "equipment.composite_guard.name", + "value": "Composite plating" + }, + { + "key": "equipment.reactive_guard.name", + "value": "Reactive shield" + }, + { + "key": "item.alloy", + "value": "ALLOY" + }, + { + "key": "item.cells", + "value": "CELLS" + }, + { + "key": "item.blueprint_boost_efficient", + "value": "SCHEMATIC / EFFICIENT DRIVE" + }, + { + "key": "item.blueprint_coil_capacitor", + "value": "SCHEMATIC / COIL CAPACITOR" + }, + { + "key": "item.blueprint_reactive_guard", + "value": "SCHEMATIC / REACTIVE SHIELD" + }, + { + "key": "ui.cargo.hold", + "value": "Cargo hold · 4×4" + }, + { + "key": "ui.cargo.case", + "value": "Ejection safe case · 2×2" + }, + { + "key": "ui.cargo.selection", + "value": "Selected {0} {1}×{2} cells Quantity {3}" + }, + { + "key": "ui.cargo.rotate", + "value": "Rotate (R)" + }, + { + "key": "ui.cargo.cancel", + "value": "Deselect" + }, + { + "key": "ui.cargo.install", + "value": "Install" + }, + { + "key": "ui.cargo.mark", + "value": "Mark for pod" + }, + { + "key": "ui.cargo.unmark", + "value": "Unmark" + }, + { + "key": "notice.installFailed", + "value": "Cannot install. Check the loadout budget and the space for the replaced part." + }, + { + "key": "notice.placement", + "value": "Cannot place here. Check bounds, occupied cells and stack quantity." + }, + { + "key": "ui.home.chooseItem", + "value": "Equip {0} · {1}" + }, + { + "key": "ui.home.manufacture", + "value": "Build {0} · {1} alloy / {2} cells" + }, + { + "key": "settings.title", + "value": "SETTINGS" + }, + { + "key": "settings.help", + "value": "Changes are saved when you select Apply. Language changes immediately." + }, + { + "key": "settings.language", + "value": "Language" + }, + { + "key": "settings.audio", + "value": "Audio" + }, + { + "key": "settings.display", + "value": "Display" + }, + { + "key": "settings.controls", + "value": "Controls" + }, + { + "key": "settings.volume", + "value": "Master volume" + }, + { + "key": "settings.volumeHelp", + "value": "Adjust the volume of all game sounds. Set to 0% to mute." + }, + { + "key": "settings.percent", + "value": "{0}%" + }, + { + "key": "settings.multiplier", + "value": "{0}×" + }, + { + "key": "settings.quality", + "value": "Graphics quality" + }, + { + "key": "settings.quality.veryLow", + "value": "Very low" + }, + { + "key": "settings.quality.low", + "value": "Low" + }, + { + "key": "settings.quality.medium", + "value": "Medium" + }, + { + "key": "settings.quality.high", + "value": "High" + }, + { + "key": "settings.quality.veryHigh", + "value": "Very high" + }, + { + "key": "settings.quality.ultra", + "value": "Ultra" + }, + { + "key": "settings.resolution", + "value": "Resolution" + }, + { + "key": "settings.windowMode", + "value": "Window mode" + }, + { + "key": "settings.windowed", + "value": "Windowed" + }, + { + "key": "settings.fullscreen", + "value": "Fullscreen" + }, + { + "key": "settings.displayHelp", + "value": "After applying a display change, confirm it within 15 seconds or the previous display settings return." + }, + { + "key": "settings.display.confirm", + "value": "Keep these display settings?" + }, + { + "key": "settings.display.countdown", + "value": "The previous display settings return in {0} seconds." + }, + { + "key": "settings.display.keep", + "value": "Keep and save" + }, + { + "key": "settings.display.revert", + "value": "Revert" + }, + { + "key": "settings.display.reverted", + "value": "The previous display settings have been restored. Other edits are still pending." + }, + { + "key": "settings.mouse", + "value": "Mouse" + }, + { + "key": "settings.sensitivity", + "value": "Mouse sensitivity" + }, + { + "key": "settings.aimSensitivity", + "value": "Aim sensitivity multiplier" + }, + { + "key": "settings.bindings", + "value": "Key bindings" + }, + { + "key": "settings.bind.help", + "value": "Select a binding, then press a keyboard key or mouse button. Esc cancels capture and always works as a menu back key. Duplicate bindings are rejected." + }, + { + "key": "settings.bind.waiting", + "value": "Waiting for input" + }, + { + "key": "settings.bind.prompt", + "value": "Press a new key or mouse button for {0}. Press Esc to cancel." + }, + { + "key": "settings.bind.cancel", + "value": "Cancel binding" + }, + { + "key": "settings.bind.changed", + "value": "The binding for {0} has changed. Select Apply to save it." + }, + { + "key": "settings.bind.cancelled", + "value": "Binding cancelled; the previous key is unchanged." + }, + { + "key": "settings.bind.conflict", + "value": "This key is already assigned to {0}. Choose another key." + }, + { + "key": "settings.bind.reserved", + "value": "Esc is reserved for closing menus and cancelling input. Choose another key." + }, + { + "key": "settings.bind.unsupported", + "value": "Choose a keyboard key or mouse button." + }, + { + "key": "settings.reset", + "value": "Restore defaults" + }, + { + "key": "settings.resetHint", + "value": "Default settings and bindings are ready. Select Apply to save them." + }, + { + "key": "settings.back", + "value": "Back / discard edits" + }, + { + "key": "settings.apply", + "value": "Apply and save" + }, + { + "key": "settings.saved", + "value": "Settings saved." + }, + { + "key": "settings.saveFailed", + "value": "Settings could not be saved. Check available disk space and folder access, then try again." + }, + { + "key": "settings.action.move.up", + "value": "Move forward" + }, + { + "key": "settings.action.move.down", + "value": "Move backward" + }, + { + "key": "settings.action.move.left", + "value": "Move left" + }, + { + "key": "settings.action.move.right", + "value": "Move right" + }, + { + "key": "settings.action.fire", + "value": "Primary weapon" + }, + { + "key": "settings.action.aim", + "value": "Precision aim" + }, + { + "key": "settings.action.boost", + "value": "Boost" + }, + { + "key": "settings.action.jump", + "value": "Jump / lift" + }, + { + "key": "settings.action.view", + "value": "Switch perspective" + }, + { + "key": "settings.action.interact", + "value": "Interact" + }, + { + "key": "settings.action.inventory", + "value": "Open cargo hold" + }, + { + "key": "settings.action.cargo", + "value": "Launch cargo pod" + }, + { + "key": "settings.action.pause", + "value": "Pause" + }, + { + "key": "settings.action.secondary", + "value": "Secondary weapon" + }, + { + "key": "settings.action.guard", + "value": "Active defense" + }, + { + "key": "settings.key.mouseLeft", + "value": "Mouse left" + }, + { + "key": "settings.key.mouseRight", + "value": "Mouse right" + }, + { + "key": "settings.key.mouseMiddle", + "value": "Mouse middle" + }, + { + "key": "settings.key.mouseForward", + "value": "Mouse forward" + }, + { + "key": "settings.key.mouseBack", + "value": "Mouse back" + }, + { + "key": "settings.key.space", + "value": "Space" + }, + { + "key": "settings.key.leftShift", + "value": "Left Shift" + }, + { + "key": "settings.key.rightShift", + "value": "Right Shift" + }, + { + "key": "settings.action.reload", + "value": "Reload" + }, + { + "key": "ui.combat.nav", + "value": "SECTOR MAP" + }, + { + "key": "ui.combat.legend", + "value": "◇ Objective ○ Extraction" + }, + { + "key": "ui.combat.ammo", + "value": "MAGAZINE / UNLIMITED RESERVE" + }, + { + "key": "ui.combat.reload", + "value": "[{0}] Reload" + }, + { + "key": "ui.combat.loading", + "value": "Reloading {0:0.0}s" + }, + { + "key": "ui.combat.ready", + "value": "Ready" + }, + { + "key": "ui.combat.charging", + "value": "Cooldown {0:0.0}s" + }, + { + "key": "ui.combat.lowEnergy", + "value": "Insufficient energy" + }, + { + "key": "ui.combat.cargo", + "value": "Hold {0}/16 cells · Safe case {1}/4 cells" + }, + { + "key": "ui.combat.empty", + "value": "Empty slot" + }, + { + "key": "range.resetSign", + "value": "RESET DESK" + }, + { + "key": "base.home.title", + "value": "FORWARD BASE / MECH STANDBY" + }, + { + "key": "base.navigation", + "value": "BASE TERMINAL" + }, + { + "key": "base.enterHangar", + "value": "ENTER HANGAR" + }, + { + "key": "base.deploy", + "value": "DEPLOYMENT" + }, + { + "key": "base.hangar", + "value": "HANGAR" + }, + { + "key": "base.backHome", + "value": "HOME" + }, + { + "key": "base.home.description", + "value": "Refit your mech, sort recovered loot, and prepare for the next sortie." + }, + { + "key": "base.baseStatus", + "value": "BASE RESOURCES" + }, + { + "key": "base.recipes", + "value": "RESEARCHED RECIPES" + }, + { + "key": "base.record", + "value": "Sorties {0} / Extractions {1} / Objectives {2}" + }, + { + "key": "base.activeRun", + "value": "A sortie is in progress. Base inventory is read-only until you return." + }, + { + "key": "base.menuHint", + "value": "Mouse select · Tab focus · Enter confirm · Esc back" + }, + { + "key": "base.tab.loadout", + "value": "LOADOUT" + }, + { + "key": "base.tab.warehouse", + "value": "STASH" + }, + { + "key": "base.tab.research", + "value": "RESEARCH" + }, + { + "key": "base.preview", + "value": "MECH PREVIEW" + }, + { + "key": "base.previewHint", + "value": "Drag an empty part of the preview to rotate the mech." + }, + { + "key": "base.chassisStats", + "value": "Armor {0} / Cruise {1} m/s / Boost {2} m/s" + }, + { + "key": "base.chassis", + "value": "CHASSIS" + }, + { + "key": "base.installed", + "value": "EQUIPMENT" + }, + { + "key": "base.borrowed", + "value": "LOANED, RETURNED AT BASE" + }, + { + "key": "base.partStats", + "value": "Damage {0} / Mass {1} / Power {2}" + }, + { + "key": "base.compare", + "value": "Compared to current: damage {0:+0.##;-0.##;0}, mass {1:+0.##;-0.##;0}, power {2:+0.##;-0.##;0}" + }, + { + "key": "base.useBorrowed", + "value": "Use a basic loaned part" + }, + { + "key": "base.preparation", + "value": "CARRY-IN" + }, + { + "key": "base.cargo", + "value": "CARGO" + }, + { + "key": "base.safeCase", + "value": "SAFE CASE" + }, + { + "key": "base.reserved", + "value": "RESERVED" + }, + { + "key": "base.researchHelp", + "value": "Keep the physical blueprint. Research unlocks a recipe permanently; manufacture uses unreserved stash materials to create an item." + }, + { + "key": "base.researchCost", + "value": "Research: 2 Alloy + 1 Cell" + }, + { + "key": "base.manufacture", + "value": "MANUFACTURE" + }, + { + "key": "base.deployHelp", + "value": "Deploying transfers selected equipment and cargo out of the stash. Destruction protects the safe case; equipment and loot outside it are lost. Loaned parts are returned at base." + }, + { + "key": "base.confirm", + "value": "CONFIRM" + }, + { + "key": "warehouse.title", + "value": "BASE STASH" + }, + { + "key": "warehouse.pending", + "value": "PENDING {0}" + }, + { + "key": "warehouse.occupancy", + "value": "Occupied {0} / {1} cells" + }, + { + "key": "warehouse.stock", + "value": "Alloy {0} / Cells {1}" + }, + { + "key": "warehouse.controls", + "value": "Drag to move · R rotate preview · Ctrl+click stage/unstage · Shift+drag split" + }, + { + "key": "warehouse.filter.all", + "value": "ALL" + }, + { + "key": "warehouse.filter.equipment", + "value": "GEAR" + }, + { + "key": "warehouse.filter.material", + "value": "MATERIAL" + }, + { + "key": "warehouse.filter.blueprint", + "value": "BLUEPRINT" + }, + { + "key": "warehouse.search", + "value": "Find" + }, + { + "key": "warehouse.sort", + "value": "SORT" + }, + { + "key": "warehouse.claim", + "value": "CLAIM" + }, + { + "key": "warehouse.claimAll", + "value": "CLAIM WHAT FITS" + }, + { + "key": "warehouse.inboxHelp", + "value": "Recovered items are saved. Make room and claim them before starting a new sortie." + }, + { + "key": "warehouse.emptyInbox", + "value": "No pending items" + }, + { + "key": "warehouse.selectHelp", + "value": "Select an item to inspect it, then click an empty cell to place it. Carry-in reservations transfer only when you deploy." + }, + { + "key": "warehouse.itemInfo", + "value": "Size {0}×{1} / Quantity {2} / ID {3}" + }, + { + "key": "warehouse.equip", + "value": "EQUIP" + }, + { + "key": "warehouse.rotate", + "value": "ROTATE PREVIEW" + }, + { + "key": "warehouse.unstage", + "value": "UNSTAGE" + }, + { + "key": "warehouse.recycle", + "value": "RECYCLE FOR 2 ALLOY" + }, + { + "key": "warehouse.discard", + "value": "DISCARD" + }, + { + "key": "warehouse.discardConfirm", + "value": "Permanently discard {0} ×{1}? This frees its storage space." + }, + { + "key": "warehouse.sortFirst", + "value": "Claim or discard pending items before starting a new sortie." + }, + { + "key": "warehouse.incompatible", + "value": "This item does not fit that equipment slot." + }, + { + "key": "warehouse.noSpace", + "value": "No suitable space is available. Items and materials were kept." + }, + { + "key": "warehouse.missing", + "value": "A selected item is unavailable. Select your equipment again." + }, + { + "key": "warehouse.alreadyEquipped", + "value": "This item is reserved in an equipment slot. Choose another part or a loaned part first." + }, + { + "key": "warehouse.manufactureFailed", + "value": "Cannot manufacture. Check research, unreserved materials, and space for the output." + }, + { + "key": "warehouse.saved", + "value": "Changes saved." + }, + { + "key": "warehouse.split", + "value": "SPLIT MATERIALS" + }, + { + "key": "warehouse.splitHelp", + "value": "Choose how many to move. The remainder stays in its original stack." + }, + { + "key": "warehouse.quantity", + "value": "Quantity" + }, + { + "key": "warehouse.recoveredPending", + "value": "Pod cargo recovered. {0} pending items can be sorted after you return." + }, + { + "key": "warehouse.available", + "value": "Available after reservations: Alloy {0} / Cells {1}" + }, + { + "key": "base.boostStats", + "value": "Mobility ×{0:0.##} / Mass {1} / Power {2}" + }, + { + "key": "base.moduleStats", + "value": "Mass {0} / Power {1}" + }, + { + "key": "base.boostCompare", + "value": "Compared to current: mobility {0:+0.##;-0.##;0}, mass {1:+0.##;-0.##;0}, power {2:+0.##;-0.##;0}" + }, + { + "key": "base.moduleCompare", + "value": "Compared to current: mass {0:+0.##;-0.##;0}, power {1:+0.##;-0.##;0}" + }, + { + "key": "base.cancel", + "value": "CANCEL" + }, + { + "key": "hangar.equipment", + "value": "EQUIPMENT" + }, + { + "key": "hangar.carry", + "value": "CARRIED ITEMS" + }, + { + "key": "hangar.draft", + "value": "Preparation · items transfer when deploying" + }, + { + "key": "hangar.readOnly", + "value": "Active sortie · actual loadout / read only" + }, + { + "key": "hangar.owned", + "value": "OWNED" + }, + { + "key": "hangar.emptySlot", + "value": "EMPTY" + }, + { + "key": "hangar.reserved", + "value": "RESERVED" + }, + { + "key": "hangar.capacity", + "value": "{0} / {1} cells" + }, + { + "key": "hangar.protected", + "value": "PROTECTED · retained if the mech is destroyed" + }, + { + "key": "hangar.details", + "value": "Selected item / actions" + } + ] +} diff --git a/SourceAssets/Localization/Tables/zh-Hans.json b/SourceAssets/Localization/Tables/zh-Hans.json new file mode 100644 index 0000000..e850d14 --- /dev/null +++ b/SourceAssets/Localization/Tables/zh-Hans.json @@ -0,0 +1,1316 @@ +{ + "entries": [ + { + "key": "range.choose", + "value": "左侧打靶,右侧计时穿门。越过靶区可练习回收。" + }, + { + "key": "range.start", + "value": "01 / 计时起点" + }, + { + "key": "range.upper", + "value": "02 / 高台门" + }, + { + "key": "range.landing", + "value": "在标线内停稳" + }, + { + "key": "range.gallery", + "value": "射击训练" + }, + { + "key": "range.salvage", + "value": "回收练习区" + }, + { + "key": "range.material", + "value": "材料示范箱" + }, + { + "key": "range.parts", + "value": "试装部件箱" + }, + { + "key": "range.targetGuide", + "value": "射击翻板和横移靶。已命中 {0} 次。到复位台可重练。" + }, + { + "key": "range.brake", + "value": "在高台尽头的标线内停稳。" + }, + { + "key": "range.finish", + "value": "本轮用时 {0:F1} 秒。再次穿过起点可重练。" + }, + { + "key": "range.recoveryGuide", + "value": "按 [{0}] 搜索箱子,按 [{1}] 选择货物,原地按住 [{2}] 发射。" + }, + { + "key": "range.recoverySent", + "value": "货舱已发射。前往橙色平台按 [{0}] 呼叫运输机。" + }, + { + "key": "range.board", + "value": "运输机已抵达。前往橙色平台按 [{0}] 登机。" + }, + { + "key": "range.reset", + "value": "重置练习 [{0}]" + }, + { + "key": "range.gate", + "value": "已通过起点,沿标线前进。" + }, + { + "key": "range.hit", + "value": "靶标命中 {0} 次" + }, + { + "key": "range.ready", + "value": "训练设备已复位。" + }, + { + "key": "ui.home.subtitle", + "value": "机甲回收行动" + }, + { + "key": "ui.home.loadout", + "value": "出击配装" + }, + { + "key": "ui.slot.0", + "value": "主武器" + }, + { + "key": "ui.slot.1", + "value": "副武装" + }, + { + "key": "ui.slot.2", + "value": "推进器" + }, + { + "key": "ui.slot.3", + "value": "防护模块" + }, + { + "key": "ui.home.part", + "value": "{0} / {1} [载重 {2} / 功率 {3}]" + }, + { + "key": "ui.home.budget", + "value": "载重 {0} / {1} 功率 {2} / {3}" + }, + { + "key": "ui.home.deploy", + "value": "开始出击" + }, + { + "key": "ui.home.range", + "value": "驾驶试验场" + }, + { + "key": "ui.home.resume", + "value": "继续出击" + }, + { + "key": "ui.home.locked", + "value": "所选装备库存不足。基础槽位可借用救济装,试验场可试用所有型号。" + }, + { + "key": "ui.home.moveHint", + "value": "{0} 移动 / 鼠标瞄准 / {1} 切换视角 / {2} 推进 / {3} 跳跃与升空" + }, + { + "key": "ui.home.fireHint", + "value": "{0} 主武器 / {1} 精瞄 / {2} 副武装 / {3} 护盾 / {4} 交互" + }, + { + "key": "ui.home.inventory", + "value": "机库库存" + }, + { + "key": "ui.home.stock", + "value": "合金 {0} 电芯 {1} 蓝图 {2} 撤离 {3} 终局达成 {4}" + }, + { + "key": "ui.home.researched", + "value": "{0} / 已研究" + }, + { + "key": "ui.home.research", + "value": "{0} / 研究:2 合金 + 1 电芯" + }, + { + "key": "ui.home.quit", + "value": "退出游戏" + }, + { + "key": "ui.home.language", + "value": "语言 / Language · {0}" + }, + { + "key": "ui.home.languageHelp", + "value": "选择语言" + }, + { + "key": "ui.language.current", + "value": "{0} / 当前语言" + }, + { + "key": "ui.hud.operation", + "value": "当前任务" + }, + { + "key": "ui.hud.armor", + "value": "装甲 {0}" + }, + { + "key": "ui.hud.energy", + "value": "能源 {0}" + }, + { + "key": "ui.hud.shield", + "value": "护盾生效" + }, + { + "key": "ui.hud.podUsed", + "value": "回收舱已用 / 1 次机会" + }, + { + "key": "ui.hud.podPreparing", + "value": "正在准备回收舱…" + }, + { + "key": "ui.hud.podReady", + "value": "回收舱就绪 / {0} 选择货物" + }, + { + "key": "ui.hud.hint", + "value": "{0} 视角 {1} 推进 {2} 升空 {3} 副武装 {4} 护盾 {5} 交互 {6} 货仓 按住 {7} 发射回收舱 {8} 暂停" + }, + { + "key": "ui.hud.zone", + "value": "战区 {0} / 5" + }, + { + "key": "ui.hud.status", + "value": "{0} {1}" + }, + { + "key": "ui.hud.complete", + "value": "任务完成 / 携带货物前往撤离点" + }, + { + "key": "ui.hud.boss", + "value": "击败督军" + }, + { + "key": "ui.hud.escort", + "value": "护送工程履带车抵达远端闸门" + }, + { + "key": "ui.hud.routeOpen", + "value": "路线已开放 / 搜索、撤离或前往下一站" + }, + { + "key": "ui.hud.objective", + "value": "前往中继站回收导航数据" + }, + { + "key": "ui.hud.cockpit", + "value": "第一人称" + }, + { + "key": "ui.hud.shoulder", + "value": "越肩视角" + }, + { + "key": "ui.hud.telemetry", + "value": "{0}\n{1} / {2}\n货仓 {3} / 16\n{4}" + }, + { + "key": "ui.hud.transportReady", + "value": "运输机已到达 / 前往平台登机" + }, + { + "key": "ui.hud.transportEta", + "value": "运输机抵达倒计时 {0} 秒" + }, + { + "key": "ui.hud.extractionAvailable", + "value": "可随时撤离 / 橙色平台" + }, + { + "key": "ui.cargo.title", + "value": "货仓与弹射式安全箱" + }, + { + "key": "ui.cargo.used", + "value": "主动货舱已发射。机毁时只有弹射式安全箱中的物品返航。" + }, + { + "key": "ui.cargo.help", + "value": "拖动物品或选中后点击目标格,按 R 旋转。安全箱在机毁时自动返航。标记货物后关闭面板,原地按住 {0} 三秒发射主动货舱。" + }, + { + "key": "ui.cargo.stack", + "value": "{0} {1} ×{2}" + }, + { + "key": "ui.cargo.selected", + "value": "[已选择]" + }, + { + "key": "ui.cargo.capacity", + "value": "普通货仓 {0}/16 格 主动货舱 {1}/4 格" + }, + { + "key": "ui.return", + "value": "返回驾驶" + }, + { + "key": "ui.refit.title", + "value": "战地改装" + }, + { + "key": "ui.refit.help", + "value": "已回收到替换部件。安装前请比较载重和功率。" + }, + { + "key": "ui.refit.equipped", + "value": "当前部件 / {0} 载重 {1} / 功率 {2}" + }, + { + "key": "ui.refit.recovered", + "value": "回收部件 / {0} 载重 {1} / 功率 {2}" + }, + { + "key": "ui.refit.install", + "value": "安装回收部件" + }, + { + "key": "ui.refit.keep", + "value": "保留当前配装" + }, + { + "key": "ui.routes.split", + "value": "选择终局方向" + }, + { + "key": "ui.routes.title", + "value": "选择下一战区" + }, + { + "key": "ui.routes.splitHelp", + "value": "已在两个战区积累装备。选择适合当前机甲的终局任务。" + }, + { + "key": "ui.routes.help", + "value": "可以预知任务与物资类别,具体战利品需进入战区后搜索。" + }, + { + "key": "ui.routes.boss", + "value": "督军路线 / 突击中继站 / 武器物资" + }, + { + "key": "ui.routes.escort", + "value": "工程护送 / 恢复补给线 / 推进器物资" + }, + { + "key": "ui.routes.freight", + "value": "货运回收区 / 回收导航数据 / 合金丰富" + }, + { + "key": "ui.routes.power", + "value": "能源变电站 / 重启中继站 / 电芯丰富" + }, + { + "key": "ui.routes.stay", + "value": "留在当前战区" + }, + { + "key": "ui.pause.title", + "value": "已暂停" + }, + { + "key": "ui.pause.help", + "value": "出击进度会在检查点及每次成功回收后保存。" + }, + { + "key": "ui.pause.resume", + "value": "继续游戏" + }, + { + "key": "ui.pause.range", + "value": "返回机库" + }, + { + "key": "ui.pause.quit", + "value": "保存并退出" + }, + { + "key": "report.ready", + "value": "选择底盘并配置出击装备。" + }, + { + "key": "report.range", + "value": "试验场测试结束。机库出击库存已就绪。" + }, + { + "key": "report.destroyed", + "value": "机体损毁。损失 {0} 件装备或货物,弹射式安全箱送回 {1} 件物品。" + }, + { + "key": "report.complete", + "value": "任务完成 / 随身材料与蓝图已全部入库。" + }, + { + "key": "report.extracted", + "value": "撤离成功。已装装备、普通货仓和安全箱物品已回收。" + }, + { + "key": "notice.unfinished", + "value": "还有未完成的出击,请先继续该次出击。" + }, + { + "key": "notice.budget", + "value": "配装超出底盘的载重或功率上限。" + }, + { + "key": "notice.researchFirst", + "value": "所选装备实物不足,请从仓库选择装备或制造。" + }, + { + "key": "notice.saveFailed", + "value": "保存失败,请重试。" + }, + { + "key": "notice.range", + "value": "驾驶试验场 / {0} 切换视角,{1} 操作附近设备。" + }, + { + "key": "notice.enter", + "value": "搜索战区,完成任务后开放下一站路线。" + }, + { + "key": "notice.podSecured", + "value": "货物回收成功 / 已立即入库,可继续出击。" + }, + { + "key": "notice.cargoUnavailable", + "value": "所选货物已不可用,请重新选择。" + }, + { + "key": "notice.twoSlots", + "value": "所选物品放不进 2×2 主动货舱。" + }, + { + "key": "notice.holdFull", + "value": "随身货仓已满,请发射回收舱或整机撤离。" + }, + { + "key": "notice.recovered", + "value": "已回收 / {0} ×{1}" + }, + { + "key": "notice.partBudget", + "value": "该部件会使配装超出底盘预算。" + }, + { + "key": "notice.refit", + "value": "战地改装完成 / {0}" + }, + { + "key": "notice.routeOpen", + "value": "路线已开放 / 继续搜索、撤离,或前往下一站闸门。" + }, + { + "key": "notice.complete", + "value": "任务完成 / 请携带货物返回撤离平台。" + }, + { + "key": "notice.objectiveFirst", + "value": "请先完成本战区任务,以开放路线。" + }, + { + "key": "notice.rangeComplete", + "value": "试验场任务完成。可以撤离或继续测试机甲。" + }, + { + "key": "notice.extract", + "value": "使用撤离平台将随身货物带回机库。" + }, + { + "key": "notice.researched", + "value": "研究完成:配方已解锁,可消耗材料制造装备。" + }, + { + "key": "notice.researchCost", + "value": "研究需要已回收的蓝图、2 合金和 1 电芯。" + }, + { + "key": "notice.transportInbound", + "value": "运输机正在途中 / 等待时可以自由移动与作战。" + }, + { + "key": "notice.extractionCalled", + "value": "已呼叫撤离 / 等待 15 秒后返回平台登机。" + }, + { + "key": "notice.transportReady", + "value": "接应运输机已抵达 / 返回橙色撤离坪,按 {0} 登机。" + }, + { + "key": "notice.escortLost", + "value": "工程履带车损毁 / 仍可前往撤离点撤离。" + }, + { + "key": "prompt.search", + "value": "{0} 搜索物资" + }, + { + "key": "prompt.relayOnline", + "value": "中继站已上线" + }, + { + "key": "prompt.objective", + "value": "{0} 取回导航数据" + }, + { + "key": "prompt.route", + "value": "{0} 选择下一站路线" + }, + { + "key": "prompt.routeLocked", + "value": "路线未开放 / 请先完成任务" + }, + { + "key": "prompt.board", + "value": "{0} 登机 / 带回随身物资" + }, + { + "key": "prompt.inbound", + "value": "运输机正在途中" + }, + { + "key": "prompt.call", + "value": "{0} 呼叫接应 / 15 秒" + }, + { + "key": "world.extraction", + "value": "撤离点" + }, + { + "key": "world.relay", + "value": "中继站 / 下一站路线" + }, + { + "key": "world.overseer", + "value": "督军" + }, + { + "key": "world.bulwark", + "value": "重装壁垒" + }, + { + "key": "world.skimmer", + "value": "掠行者" + }, + { + "key": "world.sentinel", + "value": "哨兵" + }, + { + "key": "world.escort", + "value": "工程履带车" + }, + { + "key": "chassis.chassis_0.name", + "value": "红隼 / 轻型" + }, + { + "key": "chassis.chassis_0.description", + "value": "空中机动" + }, + { + "key": "chassis.chassis_1.name", + "value": "守卫 / 中型" + }, + { + "key": "chassis.chassis_1.description", + "value": "均衡突击" + }, + { + "key": "chassis.chassis_2.name", + "value": "堡垒 / 重型" + }, + { + "key": "chassis.chassis_2.description", + "value": "重装推进" + }, + { + "key": "equipment.rapid.name", + "value": "冰雹 / 速射炮" + }, + { + "key": "equipment.coil.name", + "value": "长矛 / 电磁步枪" + }, + { + "key": "equipment.siege.name", + "value": "重锤 / 重型火炮" + }, + { + "key": "equipment.guided.name", + "value": "磷火 / 制导齐射" + }, + { + "key": "equipment.pulse.name", + "value": "破阵 / 脉冲爆发" + }, + { + "key": "equipment.efficient.name", + "value": "巡航 / 高效推进器" + }, + { + "key": "equipment.vector.name", + "value": "矢量 / 超载推进器" + }, + { + "key": "equipment.composite_guard.name", + "value": "复合装甲" + }, + { + "key": "equipment.reactive_guard.name", + "value": "反应护盾" + }, + { + "key": "item.alloy", + "value": "合金" + }, + { + "key": "item.cells", + "value": "电芯" + }, + { + "key": "item.blueprint_boost_efficient", + "value": "蓝图 / 高效推进器" + }, + { + "key": "item.blueprint_coil_capacitor", + "value": "蓝图 / 电磁电容" + }, + { + "key": "item.blueprint_reactive_guard", + "value": "蓝图 / 反应护盾" + }, + { + "key": "ui.cargo.hold", + "value": "普通货仓 · 4×4" + }, + { + "key": "ui.cargo.case", + "value": "弹射式安全箱 · 2×2" + }, + { + "key": "ui.cargo.selection", + "value": "已选 {0} {1}×{2} 格 移动数量 {3}" + }, + { + "key": "ui.cargo.rotate", + "value": "旋转 (R)" + }, + { + "key": "ui.cargo.cancel", + "value": "取消选择" + }, + { + "key": "ui.cargo.install", + "value": "安装" + }, + { + "key": "ui.cargo.mark", + "value": "标记发射" + }, + { + "key": "ui.cargo.unmark", + "value": "取消标记" + }, + { + "key": "notice.installFailed", + "value": "无法安装。请检查机体预算和旧件所需的空间。" + }, + { + "key": "notice.placement", + "value": "无法放置。请检查边界、占用和堆叠数量。" + }, + { + "key": "ui.home.chooseItem", + "value": "选用 {0} · {1}" + }, + { + "key": "ui.home.manufacture", + "value": "制造 {0} · 合金 {1} / 电芯 {2}" + }, + { + "key": "settings.title", + "value": "设置" + }, + { + "key": "settings.help", + "value": "点击应用后保存设置,语言切换即时生效。" + }, + { + "key": "settings.language", + "value": "语言" + }, + { + "key": "settings.audio", + "value": "音频" + }, + { + "key": "settings.display", + "value": "画面" + }, + { + "key": "settings.controls", + "value": "操作与按键" + }, + { + "key": "settings.volume", + "value": "总音量" + }, + { + "key": "settings.volumeHelp", + "value": "调节游戏内所有声音的音量,设为 0% 即可静音。" + }, + { + "key": "settings.percent", + "value": "{0}%" + }, + { + "key": "settings.multiplier", + "value": "{0} 倍" + }, + { + "key": "settings.quality", + "value": "画质" + }, + { + "key": "settings.quality.veryLow", + "value": "极低" + }, + { + "key": "settings.quality.low", + "value": "低" + }, + { + "key": "settings.quality.medium", + "value": "中" + }, + { + "key": "settings.quality.high", + "value": "高" + }, + { + "key": "settings.quality.veryHigh", + "value": "极高" + }, + { + "key": "settings.quality.ultra", + "value": "最高" + }, + { + "key": "settings.resolution", + "value": "分辨率" + }, + { + "key": "settings.windowMode", + "value": "显示模式" + }, + { + "key": "settings.windowed", + "value": "窗口" + }, + { + "key": "settings.fullscreen", + "value": "全屏" + }, + { + "key": "settings.displayHelp", + "value": "更改分辨率或显示模式后,需要在 15 秒内确认,否则自动恢复上一次显示设置。" + }, + { + "key": "settings.display.confirm", + "value": "保留当前显示设置?" + }, + { + "key": "settings.display.countdown", + "value": "将在 {0} 秒后自动恢复上一次显示设置。" + }, + { + "key": "settings.display.keep", + "value": "保留并保存" + }, + { + "key": "settings.display.revert", + "value": "恢复原设置" + }, + { + "key": "settings.display.reverted", + "value": "已恢复上一次显示设置,其他修改仍待应用。" + }, + { + "key": "settings.mouse", + "value": "鼠标" + }, + { + "key": "settings.sensitivity", + "value": "鼠标灵敏度" + }, + { + "key": "settings.aimSensitivity", + "value": "精瞄灵敏度倍率" + }, + { + "key": "settings.bindings", + "value": "按键设置" + }, + { + "key": "settings.bind.help", + "value": "点击按键后,按下键盘按键或鼠标按钮进行改绑。Esc 取消改绑,并始终可用于返回菜单;重复按键会提示冲突。" + }, + { + "key": "settings.bind.waiting", + "value": "等待按键" + }, + { + "key": "settings.bind.prompt", + "value": "请为“{0}”按下新的键盘按键或鼠标按钮,按 Esc 取消。" + }, + { + "key": "settings.bind.cancel", + "value": "取消改绑" + }, + { + "key": "settings.bind.changed", + "value": "已更改“{0}”的按键,点击应用后保存。" + }, + { + "key": "settings.bind.cancelled", + "value": "已取消改绑,保留原按键。" + }, + { + "key": "settings.bind.conflict", + "value": "该按键已用于“{0}”,请选择其他按键。" + }, + { + "key": "settings.bind.reserved", + "value": "Esc 用于关闭菜单和取消输入,请选择其他按键。" + }, + { + "key": "settings.bind.unsupported", + "value": "请选择键盘按键或鼠标按钮。" + }, + { + "key": "settings.reset", + "value": "恢复默认" + }, + { + "key": "settings.resetHint", + "value": "已恢复默认设置与按键,点击应用后保存。" + }, + { + "key": "settings.back", + "value": "返回 / 放弃修改" + }, + { + "key": "settings.apply", + "value": "应用并保存" + }, + { + "key": "settings.saved", + "value": "设置已保存。" + }, + { + "key": "settings.saveFailed", + "value": "设置保存失败,请检查磁盘空间与目录写入权限后重试。" + }, + { + "key": "settings.action.move.up", + "value": "向前移动" + }, + { + "key": "settings.action.move.down", + "value": "向后移动" + }, + { + "key": "settings.action.move.left", + "value": "向左移动" + }, + { + "key": "settings.action.move.right", + "value": "向右移动" + }, + { + "key": "settings.action.fire", + "value": "主武器" + }, + { + "key": "settings.action.aim", + "value": "精瞄" + }, + { + "key": "settings.action.boost", + "value": "推进" + }, + { + "key": "settings.action.jump", + "value": "跳跃 / 升空" + }, + { + "key": "settings.action.view", + "value": "切换视角" + }, + { + "key": "settings.action.interact", + "value": "交互" + }, + { + "key": "settings.action.inventory", + "value": "打开货仓" + }, + { + "key": "settings.action.cargo", + "value": "发射回收舱" + }, + { + "key": "settings.action.pause", + "value": "暂停" + }, + { + "key": "settings.action.secondary", + "value": "副武装" + }, + { + "key": "settings.action.guard", + "value": "主动防护" + }, + { + "key": "settings.key.mouseLeft", + "value": "鼠标左键" + }, + { + "key": "settings.key.mouseRight", + "value": "鼠标右键" + }, + { + "key": "settings.key.mouseMiddle", + "value": "鼠标中键" + }, + { + "key": "settings.key.mouseForward", + "value": "鼠标前侧键" + }, + { + "key": "settings.key.mouseBack", + "value": "鼠标后侧键" + }, + { + "key": "settings.key.space", + "value": "空格" + }, + { + "key": "settings.key.leftShift", + "value": "左 Shift" + }, + { + "key": "settings.key.rightShift", + "value": "右 Shift" + }, + { + "key": "settings.action.reload", + "value": "换弹" + }, + { + "key": "ui.combat.nav", + "value": "区域地图" + }, + { + "key": "ui.combat.legend", + "value": "◇ 目标 ○ 撤离点" + }, + { + "key": "ui.combat.ammo", + "value": "弹匣 / 无限备弹" + }, + { + "key": "ui.combat.reload", + "value": "[{0}] 换弹" + }, + { + "key": "ui.combat.loading", + "value": "换弹中 {0:0.0} 秒" + }, + { + "key": "ui.combat.ready", + "value": "就绪" + }, + { + "key": "ui.combat.charging", + "value": "冷却 {0:0.0} 秒" + }, + { + "key": "ui.combat.lowEnergy", + "value": "能源不足" + }, + { + "key": "ui.combat.cargo", + "value": "普通货仓 {0}/16 格 · 安全箱 {1}/4 格" + }, + { + "key": "ui.combat.empty", + "value": "未安装" + }, + { + "key": "range.resetSign", + "value": "训练复位台" + }, + { + "key": "base.home.title", + "value": "前沿基地 / 机甲待命" + }, + { + "key": "base.navigation", + "value": "基地终端" + }, + { + "key": "base.enterHangar", + "value": "进入机库" + }, + { + "key": "base.deploy", + "value": "作战部署" + }, + { + "key": "base.hangar", + "value": "机库" + }, + { + "key": "base.backHome", + "value": "返回主页" + }, + { + "key": "base.home.description", + "value": "整备机体、整理战利品,然后决定下一次出击。" + }, + { + "key": "base.baseStatus", + "value": "基地资源" + }, + { + "key": "base.recipes", + "value": "已研究配方" + }, + { + "key": "base.record", + "value": "出击 {0} / 撤离 {1} / 目标达成 {2}" + }, + { + "key": "base.activeRun", + "value": "存在未结束出击。基地库存当前只读,请继续出击后返航整理。" + }, + { + "key": "base.menuHint", + "value": "鼠标选择 · Tab 切换焦点 · Enter 确认 · Esc 返回" + }, + { + "key": "base.tab.loadout", + "value": "机体整备" + }, + { + "key": "base.tab.warehouse", + "value": "基地仓库" + }, + { + "key": "base.tab.research", + "value": "研究制造" + }, + { + "key": "base.preview", + "value": "机体预览" + }, + { + "key": "base.previewHint", + "value": "在预览空白区域拖动可旋转机体。" + }, + { + "key": "base.chassisStats", + "value": "装甲 {0} / 巡航 {1} m/s / 推进 {2} m/s" + }, + { + "key": "base.chassis", + "value": "底盘" + }, + { + "key": "base.installed", + "value": "装备槽" + }, + { + "key": "base.borrowed", + "value": "借用,返航归还" + }, + { + "key": "base.partStats", + "value": "伤害 {0} / 载重 {1} / 功率 {2}" + }, + { + "key": "base.compare", + "value": "相对当前装备:伤害 {0:+0.##;-0.##;0} 载重 {1:+0.##;-0.##;0} 功率 {2:+0.##;-0.##;0}" + }, + { + "key": "base.useBorrowed", + "value": "改用基础借用件" + }, + { + "key": "base.preparation", + "value": "随行预装" + }, + { + "key": "base.cargo", + "value": "普通货仓" + }, + { + "key": "base.safeCase", + "value": "安全箱" + }, + { + "key": "base.reserved", + "value": "已整备" + }, + { + "key": "base.researchHelp", + "value": "蓝图作为实物保留。研究永久解锁配方,制造消耗仓库内未预装的材料并生成实物。" + }, + { + "key": "base.researchCost", + "value": "研究:2 合金 + 1 电芯" + }, + { + "key": "base.manufacture", + "value": "制造一件" + }, + { + "key": "base.deployHelp", + "value": "确认出击后,整备装备和预装货物将从仓库携出。机毁保全安全箱,箱外装备与物资损失。借用件返基地归还。" + }, + { + "key": "base.confirm", + "value": "确认" + }, + { + "key": "warehouse.title", + "value": "基地仓库" + }, + { + "key": "warehouse.pending", + "value": "待整理 {0}" + }, + { + "key": "warehouse.occupancy", + "value": "占用 {0} / {1} 格" + }, + { + "key": "warehouse.stock", + "value": "合金 {0} / 电芯 {1}" + }, + { + "key": "warehouse.controls", + "value": "拖放移动 · R 旋转预览 · Ctrl+单击预装/取消 · Shift+拖动拆分" + }, + { + "key": "warehouse.filter.all", + "value": "全部" + }, + { + "key": "warehouse.filter.equipment", + "value": "装备" + }, + { + "key": "warehouse.filter.material", + "value": "材料" + }, + { + "key": "warehouse.filter.blueprint", + "value": "蓝图" + }, + { + "key": "warehouse.search", + "value": "查找" + }, + { + "key": "warehouse.sort", + "value": "整理" + }, + { + "key": "warehouse.claim", + "value": "入仓" + }, + { + "key": "warehouse.claimAll", + "value": "一键尝试入仓" + }, + { + "key": "warehouse.inboxHelp", + "value": "回收物资已保存。腾出仓库空间后领取,清空待整理区才能开启新的正式出击。" + }, + { + "key": "warehouse.emptyInbox", + "value": "暂无待整理物资" + }, + { + "key": "warehouse.selectHelp", + "value": "选择物品查看详情;选择后点击空格可摆放。预装只预占,确认出击才携出。" + }, + { + "key": "warehouse.itemInfo", + "value": "尺寸 {0}×{1} / 数量 {2} / 编号 {3}" + }, + { + "key": "warehouse.equip", + "value": "装备" + }, + { + "key": "warehouse.rotate", + "value": "旋转预览" + }, + { + "key": "warehouse.unstage", + "value": "取消预装" + }, + { + "key": "warehouse.recycle", + "value": "蓝图换取 2 合金" + }, + { + "key": "warehouse.discard", + "value": "弃置" + }, + { + "key": "warehouse.discardConfirm", + "value": "确定永久弃置「{0}」×{1}?此操作会释放占用空间。" + }, + { + "key": "warehouse.sortFirst", + "value": "请先整理或弃置待整理物资,再开启新的正式出击。" + }, + { + "key": "warehouse.incompatible", + "value": "物品与装备槽不兼容。" + }, + { + "key": "warehouse.noSpace", + "value": "没有合适的连续空位;物品和材料已保留。" + }, + { + "key": "warehouse.missing", + "value": "所选实物已不存在,请重新选择装备。" + }, + { + "key": "warehouse.alreadyEquipped", + "value": "此物品已预占装备槽。请先改用其他装备或借用件。" + }, + { + "key": "warehouse.manufactureFailed", + "value": "无法制造:请检查已研究配方、未预装材料及产物所需空位。" + }, + { + "key": "warehouse.saved", + "value": "操作已保存。" + }, + { + "key": "warehouse.split", + "value": "拆分材料" + }, + { + "key": "warehouse.splitHelp", + "value": "选择本次移动的数量;其余材料保留在原堆叠。" + }, + { + "key": "warehouse.quantity", + "value": "数量" + }, + { + "key": "warehouse.recoveredPending", + "value": "货舱物资已回收,其中 {0} 件待返航后整理入仓。" + }, + { + "key": "warehouse.available", + "value": "扣除预装后可用:合金 {0} / 电芯 {1}" + }, + { + "key": "base.boostStats", + "value": "机动倍率 {0:0.##} / 载重 {1} / 功率 {2}" + }, + { + "key": "base.moduleStats", + "value": "载重 {0} / 功率 {1}" + }, + { + "key": "base.boostCompare", + "value": "相对当前:机动倍率 {0:+0.##;-0.##;0} / 载重 {1:+0.##;-0.##;0} / 功率 {2:+0.##;-0.##;0}" + }, + { + "key": "base.moduleCompare", + "value": "相对当前:载重 {0:+0.##;-0.##;0} / 功率 {1:+0.##;-0.##;0}" + }, + { + "key": "base.cancel", + "value": "取消" + }, + { + "key": "hangar.equipment", + "value": "机体装备" + }, + { + "key": "hangar.carry", + "value": "携带物品" + }, + { + "key": "hangar.draft", + "value": "整备草案 · 确认出击时转移实物" + }, + { + "key": "hangar.readOnly", + "value": "出击进行中 · 当前实物装备 / 只读" + }, + { + "key": "hangar.owned", + "value": "自有装备" + }, + { + "key": "hangar.emptySlot", + "value": "空槽" + }, + { + "key": "hangar.reserved", + "value": "预占" + }, + { + "key": "hangar.capacity", + "value": "{0} / {1} 格" + }, + { + "key": "hangar.protected", + "value": "机毁保全 · 机体损毁后仍可带回" + }, + { + "key": "hangar.details", + "value": "选中物品 / 操作" + } + ] +} diff --git a/SourceAssets/ThirdParty/Adobe/NotoSansSC/NotoSansSC-VF.ttf b/SourceAssets/ThirdParty/Adobe/NotoSansSC/NotoSansSC-VF.ttf new file mode 100644 index 0000000..cc79aef Binary files /dev/null and b/SourceAssets/ThirdParty/Adobe/NotoSansSC/NotoSansSC-VF.ttf differ diff --git a/SourceAssets/ThirdParty/Adobe/NotoSansSC/OFL.txt b/SourceAssets/ThirdParty/Adobe/NotoSansSC/OFL.txt new file mode 100644 index 0000000..83c2cf3 --- /dev/null +++ b/SourceAssets/ThirdParty/Adobe/NotoSansSC/OFL.txt @@ -0,0 +1,58 @@ +© 2014-2021 Adobe (http://www.adobe.com/), with Reserved Font Name 'Source'. + +SIL Open Font License 1.1 +========================= + +PREAMBLE + +The goals of the Open Font License (OFL) are to stimulate worldwide development of collaborative font projects, to support the font creation efforts of academic and linguistic communities, and to provide a free and open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and redistributed freely as long as they are not sold by themselves. The fonts, including any derivative works, can be bundled, embedded, redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, however, cannot be released under any other type of license. The requirement for fonts to remain under this license does not apply to any document created using the fonts or their derivatives. + +DEFINITIONS +“Font Software” refers to the set of files released by the Copyright Holder(s) under this license and clearly marked as such. This may include source files, build scripts and documentation. + +“Reserved Font Name” refers to any names specified as such after the copyright statement(s). + +“Original Version” refers to the collection of Font Software components as distributed by the Copyright Holder(s). + +“Modified Version” refers to any derivative made by adding to, deleting, or substituting – in part or in whole – any of the components of the Original Version, by changing formats or by porting the Font Software to a new environment. + +“Author” refers to any designer, engineer, programmer, technical writer or other person who contributed to the Font Software. + + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining a copy of the Font Software, to use, study, copy, merge, embed, modify, redistribute, and sell modified and unmodified copies of the Font Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, redistributed and/or sold with any software, provided that each copy contains the above copyright notice and this license. These can be included either as stand-alone text files, human-readable headers or in the appropriate machine-readable metadata fields within text or binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font Name(s) unless explicit written permission is granted by the corresponding Copyright Holder. This restriction only applies to the primary font name as presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font Software shall not be used to promote, endorse or advertise any Modified Version, except to acknowledge the contribution(s) of the Copyright Holder(s) and the Author(s) or with their explicit written permission. + +5) The Font Software, modified or unmodified, in part or in whole, must be distributed entirely under this license, and must not be distributed under any other license. The requirement for fonts to remain under this license does not apply to any document created using the Font Software. + +TERMINATION + +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER + +THE FONT SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. + + +© 2003-2009 SIL International, all rights reserved, unless otherwise noted elsewhere on this page. +Provided by SIL’s Non-Roman Script Initiative. Contact us at nrsi@sil.org. \ No newline at end of file diff --git a/SourceAssets/ThirdParty/fonttools/4.55.3/fonttools-4.55.3-cp312-cp312-win_amd64.whl b/SourceAssets/ThirdParty/fonttools/4.55.3/fonttools-4.55.3-cp312-cp312-win_amd64.whl new file mode 100644 index 0000000..42369c8 Binary files /dev/null and b/SourceAssets/ThirdParty/fonttools/4.55.3/fonttools-4.55.3-cp312-cp312-win_amd64.whl differ diff --git a/SourceAssets/ThirdParty/resvg-py/0.5.0/resvg_py-0.5.0-cp310-abi3-win_amd64.whl b/SourceAssets/ThirdParty/resvg-py/0.5.0/resvg_py-0.5.0-cp310-abi3-win_amd64.whl new file mode 100644 index 0000000..3ac8d76 Binary files /dev/null and b/SourceAssets/ThirdParty/resvg-py/0.5.0/resvg_py-0.5.0-cp310-abi3-win_amd64.whl differ diff --git a/SourceAssets/UI/Icons/Equipment/coil.svg b/SourceAssets/UI/Icons/Equipment/coil.svg new file mode 100644 index 0000000..d5145a6 --- /dev/null +++ b/SourceAssets/UI/Icons/Equipment/coil.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/SourceAssets/UI/Icons/Equipment/composite_guard.svg b/SourceAssets/UI/Icons/Equipment/composite_guard.svg new file mode 100644 index 0000000..9a3fc35 --- /dev/null +++ b/SourceAssets/UI/Icons/Equipment/composite_guard.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/SourceAssets/UI/Icons/Equipment/efficient.svg b/SourceAssets/UI/Icons/Equipment/efficient.svg new file mode 100644 index 0000000..82d6dc4 --- /dev/null +++ b/SourceAssets/UI/Icons/Equipment/efficient.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/SourceAssets/UI/Icons/Equipment/guided.svg b/SourceAssets/UI/Icons/Equipment/guided.svg new file mode 100644 index 0000000..eb0f1b8 --- /dev/null +++ b/SourceAssets/UI/Icons/Equipment/guided.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/SourceAssets/UI/Icons/Equipment/pulse.svg b/SourceAssets/UI/Icons/Equipment/pulse.svg new file mode 100644 index 0000000..0dcbefb --- /dev/null +++ b/SourceAssets/UI/Icons/Equipment/pulse.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/SourceAssets/UI/Icons/Equipment/rapid.svg b/SourceAssets/UI/Icons/Equipment/rapid.svg new file mode 100644 index 0000000..d967310 --- /dev/null +++ b/SourceAssets/UI/Icons/Equipment/rapid.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/SourceAssets/UI/Icons/Equipment/reactive_guard.svg b/SourceAssets/UI/Icons/Equipment/reactive_guard.svg new file mode 100644 index 0000000..e20e977 --- /dev/null +++ b/SourceAssets/UI/Icons/Equipment/reactive_guard.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/SourceAssets/UI/Icons/Equipment/siege.svg b/SourceAssets/UI/Icons/Equipment/siege.svg new file mode 100644 index 0000000..e35c0ef --- /dev/null +++ b/SourceAssets/UI/Icons/Equipment/siege.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/SourceAssets/UI/Icons/Equipment/vector.svg b/SourceAssets/UI/Icons/Equipment/vector.svg new file mode 100644 index 0000000..5598351 --- /dev/null +++ b/SourceAssets/UI/Icons/Equipment/vector.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/SourceAssets/UI/Illustrations/MechFrontSilhouette.svg b/SourceAssets/UI/Illustrations/MechFrontSilhouette.svg new file mode 100644 index 0000000..e8564df --- /dev/null +++ b/SourceAssets/UI/Illustrations/MechFrontSilhouette.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/Tools/Unreal/Build.ps1 b/Tools/Unreal/Build.ps1 new file mode 100644 index 0000000..1d80628 --- /dev/null +++ b/Tools/Unreal/Build.ps1 @@ -0,0 +1,11 @@ +param([ValidateSet('Editor','Game','Package')][string]$Mode='Editor',[string]$EngineRoot='C:/Program Files/Epic Games/UE_5.8') +$ErrorActionPreference='Stop' +$projectRoot=(Resolve-Path (Join-Path $PSScriptRoot '../..')).Path +$projectFile=Join-Path $projectRoot 'ueRouge.uproject' +if($Mode -eq 'Package'){ + & (Join-Path $EngineRoot 'Engine/Build/BatchFiles/RunUAT.bat') BuildCookRun -project="$projectFile" -noP4 -platform=Win64 -clientconfig=Development -build -cook -stage -pak -archive -archivedirectory="$projectRoot/Builds/Windows/x64" -utf8output -unattended +}else{ + $targetName=if($Mode -eq 'Editor'){'ueRougeEditor'}else{'ueRouge'} + & (Join-Path $EngineRoot 'Engine/Build/BatchFiles/Build.bat') $targetName Win64 Development "$projectFile" -WaitMutex -NoHotReloadFromIDE -NoUBTMakefiles -UTF8Output +} +if($LASTEXITCODE -ne 0){throw "UE $Mode build failed ($LASTEXITCODE)"} diff --git a/Tools/Unreal/CheckMcpReadiness.py b/Tools/Unreal/CheckMcpReadiness.py new file mode 100644 index 0000000..ad70cee --- /dev/null +++ b/Tools/Unreal/CheckMcpReadiness.py @@ -0,0 +1,177 @@ +"""Read-only, project-bound UE MCP probe using its own short-lived session.""" +import asyncio +import base64 +import json +import logging +import re +import struct +import subprocess +import sys +from datetime import datetime, timedelta, timezone +from pathlib import Path + +from mcp import ClientSession +from mcp.client.streamable_http import streamablehttp_client +from mcp.types import Implementation + +ROOT = Path(__file__).resolve().parents[2] +PROJECT = ROOT / "ueRouge.uproject" +ENDPOINT = "http://127.0.0.1:8000/mcp" +ZONE = timezone(timedelta(hours=8)) +CAPABILITIES = { + "scene": ("editor_toolset.toolsets.scene.SceneTools", ["get_current_level", "load_level"]), + "assets": ("editor_toolset.toolsets.asset.AssetTools", ["find_assets", "save_assets", "is_dirty"]), + "static_mesh_import": ("editor_toolset.toolsets.static_mesh.StaticMeshTools", ["import_file", "get_bounds"]), + "skeletal_mesh_import": ("editor_toolset.toolsets.skeletal_mesh.SkeletalMeshTools", ["import_file", "get_socket_names"]), + "texture_import": ("editor_toolset.toolsets.texture.TextureTools", ["import_file"]), + "blueprint": ("editor_toolset.toolsets.blueprint.BlueprintTools", ["create", "compile_blueprint"]), + "umg": ("UMGToolSet.UMGToolSet", ["CreateWidgetBlueprint", "AddWidget", "CompileWidgetBlueprint"]), + "data_assets": ("editor_toolset.toolsets.data_asset.DataAssetTools", ["create"]), + "automation": ("AutomationTestToolset.AutomationTestToolset", ["DiscoverTests", "RunTests", "GetTestResults"]), + "editor": ("EditorToolset.EditorAppToolset", ["IsPIERunning", "StartPIE", "StopPIE", "GetCameraTransform", "CaptureViewport"]), +} + + +def redact(text): + text = re.sub(r"session id '[^']+'", "session id ''", text) + text = re.sub(r"(?i)(password|token|api_key|密码)(\s*[:=]\s*)[^,\s\"}]+", r"\1\2", text) + return re.sub(r"https?://[^/\s:@]+:[^/\s@]+@", "", text) + + +def verify_owner(): + command = r""" +$listener = Get-NetTCPConnection -LocalPort 8000 -State Listen -ErrorAction Stop | Where-Object LocalAddress -eq '127.0.0.1' | Select-Object -First 1 +if (-not $listener) { throw 'No loopback listener on port 8000' } +$process = Get-CimInstance Win32_Process -Filter "ProcessId = $($listener.OwningProcess)" +$matches = [regex]::Matches($process.CommandLine, '(?i)"([^"\r\n]+\.uproject)"|([A-Z]:[\\/][^\s"]+\.uproject)') +$projects = @($matches | ForEach-Object { if ($_.Groups[1].Success) { $_.Groups[1].Value } else { $_.Groups[2].Value } }) +[pscustomobject]@{ processId = $process.ProcessId; processName = $process.Name; executablePath = $process.ExecutablePath; projectArguments = $projects; port = $listener.LocalPort } | ConvertTo-Json -Compress +""" + result = subprocess.run(["powershell.exe", "-NoProfile", "-Command", command], capture_output=True, check=True) + owner = json.loads(result.stdout.decode("utf-8-sig")) + projects = owner["projectArguments"] + if isinstance(projects, str): + projects = [projects] + if owner["processName"].casefold() != "unrealeditor.exe" or len(projects) != 1 or Path(projects[0]).resolve() != PROJECT.resolve(): + raise RuntimeError("Port 8000 does not belong to the exact target project") + version_path = Path(owner["executablePath"]).parents[2] / "Build/Build.version" + version = json.loads(version_path.read_text(encoding="utf-8-sig")) + owner["engineVersion"] = ".".join(str(version[k]) for k in ("MajorVersion", "MinorVersion", "PatchVersion")) + owner["projectFile"] = str(PROJECT).replace("\\", "/") + return owner + + +def unpack(result): + if result.isError: + raise RuntimeError("MCP call returned an error; raw payload omitted") + if result.structuredContent is not None: + return result.structuredContent + strings = [block.text for block in result.content if block.type == "text"] + text = "\n".join(strings) + try: + return json.loads(text) + except json.JSONDecodeError: + return {"text": text} + + +class Warnings(logging.Handler): + def __init__(self, output): + super().__init__(logging.WARNING) + self.output = output + + def emit(self, record): + self.output.append(redact(record.getMessage())) + + +async def check(): + owner = verify_owner() + now = datetime.now(ZONE) + folder = ROOT / "Docs/QA/GameFrameworkMcpReadiness" / now.strftime("%Y-%m-%d-%H%M%S") + folder.mkdir(parents=True, exist_ok=False) + warnings = [] + logger = logging.getLogger("mcp.client.streamable_http") + handler = Warnings(warnings) + old_propagate = logger.propagate + logger.propagate = False + logger.addHandler(handler) + report = {"checkedAt": now.isoformat(), "endpoint": ENDPOINT, "project": owner, + "bindingVerification": "Loopback listener PID and exact .uproject process argument before and after native MCP calls", + "readOnly": True, "capabilities": {}, "ueBuildExecuted": False, + "gameFrameworkBuildExecuted": False, "packagedGameExecuted": False} + try: + async with streamablehttp_client(ENDPOINT, timeout=15, sse_read_timeout=30, terminate_on_close=True) as (reader, writer, _): + async with ClientSession(reader, writer, client_info=Implementation(name="irons-gameFramework-readiness", version="1.0")) as session: + init = await session.initialize() + report["protocolVersion"] = init.protocolVersion + tools = await session.list_tools() + report["routerTools"] = [tool.name for tool in tools.tools] + discovered = unpack(await session.call_tool("list_toolsets", {})) + text = discovered.get("text", "") + report["toolsets"] = re.findall(r"^- ([^:\n]+):", text, re.M) + report["toolsetCount"] = len(report["toolsets"]) + + async def call(toolset, name, arguments=None): + result = await session.call_tool("call_tool", {"toolset_name": toolset, "tool_name": name, "arguments": arguments or {}}) + return unpack(result) + + report["pieBefore"] = (await call("EditorToolset.EditorAppToolset", "IsPIERunning"))["returnValue"] + report["activeLevel"] = (await call("editor_toolset.toolsets.scene.SceneTools", "get_current_level"))["returnValue"] + for label, (toolset, expected) in CAPABILITIES.items(): + schema = unpack(await session.call_tool("describe_toolset", {"toolset_name": toolset})) + names = [tool["name"].rsplit(".", 1)[-1] for tool in schema["tools"]] + report["capabilities"][label] = {"toolset": toolset, "requiredTools": expected, + "allAdvertised": all(name in names for name in expected), + "verification": "Schema discovery; no asset mutations or test execution"} + log_args = {"category": "", "pattern": "(?i)(error|warning)", "maxEntries": 20} + entries = (await call("EditorToolset.LogsToolset", "GetLogEntries", log_args))["returnValue"] + report["recentMatchingLogEntries"] = [redact(entry) for entry in entries] + report["logScope"] = "Last 20 matching entries from this editor session, including entries predating this check; not a zero-error certification" + + # This UE build advertises optional struct parameters, but its + # RPC binder requires explicit values. Reuse the observed camera. + camera = (await call("EditorToolset.EditorAppToolset", "GetCameraTransform"))["returnValue"] + capture_arguments = {"captureTransform": camera, "bShowUI": False, + "annotations": {"gridSpacing": 0, "gridExtent": 0, "gridHeight": 0, + "maxLabelDistance": 0, "maxLabels": 0, + "classFilter": {"refPath": "/Script/Engine.Actor"}}} + capture_result = await session.call_tool("call_tool", {"toolset_name": "EditorToolset.EditorAppToolset", "tool_name": "CaptureViewport", "arguments": capture_arguments}) + capture = unpack(capture_result).get("returnValue", {}) + image = capture.get("image", {}) + encoded = image.get("data") + if not encoded: + encoded = next((block.data for block in capture_result.content if block.type == "image"), None) + if not encoded: + raise RuntimeError("Viewport capture returned no image") + pixels = base64.b64decode(encoded) + if not pixels.startswith(b"\x89PNG\r\n\x1a\n"): + raise RuntimeError("Viewport capture is not PNG") + width, height = struct.unpack(">II", pixels[16:24]) + (folder / "editor-viewport.png").write_bytes(pixels) + report["screenshot"] = {"file": "editor-viewport.png", "width": width, "height": height, + "environment": "UE editor level viewport", "level": report["activeLevel"]} + report["viewportCaptureBindingNote"] = "Explicit observed camera and disabled annotation values supplied because this build rejects omitted optional struct parameters" + report["pieAfter"] = (await call("EditorToolset.EditorAppToolset", "IsPIERunning"))["returnValue"] + report["activeLevelAfter"] = (await call("editor_toolset.toolsets.scene.SceneTools", "get_current_level"))["returnValue"] + if report["pieBefore"] != report["pieAfter"] or report["activeLevel"] != report["activeLevelAfter"]: + raise RuntimeError("Editor state changed during the check") + after = verify_owner() + if after["processId"] != owner["processId"]: + raise RuntimeError("Editor process changed during the check") + report["nativeQueriesSucceeded"] = True + report["clientSessionClosed"] = True + report["sharedServicePreserved"] = True + finally: + logger.removeHandler(handler) + logger.propagate = old_propagate + report["clientWarnings"] = warnings + report["serverSessionDeletionIndependentlyVerified"] = False + (folder / "verification.json").write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") + print(json.dumps({"recordDirectory": str(folder), "engineVersion": owner["engineVersion"], + "nativeQueriesSucceeded": report["nativeQueriesSucceeded"], "toolsetCount": report["toolsetCount"], + "pie": report["pieAfter"], "level": report["activeLevel"], "screenshot": report["screenshot"], + "clientWarnings": warnings, "sharedServicePreserved": True}, ensure_ascii=False)) + + +if __name__ == "__main__": + sys.stdout.reconfigure(encoding="utf-8") + asyncio.run(check()) diff --git a/Tools/Unreal/Commands/Bootstrap.json b/Tools/Unreal/Commands/Bootstrap.json new file mode 100644 index 0000000..d5cc7c7 --- /dev/null +++ b/Tools/Unreal/Commands/Bootstrap.json @@ -0,0 +1,6 @@ +[ + {"toolset_name":"IronsEditor.IronsMigrationToolset","tool_name":"Snapshot","arguments":{}}, + {"toolset_name":"IronsEditor.IronsMigrationToolset","tool_name":"ImportVisuals","arguments":{}}, + {"toolset_name":"IronsEditor.IronsMigrationToolset","tool_name":"Bootstrap","arguments":{}}, + {"toolset_name":"IronsEditor.IronsMigrationToolset","tool_name":"Snapshot","arguments":{}} +] diff --git a/Tools/Unreal/Commands/Finish.json b/Tools/Unreal/Commands/Finish.json new file mode 100644 index 0000000..a0adf9d --- /dev/null +++ b/Tools/Unreal/Commands/Finish.json @@ -0,0 +1,5 @@ +[ + {"toolset_name":"EditorToolset.EditorAppToolset","tool_name":"IsPIERunning","arguments":{}}, + {"toolset_name":"editor_toolset.toolsets.asset.AssetTools","tool_name":"save_assets","arguments":{"asset_paths":[]}}, + {"toolset_name":"IronsEditor.IronsMigrationToolset","tool_name":"Snapshot","arguments":{}} +] diff --git a/Tools/Unreal/Commands/FinishTemplateCleanup.json b/Tools/Unreal/Commands/FinishTemplateCleanup.json new file mode 100644 index 0000000..5b49045 --- /dev/null +++ b/Tools/Unreal/Commands/FinishTemplateCleanup.json @@ -0,0 +1,5 @@ +[ + {"toolset_name":"IronsEditor.IronsMigrationToolset","tool_name":"Snapshot","arguments":{}}, + {"toolset_name":"IronsEditor.IronsMigrationToolset","tool_name":"CleanupTemplateRemainders","arguments":{}}, + {"toolset_name":"editor_toolset.toolsets.asset.AssetTools","tool_name":"save_assets","arguments":{"asset_paths":[]}} +] diff --git a/Tools/Unreal/Commands/HostTests.json b/Tools/Unreal/Commands/HostTests.json new file mode 100644 index 0000000..e111222 --- /dev/null +++ b/Tools/Unreal/Commands/HostTests.json @@ -0,0 +1,5 @@ +[ + {"toolset_name":"AutomationTestToolset.AutomationTestToolset","tool_name":"DiscoverTests","arguments":{"bForceRediscover":false}}, + {"toolset_name":"AutomationTestToolset.AutomationTestToolset","tool_name":"RunTests","arguments":{"testNames":["IRONSPORE.Host.AtomicV1"]}}, + {"toolset_name":"AutomationTestToolset.AutomationTestToolset","tool_name":"GetTestResults","arguments":{}} +] diff --git a/Tools/Unreal/Commands/ImportModels.json b/Tools/Unreal/Commands/ImportModels.json new file mode 100644 index 0000000..3d2f449 --- /dev/null +++ b/Tools/Unreal/Commands/ImportModels.json @@ -0,0 +1,5 @@ +[ + {"toolset_name":"editor_toolset.toolsets.static_mesh.StaticMeshTools","tool_name":"import_file","arguments":{"folder_path":"/Game/IRONSPORE/Art/Mechs","asset_name":"SM_MechScout","source_file":"D:/vibeCoding/ueRouge/SourceAssets/Exports/Unity/Mechs/Mech_Scout_A.fbx","import_materials":true,"import_textures":false,"combine_meshes":true}}, + {"toolset_name":"editor_toolset.toolsets.static_mesh.StaticMeshTools","tool_name":"import_file","arguments":{"folder_path":"/Game/IRONSPORE/Art/Environment","asset_name":"SM_HangarBay","source_file":"D:/vibeCoding/ueRouge/SourceAssets/Exports/Unity/Environment/HangarBay.fbx","import_materials":true,"import_textures":false,"combine_meshes":true}}, + {"toolset_name":"editor_toolset.toolsets.asset.AssetTools","tool_name":"save_assets","arguments":{"asset_paths":[]}} +] diff --git a/Tools/Unreal/Commands/PerformancePie.json b/Tools/Unreal/Commands/PerformancePie.json new file mode 100644 index 0000000..9d5f26f --- /dev/null +++ b/Tools/Unreal/Commands/PerformancePie.json @@ -0,0 +1,7 @@ +[ + {"toolset_name":"EditorToolset.EditorAppToolset","tool_name":"StartPIE","arguments":{"options":{"bSimulate":false,"playMode":"PlayMode_InEditorFloating","warmupSeconds":2}}}, + {"toolset_name":"IronsEditor.IronsMigrationToolset","tool_name":"Snapshot","arguments":{}}, + {"toolset_name":"EditorToolset.EditorAppToolset","tool_name":"StopPIE","arguments":{}}, + {"toolset_name":"editor_toolset.toolsets.asset.AssetTools","tool_name":"save_assets","arguments":{"asset_paths":[]}}, + {"toolset_name":"IronsEditor.IronsMigrationToolset","tool_name":"Snapshot","arguments":{}} +] diff --git a/Tools/Unreal/Commands/PerformanceTests.json b/Tools/Unreal/Commands/PerformanceTests.json new file mode 100644 index 0000000..cf69304 --- /dev/null +++ b/Tools/Unreal/Commands/PerformanceTests.json @@ -0,0 +1,5 @@ +[ + {"toolset_name":"AutomationTestToolset.AutomationTestToolset","tool_name":"DiscoverTests","arguments":{"bForceRediscover":true}}, + {"toolset_name":"AutomationTestToolset.AutomationTestToolset","tool_name":"RunTests","arguments":{"testNames":["IRONSPORE.Performance.RollingWindow","IRONSPORE.Performance.ClockAndValidity","IRONSPORE.Performance.ViewportLayout","IRONSPORE.Host.AtomicV1"]}}, + {"toolset_name":"AutomationTestToolset.AutomationTestToolset","tool_name":"GetTestResults","arguments":{}} +] diff --git a/Tools/Unreal/Commands/RemoveTemplate.json b/Tools/Unreal/Commands/RemoveTemplate.json new file mode 100644 index 0000000..e0f3ca1 --- /dev/null +++ b/Tools/Unreal/Commands/RemoveTemplate.json @@ -0,0 +1,14 @@ +[ + {"toolset_name":"IronsEditor.IronsMigrationToolset","tool_name":"Snapshot","arguments":{}}, + {"toolset_name":"editor_toolset.toolsets.asset.AssetTools","tool_name":"get_dependencies","arguments":{"asset_path":"/Game/IRONSPORE/Maps/L_Hangar"}}, + {"toolset_name":"editor_toolset.toolsets.asset.AssetTools","tool_name":"get_dependencies","arguments":{"asset_path":"/Game/IRONSPORE/UI/WBP_IronsRoot"}}, + {"toolset_name":"editor_toolset.toolsets.asset.AssetTools","tool_name":"delete","arguments":{"path":"/Game/DemoTemplate"}}, + {"toolset_name":"editor_toolset.toolsets.asset.AssetTools","tool_name":"delete","arguments":{"path":"/Game/FirstPerson"}}, + {"toolset_name":"editor_toolset.toolsets.asset.AssetTools","tool_name":"delete","arguments":{"path":"/Game/Characters"}}, + {"toolset_name":"editor_toolset.toolsets.asset.AssetTools","tool_name":"delete","arguments":{"path":"/Game/Input"}}, + {"toolset_name":"editor_toolset.toolsets.asset.AssetTools","tool_name":"delete","arguments":{"path":"/Game/LevelPrototyping"}}, + {"toolset_name":"editor_toolset.toolsets.asset.AssetTools","tool_name":"delete","arguments":{"path":"/Game/__ExternalActors__"}}, + {"toolset_name":"editor_toolset.toolsets.asset.AssetTools","tool_name":"delete","arguments":{"path":"/Game/__ExternalObjects__"}}, + {"toolset_name":"editor_toolset.toolsets.asset.AssetTools","tool_name":"save_assets","arguments":{"asset_paths":[]}}, + {"toolset_name":"IronsEditor.IronsMigrationToolset","tool_name":"Snapshot","arguments":{}} +] diff --git a/Tools/Unreal/Commands/StartPie.json b/Tools/Unreal/Commands/StartPie.json new file mode 100644 index 0000000..e1afc1d --- /dev/null +++ b/Tools/Unreal/Commands/StartPie.json @@ -0,0 +1,5 @@ +[ + {"toolset_name":"IronsEditor.IronsMigrationToolset","tool_name":"Snapshot","arguments":{}}, + {"toolset_name":"EditorToolset.EditorAppToolset","tool_name":"StartPIE","arguments":{"options":{"bSimulate":false,"playMode":"PlayMode_InEditorFloating","warmupSeconds":2}}}, + {"toolset_name":"IronsEditor.IronsMigrationToolset","tool_name":"Snapshot","arguments":{}} +] diff --git a/Tools/Unreal/LaunchEditor.ps1 b/Tools/Unreal/LaunchEditor.ps1 new file mode 100644 index 0000000..f4bd399 --- /dev/null +++ b/Tools/Unreal/LaunchEditor.ps1 @@ -0,0 +1,10 @@ +param([switch]$QA,[string]$EngineRoot='C:/Program Files/Epic Games/UE_5.8') +$ErrorActionPreference='Stop' +$root=(Resolve-Path (Join-Path $PSScriptRoot '../..')).Path +$project=Join-Path $root 'ueRouge.uproject' +$occupied=Get-NetTCPConnection -LocalPort 8000 -State Listen -ErrorAction SilentlyContinue +if($occupied){throw 'Port 8000 is already occupied. Verify its project before launching another MCP editor.'} +$arguments=@(('"'+$project+'"'),'-ModelContextProtocolStartServer','-ModelContextProtocolPort=8000','-NoLiveCoding','-log') +if($QA){$arguments+=('-IronsporeDataRoot="'+$root.Replace('\','/')+'/Saved/QA/Interactive"');$arguments+='-IronsporeQA'} +$process=Start-Process -FilePath (Join-Path $EngineRoot 'Engine/Binaries/Win64/UnrealEditor.exe') -ArgumentList $arguments -WindowStyle Hidden -PassThru +Write-Output ('Started target editor PID '+$process.Id) diff --git a/Tools/Unreal/McpClient.py b/Tools/Unreal/McpClient.py new file mode 100644 index 0000000..3218a0f --- /dev/null +++ b/Tools/Unreal/McpClient.py @@ -0,0 +1,75 @@ +"""Project-bound, task-owned MCP session. Execute reviewed native UE commands.""" +import argparse +import asyncio +import base64 +import json +import logging +import sys +from pathlib import Path +from mcp import ClientSession +from mcp.client.streamable_http import streamablehttp_client +from mcp.types import Implementation +from CheckMcpReadiness import ROOT, ENDPOINT, verify_owner, unpack, redact, Warnings + + +async def run(args): + owner = verify_owner() + warnings = [] + logger = logging.getLogger("mcp.client.streamable_http") + handler = Warnings(warnings) + logger.addHandler(handler) + logger.propagate = False + commands = json.loads(Path(args.file).read_text(encoding="utf-8-sig")) + if isinstance(commands, dict): + commands = [commands] + output = {"project": owner, "commands": [], "warnings": warnings} + try: + async with streamablehttp_client(ENDPOINT, timeout=180, sse_read_timeout=240, terminate_on_close=True) as (r, w, _): + async with ClientSession(r, w, client_info=Implementation(name="irons-migration", version="1.0")) as session: + await session.initialize() + async def call(toolset, name, arguments=None): + return unpack(await session.call_tool("call_tool", {"toolset_name":toolset, "tool_name":name,"arguments":arguments or {}})) + output["pieBefore"] = await call("EditorToolset.EditorAppToolset", "IsPIERunning") + output["levelBefore"] = await call("IronsEditor.IronsMigrationToolset", "Snapshot") if output["pieBefore"].get("returnValue") else await call("editor_toolset.toolsets.scene.SceneTools", "get_current_level") + for command in commands: + if verify_owner()["processId"] != owner["processId"]: + raise RuntimeError("Target process changed") + method = command.get("method", "call_tool") + arguments = command.get("arguments", {}) + payload = arguments if method != "call_tool" else {k: command[k] for k in ("toolset_name","tool_name","arguments") if k in command} + result = await session.call_tool(method, payload) + data = unpack(result) + if "image_path" in command: + payload = data.get("returnValue", {}) + image = payload.get("image",payload) + encoded = image.get("data") or next((b.data for b in result.content if b.type == "image"), None) + if not encoded: + raise RuntimeError("Capture did not return pixels") + path = Path(command["image_path"]).resolve() + path.relative_to(ROOT) + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(base64.b64decode(encoded)) + data = {"savedImage": str(path)} + output["commands"].append({"tool": command.get("tool_name", method),"result":data}) + if args.print_results: + print(redact(json.dumps(output["commands"][-1], ensure_ascii=False)), flush=True) + output["pieAfter"] = await call("EditorToolset.EditorAppToolset", "IsPIERunning") + output["levelAfter"] = await call("IronsEditor.IronsMigrationToolset", "Snapshot") if output["pieAfter"].get("returnValue") else await call("editor_toolset.toolsets.scene.SceneTools", "get_current_level") + if verify_owner()["processId"] != owner["processId"]: + raise RuntimeError("Target process changed") + output["clientClosed"] = True + finally: + logger.removeHandler(handler) + path = ROOT / args.output + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(redact(json.dumps(output, ensure_ascii=False, indent=2))+"\n",encoding="utf-8") + print(json.dumps({"record":str(path),"calls":len(output["commands"]),"warnings":warnings},ensure_ascii=False)) + + +if __name__ == "__main__": + sys.stdout.reconfigure(encoding="utf-8") + parser = argparse.ArgumentParser() + parser.add_argument("--file",required=True) + parser.add_argument("--output",default="Saved/Migration/mcp-results.json") + parser.add_argument("--print-results",action="store_true") + asyncio.run(run(parser.parse_args())) diff --git a/Tools/Unreal/PreparePresentation.py b/Tools/Unreal/PreparePresentation.py new file mode 100644 index 0000000..692e345 --- /dev/null +++ b/Tools/Unreal/PreparePresentation.py @@ -0,0 +1,239 @@ +"""Reproducible presentation derivatives; editable originals stay in SourceAssets.""" +import hashlib +import json +import math +import random +import re +import shutil +import struct +import wave +import zipfile +from pathlib import Path +import resvg_py +from fontTools.ttLib import TTFont +from fontTools.varLib.instancer import instantiateVariableFont + +ROOT=Path(__file__).resolve().parents[2] +STRINGS=""" +subtitle=机甲搜打撤 · 轨道边境|MECH EXTRACTION / ORBITAL FRONTIER +baseTitle=机库指挥台|HANGAR COMMAND +enterHangar=进入机库|ENTER HANGAR +deploy=整备并出击|DEPLOY +resume=继续出击|RESUME SORTIE +training=试验场|TRAINING RANGE +research=研究与制造|RESEARCH & FABRICATION +settings=设置|SETTINGS +performance=性能统计|PERFORMANCE +perfOn=开启|ON +perfOff=关闭|OFF +perfPosition=显示位置|POSITION +perfTopRight=右上角|TOP RIGHT +perfTopLeft=左上角|TOP LEFT +perfBottomRight=右下角|BOTTOM RIGHT +perfBottomLeft=左下角|BOTTOM LEFT +perfHint=60 FPS 参考预算 16.67 ms · — 表示暂无数据|60 FPS BUDGET: 16.67 ms / — MEANS UNAVAILABLE +perfFps=FPS|FPS +perfFrame=帧耗时|FRAME +perfGpu=GPU|GPU +perfGame=CPU 游戏|CPU GAME +perfRender=CPU 渲染|CPU RENDER +perfRhi=RHI|RHI +quit=退出游戏|EXIT GAME +baseHint=整备装备,搜索物资,活着撤离。|Equip. Search. Extract alive. +pilot=驾驶员档案|PILOT DOSSIER +warehouse=基地仓库|BASE WAREHOUSE +pending=待整理物资|RECOVERY INBOX +sorties=出击|SORTIES +returns=撤离|RETURNS +activeRun=任务进行中 · 可继续|ACTIVE SORTIE / RESUMABLE +standby=基地待命|BASE STANDBY +carryHint=拖动放置 · R 旋转 · 安全箱机毁保全|DRAG TO PLACE / R ROTATES / SECURE CASE SURVIVES +fieldSystems=战地改装与回收|FIELD SYSTEMS +fieldHint=安装实物装备,选择货舱物品后回收|INSTALL REAL PARTS / SELECT CARGO FOR RECOVERY +fieldEquipment=当前实物装备 · 可战地改装|INSTALLED PARTS / FIELD MODIFICATIONS +warehouseHint=滚轮浏览 · 有限容量 · 物品唯一归属|SCROLL TO BROWSE / LIMITED CAPACITY / UNIQUE ITEMS +primary=主武器|PRIMARY +secondary=副武器|SECONDARY +booster=推进器|BOOSTER +guard=防御模块|GUARD +mass=载重|MASS +power=功率|POWER +cargo=货仓|CARGO +safeCase=安全箱 · 2 × 2|SECURE CASE / 2 × 2 +cellsUsed=格已用|CELLS USED +sort=整理仓库|SORT STASH +podSelect=选择主动回收物品|SELECT RECOVERY CARGO +podRule=每次出击一舱 · 2 × 2 · 最多四件|ONE POD PER SORTIE / 2 × 2 / UP TO FOUR ITEMS +holdPod=关闭物品栏,落地静止长按|CLOSE INVENTORY, STAND STILL, HOLD +selected=已选择|SELECTED +uninstall=卸入货仓|UNINSTALL TO CARGO +borrow=借用基础件|BORROW BASELINE PART +borrowed=借用件|BORROWED +equip=装备此物品|EQUIP ITEM +toCargo=预装到货仓|PREPARE CARGO +toSafe=预装到安全箱|PREPARE SECURE CASE +unprepare=取消预装|CANCEL PREPARATION +install=安装装备|INSTALL PART +transfer=转移至另一容器|TRANSFER CONTAINER +recycle=回收已研究蓝图|RECYCLE KNOWN BLUEPRINT +dragHint=拖拽摆放 · R 旋转 · 数量可拆分|DRAG / R ROTATES / QUANTITY SPLITS STACKS +selectItem=选择物品查看操作|SELECT AN ITEM FOR ACTIONS +back=返回|BACK +researchHint=研究消耗合金 2、电芯 1;制造按配方消耗实物材料。|RESEARCH COSTS 2 ALLOY + 1 CELL. FABRICATION USES PHYSICAL STOCK. +manufacture=制造装备|FABRICATE PART +inboxHint=仓库满时物资保留在此;整理完才能再次出击。|OVERFLOW IS PRESERVED HERE. CLEAR INBOX BEFORE A NEW SORTIE. +claim=收入仓库|CLAIM +claimAll=全部收入仓库|CLAIM ALL +discard=丢弃|DISCARD +confirmDiscard=再次点击丢弃以确认。|CLICK DISCARD AGAIN TO CONFIRM. +settingsHint=显示、鼠标、声音与键位立即生效并保存。|DISPLAY, MOUSE, AUDIO AND KEY BINDINGS APPLY AND SAVE. +display=显示|DISPLAY +fullscreen=切换全屏 / 窗口|TOGGLE FULLSCREEN / WINDOWED +sensitivity=鼠标灵敏度|MOUSE SENSITIVITY +aimSensitivity=瞄准灵敏度倍率|AIM SENSITIVITY +fire=主武器开火|PRIMARY FIRE +aim=瞄准|AIM +invertY=反转鼠标纵轴|INVERT MOUSE Y +controls=键鼠操作|KEYBOARD & MOUSE +forward=向前|FORWARD +backward=向后|BACKWARD +left=向左|LEFT +right=向右|RIGHT +jump=跳跃 / 轻型悬浮|JUMP / LIGHT-CHASSIS LIFT +boost=推进冲刺|BOOST +reload=装填|RELOAD +view=切换第一 / 越肩视角|COCKPIT / SHOULDER VIEW +interact=交互 / 登机|INTERACT / BOARD +inventory=携带物品|INVENTORY +pod=主动回收(长按)|RECOVERY POD (HOLD) +pressKey=请按新按键 · Esc 取消|PRESS A KEY / ESC CANCELS +keyConflict=该按键已被其他操作使用。|THAT KEY IS ALREADY ASSIGNED. +volume=音量|VOLUME +quality=切换画质等级|CYCLE QUALITY +paused=任务暂停|SORTIE PAUSED +upgrade=局内改装|FIELD UPGRADE +pauseHint=查看携带物资,或保存进度后返回基地。|REVIEW YOUR CARGO OR SAVE YOUR SORTIE AND RETURN TO BASE. +saveAndHome=保存并返回主页|SAVE & RETURN HOME +upgradeDamage=火力强化 · 提升伤害|FIREPOWER / INCREASE DAMAGE +upgradeRepair=战地修复 · 恢复装甲|FIELD REPAIR / RESTORE ARMOR +upgradeEnergy=能源维护 · 恢复能源|ENERGY SERVICE / RESTORE ENERGY +armor=装甲|ARMOR +energy=能源|ENERGY +stage=区域|SECTOR +salvage=携带|CARGO +routeDone=路线完成,仍需主动撤离|ROUTE COMPLETE / EXTRACT TO BANK LOOT +objectiveDone=目标完成,可前往下一片区|OBJECTIVE COMPLETE / NEXT SECTOR UNLOCKED +findObjective=搜索物资并寻找目标终端|SEARCH SALVAGE AND FIND THE OBJECTIVE TERMINAL +podUsed=主动回收舱:已使用|RECOVERY POD: USED +podAvailable=主动回收舱:1 / 1|RECOVERY POD: 1 / 1 +reloading=装填中|RELOADING +wait=接应抵达倒计时|EXTRACTION ARRIVES IN +podHolding=货舱回收准备中,保持静止|RECOVERY ARMING / HOLD POSITION +footer=键鼠操作 · Esc 返回 · 搜索,战斗,撤离|KEYBOARD & MOUSE / ESC BACK / SEARCH, FIGHT, EXTRACT +completed=完成路线|COMPLETED ROUTES +rangeStats=试验场 · 命中 / 关卡 / 用时|RANGE / HITS / GATES / TIME +rangeReset=试验场计时和命中已重置。|RANGE TIMER AND HITS RESET. +courseGate=机动计时门|TIMED GATE +extraction=撤离平台|EXTRACTION PAD +routeBoss=首领路线|BOSS ROUTE +routeEscort=护送路线|ESCORT ROUTE +rangeResetLabel=重置试验场|RESET RANGE +saved=操作已保存|SAVED +invalid=该操作当前不可用。|THAT ACTION IS NOT AVAILABLE. +missing=找不到对应物品。|ITEM NOT FOUND. +noSpace=空间不足或物品无法放入。|NOT ENOUGH SPACE OR ITEM DOES NOT FIT. +locked=当前状态禁止该操作。|ACTION LOCKED IN THE CURRENT STATE. +budget=装备超出底盘载重或功率预算。|LOADOUT EXCEEDS CHASSIS MASS OR POWER BUDGET. +funds=可用材料不足。|INSUFFICIENT AVAILABLE MATERIALS. +duplicate=该物资或事件已经处理。|THIS SOURCE OR EVENT HAS ALREADY BEEN PROCESSED. +saveFailed=保存失败,操作未结算。请检查磁盘。|SAVE FAILED. TRANSACTION NOT COMMITTED. CHECK DISK. +corrupt=存档及备份无法读取,已保留原文件。|PROFILE AND BACKUP UNREADABLE. ORIGINAL FILES PRESERVED. +unsupported=存档版本不受支持。|UNSUPPORTED SAVE VERSION. +destroyed=机体损毁。安全箱已保全,其余物资损失。|MECH DESTROYED. SECURE CASE PRESERVED; OTHER CARGO LOST. +extracted=撤离成功,物资已回收入库。|EXTRACTED. RECOVERED ITEMS BANKED. +nothingNear=靠近物资箱、终端或撤离点后交互。|MOVE CLOSER TO SALVAGE, TERMINALS OR EXTRACTION. +loot=已搜索物资|SALVAGE RECOVERED +called=已呼叫接应,等待后需主动登机。|EXTRACTION CALLED. WAIT, THEN BOARD MANUALLY. +finalObjective=完成首领战或护送后仍需主动撤离。|DEFEAT THE BOSS OR COMPLETE ESCORT, THEN EXTRACT. +escort=护送运输车|ESCORT TRANSPORT +explore=搜索箱体 · 击败敌人 · 终端推进 · 绿色平台撤离|SEARCH CRATES / FIGHT / ACTIVATE TERMINALS / GREEN PAD EXTRACTS +search=搜索|SEARCH +board=登机并撤离|BOARD & EXTRACT +call=呼叫接应|CALL EXTRACTION +nextStage=前往下一片区(右侧为护送路线)|NEXT SECTOR (RIGHT GATE: ESCORT ROUTE) +activate=启动目标终端|ACTIVATE OBJECTIVE TERMINAL +readyToBoard=接应已抵达,回到平台按交互键登机。|TRANSPORT READY. RETURN TO PAD AND PRESS INTERACT TO BOARD. +podSent=选定货舱物资已发射回收,本次回收舱已用尽。|SELECTED CARGO RECOVERED. THIS SORTIE'S POD IS USED. +podInterrupted=回收被移动、起跳或命中打断。|RECOVERY INTERRUPTED BY MOVEMENT, AIRBORNE STATE OR DAMAGE. +escortLost=护送运输车已损毁。|ESCORT TRANSPORT DESTROYED. +""" + + +def prepare_localization(): + raw=ROOT/"Content/IRONSPORE/Raw" + pairs={} + for line in STRINGS.strip().splitlines(): + key,text=line.split("=",1) + pairs["ue."+key]=text.split("|",1) + required=set() + for file in (ROOT/"Source").rglob("*.cpp"): + required.update(re.findall(r'TEXT\("(ue\.[^" ]+)"\)',file.read_text(encoding="utf-8"))) + if required-pairs.keys(): + raise RuntimeError("Missing strings: "+str(sorted(required-pairs.keys()))) + for index,locale in enumerate(("zh-Hans","en")): + data=json.loads((ROOT/f"SourceAssets/Localization/Tables/{locale}.json").read_text(encoding="utf-8")) + entries={item["key"]:item["value"] for item in data["entries"]} + entries.update({key:values[index] for key,values in pairs.items()}) + folder=raw/"Localization";folder.mkdir(exist_ok=True) + (folder/f"{locale}.json").write_text(json.dumps({"entries":[{"key":k,"value":v} for k,v in entries.items()]},ensure_ascii=False,indent=2)+"\n",encoding="utf-8") + + +def prepare(): + raw=ROOT/"Content/IRONSPORE/Raw" + (raw/"Fonts").mkdir(parents=True,exist_ok=True) + font=TTFont(ROOT/"SourceAssets/ThirdParty/Adobe/NotoSansSC/NotoSansSC-VF.ttf") + font=instantiateVariableFont(font,{"wght":400},inplace=True) + for name in font["name"].names: + if name.nameID in (1,4,6,16,17): + value="Regular" if name.nameID==17 else "IronsSansSC-Regular" if name.nameID==6 else "Irons Sans SC" + name.string=value.encode(name.getEncoding()) + font.recalcTimestamp=False + font.save(raw/"Fonts/IronsSansSC-Regular.ttf") + prepare_localization() + derived=ROOT/"SourceAssets/Derived/UI";derived.mkdir(parents=True,exist_ok=True) + for svg in (ROOT/"SourceAssets/UI").rglob("*.svg"): + (derived/f"T_{svg.stem}.png").write_bytes(resvg_py.svg_to_bytes(svg_path=str(svg),zoom=2,skip_system_fonts=True)) + audio=ROOT/"SourceAssets/Audio/Generated";audio.mkdir(parents=True,exist_ok=True) + rng=random.Random(7001) + for name,frequency,duration in [("S_Shot",140,.12),("S_Impact",75,.18),("S_Secondary",65,.42),("S_UI",700,.08)]: + rate=44100;samples=[] + for n in range(int(rate*duration)): + t=n/rate;decay=(1-t/duration)**3 + value=(math.sin(2*math.pi*frequency*t*(1-.5*t/duration))*.55+rng.uniform(-1,1)*.45)*decay*.65 + samples.append(struct.pack("II',data[16:24]);images.append({'file':path.name,'width':width,'height':height,'environment':launch['environment']}) + check('Captured real images',len(images)>=5) + expected=tuple(map(int,launch['requestedResolution'].split('x'))) + check('Actual captures match requested resolution',all((i['width'],i['height'])==expected for i in images)) + def load(name):return json.loads((folder/(name+'.json')).read_text(encoding='utf-8-sig')) + if launch['scenario']=='cycle': + before,deploy,pod,ready,returned,dead,final=[load(n) for n in ('before','deployed','pod-recovered','ready-still-active','returned','destroyed','final')] + check('V1 unchanged',all(p['version']==1 for p in (before,deploy,pod,ready,returned,dead,final))) + check('Deployment consumed owned starter UID once',deploy['sorties']==1 and len(deploy['run']['installed'])==4 and len(deploy['warehouse'])==0) + check('Pod bank and exact receipt',pod['run']['active'] and pod['run']['podUsed'] and total(pod['warehouse'],'alloy')==4 and pod['receipts']==[deploy['run']['id']+':cargo']) + check('Ready transport did not auto-extract',ready['run']['active'] and ready['extractions']==0 and len(ready['receipts'])==1) + check('Explicit boarding returned equipment and protected cargo',not returned['run']['active'] and returned['extractions']==1 and total(returned['warehouse'],'cells')==2 and len(returned['warehouse'])==6) + check('Death retains only safe cargo; loss cargo not banked',dead['sorties']==2 and dead['extractions']==1 and total(dead['warehouse'],'alloy')==4 and total(dead['warehouse'],'cells')==5 and len(dead['warehouse'])==2 and not dead['run']['active']) + check('Recovery receipts unique',len(final['receipts'])==len(set(final['receipts']))==4) + uids=[i['uid'] for key in ('warehouse','recoveryInbox') for i in final[key]]+[i['uid'] for key in ('cargo','safeCase','installed') for i in final['run'][key]] + check('Unique physical ownership',len(uids)==len(set(uids))) + else: + profile=load('ui-profile');check('UI sweep did not deploy or change save version',profile['version']==1 and profile['sorties']==0 and not profile['run']['active']) + check('Both languages captured',any(i['file'].startswith('hangar-zh') for i in images) and any(i['file'].startswith('hangar-en') for i in images)) + result={'passed':all(c['passed'] for c in checks),'checks':checks,'screenshots':images} + (folder/'verification.json').write_text(json.dumps(result,ensure_ascii=False,indent=2)+'\n',encoding='utf-8') + print(json.dumps({'folder':str(folder),'passed':result['passed'],'checks':len(checks),'failed':[c['name'] for c in checks if not c['passed']],'images':len(images)},ensure_ascii=False)) + return result['passed'] + + +if __name__=='__main__': + parser=argparse.ArgumentParser();parser.add_argument('folder',type=Path);args=parser.parse_args() + raise SystemExit(0 if verify(args.folder.resolve()) else 1) diff --git a/Tools/Unreal/VerifyPerformanceAcceptance.py b/Tools/Unreal/VerifyPerformanceAcceptance.py new file mode 100644 index 0000000..8a58f7e --- /dev/null +++ b/Tools/Unreal/VerifyPerformanceAcceptance.py @@ -0,0 +1,54 @@ +"""Verify evidence emitted by the real packaged process; never generate screenshots.""" +import argparse +import json +import math +import struct +from pathlib import Path + + +def verify(folder): + launch=json.loads((folder/'launch.json').read_text(encoding='utf-8-sig')) + checks=[] + def check(name,value):checks.append({'name':name,'passed':bool(value)}) + def load(name):return json.loads((folder/(name+'.json')).read_text(encoding='utf-8')) + images=[] + for p in sorted(folder.glob('*.png')): + data=p.read_bytes() + if not data.startswith(b'\x89PNG\r\n\x1a\n'):raise ValueError('Not a PNG: '+str(p)) + w,h=struct.unpack('>II',data[16:24]);images.append({'file':p.name,'width':w,'height':h,'environment':launch['environment']}) + expected=tuple(map(int,launch['requestedResolution'].split('x'))) + required={'Full':11,'UI':4,'Restart':3}[launch['suite']] + check('Actual native screenshots captured',len(images)==required) + check('PNG dimensions match requested resolution',bool(images) and all((i['width'],i['height'])==expected for i in images)) + records=[load(Path(i['file']).stem) for i in images] + check('Exactly one overlay after each map transition',bool(records) and all(r['overlayInstances']==1 for r in records)) + check('Progress save version remains V1',all(r['saveVersion']==1 for r in records)) + check('Measured frame timing and reciprocal FPS agree',all(r['metrics']['fps']>0 and abs(r['metrics']['fps']*r['metrics']['frameMs']-1000)<.001 for r in records)) + check('Actual CPU and GPU readings are available on this DX12 device',all(all(math.isfinite(r['metrics'][k]) and r['metrics'][k]>0 for k in ('gameMs','renderMs','gpuMs')) for r in records)) + if launch['suite']=='Full': + initial=load('home-default-zh') + check('Fresh preferences default to enabled top-right Chinese',initial['enabled'] and initial['corner']==0 and not initial['english']) + check('Settings toggle disables the overlay',not load('disabled-zh')['enabled']) + check('All four corners selected through settings actions',{load(n)['corner'] for n in ('home-default-zh','top-left-en','bottom-right-en','bottom-left-en')}=={0,1,2,3}) + check('Language switches while overlay is visible',load('top-left-en')['enabled'] and load('top-left-en')['english']) + start,end=load('pause-begin'),load('pause-updating') + check('Sampling continues during real world pause',start['worldPaused'] and end['worldPaused'] and end['metrics']['revision']>start['metrics']['revision']) + check('Training and home transitions exercised','TrainingRange' in load('training-shoulder')['map'] and 'TrainingRange' in load('training-cockpit')['map'] and 'Hangar' in load('after-travel')['map']) + elif launch['suite']=='Restart': + saved=load('restart-disabled');enabled=load('restart-enabled') + check('Disabled and bottom-left settings persist across process restart',not saved['enabled'] and saved['corner']==3 and saved['english']) + check('Overlay and default corner can be restored',enabled['enabled'] and enabled['corner']==0 and not enabled['english']) + compare=load('native-comparison');native=compare['nativeStatUnitRaw'];raw=compare['raw'] + check('Native stat unit collected actual timings',all(native[k]>0 for k in ('frameMs','gameMs','renderMs','gpuMs'))) + check('Raw thread and GPU samples agree with native readouts within frame jitter',all(abs(raw[k]-native[k])<=max(3,.5*native[k]) for k in ('gameMs','renderMs','gpuMs'))) + else: + check('Both languages and settings page captured',load('settings-en')['english'] and not load('settings-zh')['english']) + result={'passed':all(c['passed'] for c in checks),'checks':checks,'screenshots':images} + (folder/'verification.json').write_text(json.dumps(result,ensure_ascii=False,indent=2)+'\n',encoding='utf-8') + print(json.dumps({'folder':str(folder),'passed':result['passed'],'checks':len(checks),'failed':[c['name'] for c in checks if not c['passed']]},ensure_ascii=False)) + return result['passed'] + + +if __name__=='__main__': + parser=argparse.ArgumentParser();parser.add_argument('folder',type=Path) + raise SystemExit(0 if verify(parser.parse_args().folder.resolve()) else 1) diff --git a/ueRouge.uproject b/ueRouge.uproject new file mode 100644 index 0000000..ed89e0f --- /dev/null +++ b/ueRouge.uproject @@ -0,0 +1,91 @@ +{ + "FileVersion": 3, + "EngineAssociation": "{65AB2A29-48BF-9C3A-F100-DC8FE86C354B}", + "Category": "", + "Description": "IRONSPORE - engine-independent gameFramework host", + "Modules": [ + { + "Name": "UEAdapter", + "Type": "Runtime", + "LoadingPhase": "Default" + }, + { + "Name": "ueRouge", + "Type": "Runtime", + "LoadingPhase": "Default" + }, + { + "Name": "IronsEditor", + "Type": "Editor", + "LoadingPhase": "PostEngineInit" + } + ], + "Plugins": [ + { + "Name": "AndroidFileServer", + "Enabled": false + }, + { + "Name": "ToolsetRegistry", + "Enabled": true, + "TargetAllowList": [ + "Editor" + ] + }, + { + "Name": "EnhancedInput", + "Enabled": true + }, + { + "Name": "ModelingToolsEditorMode", + "Enabled": true, + "TargetAllowList": [ + "Editor" + ] + }, + { + "Name": "Landmass", + "Enabled": false + }, + { + "Name": "InEditorDocumentation", + "Enabled": false, + "TargetAllowList": [ + "Editor" + ], + "PlatformAllowList": [ + "Win64", + "Mac", + "Linux" + ] + }, + { + "Name": "AIAssistant", + "Enabled": true, + "TargetAllowList": [ + "Editor" + ] + }, + { + "Name": "MCPClientToolset", + "Enabled": true, + "TargetAllowList": [ + "Editor" + ] + }, + { + "Name": "ModelContextProtocol", + "Enabled": true, + "TargetAllowList": [ + "Editor" + ] + }, + { + "Name": "AllToolsets", + "Enabled": true, + "TargetAllowList": [ + "Editor" + ] + } + ] +}