Compare commits
5 Commits
feature/ze
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| e1ac8547b5 | |||
| 3c7fa78912 | |||
| 7cc715cad5 | |||
| 932157eda5 | |||
| c0d9450d30 |
@ -242,9 +242,12 @@ bEnabled=true
|
|||||||
bEnabled=true
|
bEnabled=true
|
||||||
SteamDevAppId=480
|
SteamDevAppId=480
|
||||||
bInitServerOnClient=true
|
bInitServerOnClient=true
|
||||||
|
bVACEnabled=0
|
||||||
|
|
||||||
[/Script/Engine.GameEngine]
|
[/Script/Engine.GameEngine]
|
||||||
+NetDriverDefinitions=(DefName="GameNetDriver",DriverClassName="OnlineSubsystemUtils.IpNetDriver",DriverClassNameFallback="OnlineSubsystemUtils.IpNetDriver")
|
+NetDriverDefinitions=(DefName="GameNetDriver",DriverClassName="OnlineSubsystemUtils.IpNetDriver",DriverClassNameFallback="OnlineSubsystemUtils.IpNetDriver")
|
||||||
|
; Steam sessions are selected in the multiplayer menu (Backend = Steam / Auto).
|
||||||
|
; When shipping on Steam, set DefaultPlatformService=Steam and SteamNetDriver above.
|
||||||
|
|
||||||
[/Script/OnlineSubsystemUtils.OnlineSession]
|
[/Script/OnlineSubsystemUtils.OnlineSession]
|
||||||
bUseBuildIdOverride=false
|
bUseBuildIdOverride=false
|
||||||
|
|||||||
@ -1,5 +1,10 @@
|
|||||||
[/Script/VocationLife.VocationServerSettings]
|
[/Script/VocationLife.VocationServerSettings]
|
||||||
ServerName=VocationLife Dedicated Server
|
ServerName=VocationLife Dedicated Server
|
||||||
MaxPlayers=4
|
MaxPlayers=4
|
||||||
Port=7777
|
GamePort=7777
|
||||||
bLANOnly=true
|
AdminHttpPort=8080
|
||||||
|
bLANOnly=False
|
||||||
|
bUseSteam=True
|
||||||
|
AdminPassword=vocation
|
||||||
|
DefaultMapPath=/Engine/Maps/Entry
|
||||||
|
GameRules=(bFriendlyFire=False,DayLengthMinutes=20.000000,bAllowVocationSwitch=True,StartingOre=0,MiningRespawnSeconds=30.000000)
|
||||||
|
|||||||
56
Docs/Multiplayer.md
Normal file
56
Docs/Multiplayer.md
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
# Multiplayer, Steam & Server Admin
|
||||||
|
|
||||||
|
## Start Menu
|
||||||
|
|
||||||
|
Map / Steam / LAN / IP:Port selection appears **only at game start**.
|
||||||
|
It is not bound to a key in-game (`M` is reserved for the world map).
|
||||||
|
|
||||||
|
## Steam
|
||||||
|
|
||||||
|
1. Steam Client must be running.
|
||||||
|
2. Dev AppId is `480` (Spacewar) in `Config/DefaultEngine.ini` — replace with your own AppId for release.
|
||||||
|
3. In the menu choose Backend **Steam** or **Auto**.
|
||||||
|
4. Host on one PC, Find Steam on the other.
|
||||||
|
|
||||||
|
LAN/IP works without Steam (Backend **LAN** or Direct Connect).
|
||||||
|
|
||||||
|
## Direct Connect
|
||||||
|
|
||||||
|
```
|
||||||
|
IP: 192.168.x.x
|
||||||
|
Port: 7777
|
||||||
|
```
|
||||||
|
|
||||||
|
Or console: `open 192.168.x.x:7777`
|
||||||
|
|
||||||
|
## Dedicated Server Web Admin
|
||||||
|
|
||||||
|
On headless / dedicated start, admin UI binds to **AdminHttpPort** (default `8080`):
|
||||||
|
|
||||||
|
```
|
||||||
|
http://192.168.178.129:8080
|
||||||
|
```
|
||||||
|
|
||||||
|
Default password: `vocation` (change in `Config/ServerConfig.ini`).
|
||||||
|
|
||||||
|
You can change:
|
||||||
|
|
||||||
|
- Server name, game port, max players, map
|
||||||
|
- LAN / Steam preference
|
||||||
|
- Game rules (friendly fire, day length, mining respawn, …)
|
||||||
|
|
||||||
|
**Game port changes require a server restart.**
|
||||||
|
JSON status: `GET /api/status`
|
||||||
|
|
||||||
|
Force web admin on a non-dedicated run: `-WebAdmin`
|
||||||
|
|
||||||
|
## ServerConfig.ini
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[/Script/VocationLife.VocationServerSettings]
|
||||||
|
ServerName=VocationLife Dedicated Server
|
||||||
|
GamePort=7777
|
||||||
|
AdminHttpPort=8080
|
||||||
|
AdminPassword=vocation
|
||||||
|
MaxPlayers=4
|
||||||
|
```
|
||||||
@ -4,7 +4,7 @@ A life-simulation action RPG remake inspired by Fantasy Life, built in **Unreal
|
|||||||
|
|
||||||
## Vision
|
## Vision
|
||||||
|
|
||||||
- First-person gameplay (primary) with classic top-down camera toggle
|
- First-person gameplay with classic top-down camera as the default (toggle with C)
|
||||||
- Mouse, keyboard, and gamepad support via Enhanced Input
|
- Mouse, keyboard, and gamepad support via Enhanced Input
|
||||||
- Co-op multiplayer (listen server first, dedicated server hosting)
|
- Co-op multiplayer (listen server first, dedicated server hosting)
|
||||||
- Optional hardware ray tracing and scalability presets
|
- Optional hardware ray tracing and scalability presets
|
||||||
@ -31,8 +31,7 @@ A life-simulation action RPG remake inspired by Fantasy Life, built in **Unreal
|
|||||||
| Jump | Space | A / Cross |
|
| Jump | Space | A / Cross |
|
||||||
| Interact / Craft | E | X / Square |
|
| Interact / Craft | E | X / Square |
|
||||||
| Attack | LMB | RT |
|
| Attack | LMB | RT |
|
||||||
| Toggle camera (FP / Top-Down) | V | Menu |
|
| Toggle camera (Top-Down / FP) | C | Menu |
|
||||||
| Quick save | I | Y / Triangle |
|
|
||||||
| Pause | Esc | Start |
|
| Pause | Esc | Start |
|
||||||
|
|
||||||
### Pause menu shortcuts
|
### Pause menu shortcuts
|
||||||
@ -52,6 +51,8 @@ Source/VocationLife/ Core gameplay C++ (camera, input, inventory, crafting, m
|
|||||||
Config/ Engine and server configuration
|
Config/ Engine and server configuration
|
||||||
```
|
```
|
||||||
|
|
||||||
|
See also: [Docs/Multiplayer.md](Docs/Multiplayer.md) (Steam, IP:Port, web admin).
|
||||||
|
|
||||||
## Dedicated Server Hosting
|
## Dedicated Server Hosting
|
||||||
|
|
||||||
Build the server target:
|
Build the server target:
|
||||||
|
|||||||
@ -32,6 +32,7 @@ AVocationCharacter::AVocationCharacter()
|
|||||||
FirstPersonCamera->SetupAttachment(GetCapsuleComponent());
|
FirstPersonCamera->SetupAttachment(GetCapsuleComponent());
|
||||||
FirstPersonCamera->SetRelativeLocation(FVector(0.f, 0.f, 64.f));
|
FirstPersonCamera->SetRelativeLocation(FVector(0.f, 0.f, 64.f));
|
||||||
FirstPersonCamera->bUsePawnControlRotation = true;
|
FirstPersonCamera->bUsePawnControlRotation = true;
|
||||||
|
FirstPersonCamera->SetActive(false);
|
||||||
|
|
||||||
TopDownSpringArm = CreateDefaultSubobject<USpringArmComponent>(TEXT("TopDownSpringArm"));
|
TopDownSpringArm = CreateDefaultSubobject<USpringArmComponent>(TEXT("TopDownSpringArm"));
|
||||||
TopDownSpringArm->SetupAttachment(RootComponent);
|
TopDownSpringArm->SetupAttachment(RootComponent);
|
||||||
@ -42,7 +43,7 @@ AVocationCharacter::AVocationCharacter()
|
|||||||
TopDownSpringArm->bInheritPitch = false;
|
TopDownSpringArm->bInheritPitch = false;
|
||||||
TopDownSpringArm->bInheritRoll = false;
|
TopDownSpringArm->bInheritRoll = false;
|
||||||
TopDownSpringArm->bInheritYaw = false;
|
TopDownSpringArm->bInheritYaw = false;
|
||||||
TopDownSpringArm->SetActive(false);
|
TopDownSpringArm->SetActive(true);
|
||||||
|
|
||||||
TopDownCamera = CreateDefaultSubobject<UCameraComponent>(TEXT("TopDownCamera"));
|
TopDownCamera = CreateDefaultSubobject<UCameraComponent>(TEXT("TopDownCamera"));
|
||||||
TopDownCamera->SetupAttachment(TopDownSpringArm, USpringArmComponent::SocketName);
|
TopDownCamera->SetupAttachment(TopDownSpringArm, USpringArmComponent::SocketName);
|
||||||
@ -54,21 +55,25 @@ AVocationCharacter::AVocationCharacter()
|
|||||||
|
|
||||||
if (USkeletalMeshComponent* MeshComp = GetMesh())
|
if (USkeletalMeshComponent* MeshComp = GetMesh())
|
||||||
{
|
{
|
||||||
MeshComp->SetOnlyOwnerSee(true);
|
// Visible body for top-down default; FP mode reattaches in ApplyCameraModeVisuals.
|
||||||
MeshComp->SetupAttachment(FirstPersonCamera);
|
MeshComp->SetOnlyOwnerSee(false);
|
||||||
MeshComp->SetRelativeLocation(FVector(25.f, 0.f, -90.f));
|
MeshComp->SetOwnerNoSee(false);
|
||||||
|
MeshComp->SetupAttachment(GetCapsuleComponent());
|
||||||
|
MeshComp->SetRelativeLocation(FVector(0.f, 0.f, -90.f));
|
||||||
|
MeshComp->SetRelativeRotation(FRotator(0.f, -90.f, 0.f));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (UCharacterMovementComponent* Movement = GetCharacterMovement())
|
if (UCharacterMovementComponent* Movement = GetCharacterMovement())
|
||||||
{
|
{
|
||||||
Movement->bOrientRotationToMovement = false;
|
Movement->bOrientRotationToMovement = true;
|
||||||
Movement->RotationRate = FRotator(0.f, 540.f, 0.f);
|
Movement->RotationRate = FRotator(0.f, 540.f, 0.f);
|
||||||
Movement->JumpZVelocity = 500.f;
|
Movement->JumpZVelocity = 500.f;
|
||||||
Movement->AirControl = 0.35f;
|
Movement->AirControl = 0.35f;
|
||||||
Movement->MaxWalkSpeed = 500.f;
|
Movement->MaxWalkSpeed = 500.f;
|
||||||
}
|
}
|
||||||
|
|
||||||
bUseControllerRotationPitch = true;
|
// Top-down is the default camera mode (Fantasy Life style).
|
||||||
|
bUseControllerRotationPitch = false;
|
||||||
bUseControllerRotationYaw = true;
|
bUseControllerRotationYaw = true;
|
||||||
bUseControllerRotationRoll = false;
|
bUseControllerRotationRoll = false;
|
||||||
}
|
}
|
||||||
@ -304,6 +309,25 @@ void AVocationCharacter::ApplyCameraModeVisuals()
|
|||||||
TopDownSpringArm->SetActive(!bFirstPerson);
|
TopDownSpringArm->SetActive(!bFirstPerson);
|
||||||
TopDownCamera->SetActive(!bFirstPerson);
|
TopDownCamera->SetActive(!bFirstPerson);
|
||||||
|
|
||||||
|
if (USkeletalMeshComponent* MeshComp = GetMesh())
|
||||||
|
{
|
||||||
|
if (bFirstPerson)
|
||||||
|
{
|
||||||
|
MeshComp->SetOnlyOwnerSee(true);
|
||||||
|
MeshComp->SetupAttachment(FirstPersonCamera);
|
||||||
|
MeshComp->SetRelativeLocation(FVector(25.f, 0.f, -90.f));
|
||||||
|
MeshComp->SetRelativeRotation(FRotator::ZeroRotator);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
MeshComp->SetOnlyOwnerSee(false);
|
||||||
|
MeshComp->SetOwnerNoSee(false);
|
||||||
|
MeshComp->SetupAttachment(GetCapsuleComponent());
|
||||||
|
MeshComp->SetRelativeLocation(FVector(0.f, 0.f, -90.f));
|
||||||
|
MeshComp->SetRelativeRotation(FRotator(0.f, -90.f, 0.f));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bUseControllerRotationPitch = bFirstPerson;
|
bUseControllerRotationPitch = bFirstPerson;
|
||||||
if (UCharacterMovementComponent* Movement = GetCharacterMovement())
|
if (UCharacterMovementComponent* Movement = GetCharacterMovement())
|
||||||
{
|
{
|
||||||
|
|||||||
@ -21,6 +21,11 @@ void UVocationGameInstance::Init()
|
|||||||
OnDataReady.Broadcast();
|
OnDataReady.Broadcast();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void UVocationGameInstance::MarkGameplayStarted()
|
||||||
|
{
|
||||||
|
bHasEnteredGameplay = true;
|
||||||
|
}
|
||||||
|
|
||||||
void UVocationGameInstance::InitializeDefaultData()
|
void UVocationGameInstance::InitializeDefaultData()
|
||||||
{
|
{
|
||||||
ItemDefinitions.Empty();
|
ItemDefinitions.Empty();
|
||||||
|
|||||||
@ -76,7 +76,7 @@ void AVocationHUD::DrawMainHUD()
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
const FString Controls = TEXT("WASD Move | Mouse Look | V Camera | E Interact/Craft | LMB Attack | I Save | Esc Pause");
|
const FString Controls = TEXT("WASD Move | C Camera | E Interact | LMB Attack | I Save | Esc Pause");
|
||||||
Canvas->DrawColor = FColor(200, 200, 200);
|
Canvas->DrawColor = FColor(200, 200, 200);
|
||||||
Canvas->DrawText(GEngine->GetSmallFont(), Controls, 40.f * Scale, Canvas->ClipY - 60.f * Scale, Scale, Scale);
|
Canvas->DrawText(GEngine->GetSmallFont(), Controls, 40.f * Scale, Canvas->ClipY - 60.f * Scale, Scale, Scale);
|
||||||
}
|
}
|
||||||
|
|||||||
631
Source/VocationLife/Private/VocationMainMenuWidget.cpp
Normal file
631
Source/VocationLife/Private/VocationMainMenuWidget.cpp
Normal file
@ -0,0 +1,631 @@
|
|||||||
|
// Copyright VocationLife Project. All Rights Reserved.
|
||||||
|
|
||||||
|
#include "VocationMainMenuWidget.h"
|
||||||
|
#include "VocationSessionSubsystem.h"
|
||||||
|
#include "VocationGameInstance.h"
|
||||||
|
#include "VocationGraphicsLibrary.h"
|
||||||
|
#include "Components/Button.h"
|
||||||
|
#include "Components/ComboBoxString.h"
|
||||||
|
#include "Components/EditableTextBox.h"
|
||||||
|
#include "Components/TextBlock.h"
|
||||||
|
#include "Components/VerticalBox.h"
|
||||||
|
#include "Components/VerticalBoxSlot.h"
|
||||||
|
#include "Components/WidgetSwitcher.h"
|
||||||
|
#include "Components/Border.h"
|
||||||
|
#include "Components/SizeBox.h"
|
||||||
|
#include "Components/Overlay.h"
|
||||||
|
#include "Components/OverlaySlot.h"
|
||||||
|
#include "Components/Spacer.h"
|
||||||
|
#include "Blueprint/WidgetTree.h"
|
||||||
|
#include "Engine/GameInstance.h"
|
||||||
|
#include "Kismet/KismetSystemLibrary.h"
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
void AddPadded(UVerticalBox* Box, UWidget* Child, float PadY = 6.f)
|
||||||
|
{
|
||||||
|
if (UVerticalBoxSlot* Slot = Box->AddChildToVerticalBox(Child))
|
||||||
|
{
|
||||||
|
Slot->SetPadding(FMargin(0.f, PadY));
|
||||||
|
Slot->SetHorizontalAlignment(HAlign_Fill);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UVocationMainMenuWidget::NativeConstruct()
|
||||||
|
{
|
||||||
|
Super::NativeConstruct();
|
||||||
|
BuildUI();
|
||||||
|
RefreshMaps();
|
||||||
|
ShowPage(EMenuPage::Main);
|
||||||
|
|
||||||
|
if (UGameInstance* GI = GetGameInstance())
|
||||||
|
{
|
||||||
|
if (UVocationSessionSubsystem* Sessions = GI->GetSubsystem<UVocationSessionSubsystem>())
|
||||||
|
{
|
||||||
|
Sessions->OnSessionSearchDetailed.AddDynamic(this, &UVocationMainMenuWidget::OnSessionSearchDetailed);
|
||||||
|
Sessions->OnSessionJoined.AddDynamic(this, &UVocationMainMenuWidget::OnSessionJoined);
|
||||||
|
SetStatus(FString::Printf(TEXT("Steam: %s"), Sessions->IsSteamAvailable() ? TEXT("ready") : TEXT("offline / LAN")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UVocationMainMenuWidget::NativeDestruct()
|
||||||
|
{
|
||||||
|
if (UGameInstance* GI = GetGameInstance())
|
||||||
|
{
|
||||||
|
if (UVocationSessionSubsystem* Sessions = GI->GetSubsystem<UVocationSessionSubsystem>())
|
||||||
|
{
|
||||||
|
Sessions->OnSessionSearchDetailed.RemoveDynamic(this, &UVocationMainMenuWidget::OnSessionSearchDetailed);
|
||||||
|
Sessions->OnSessionJoined.RemoveDynamic(this, &UVocationMainMenuWidget::OnSessionJoined);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Super::NativeDestruct();
|
||||||
|
}
|
||||||
|
|
||||||
|
UTextBlock* UVocationMainMenuWidget::MakeLabel(const FName& Name, const FString& Text, float Size) const
|
||||||
|
{
|
||||||
|
UTextBlock* Label = WidgetTree->ConstructWidget<UTextBlock>(UTextBlock::StaticClass(), Name);
|
||||||
|
Label->SetText(FText::FromString(Text));
|
||||||
|
FSlateFontInfo Font = Label->GetFont();
|
||||||
|
Font.Size = Size;
|
||||||
|
Label->SetFont(Font);
|
||||||
|
Label->SetJustification(ETextJustify::Center);
|
||||||
|
Label->SetColorAndOpacity(FSlateColor(FLinearColor(0.95f, 0.95f, 0.95f, 1.f)));
|
||||||
|
return Label;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UVocationMainMenuWidget::StyleMenuButton(UButton* Button) const
|
||||||
|
{
|
||||||
|
if (!Button)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
FButtonStyle Style = Button->GetStyle();
|
||||||
|
FSlateBrush Normal;
|
||||||
|
Normal.TintColor = FSlateColor(FLinearColor(0.28f, 0.28f, 0.28f, 0.95f));
|
||||||
|
Normal.DrawAs = ESlateBrushDrawType::Box;
|
||||||
|
Style.SetNormal(Normal);
|
||||||
|
|
||||||
|
FSlateBrush Hovered = Normal;
|
||||||
|
Hovered.TintColor = FSlateColor(FLinearColor(0.45f, 0.45f, 0.2f, 1.f));
|
||||||
|
Style.SetHovered(Hovered);
|
||||||
|
|
||||||
|
FSlateBrush Pressed = Normal;
|
||||||
|
Pressed.TintColor = FSlateColor(FLinearColor(0.2f, 0.2f, 0.2f, 1.f));
|
||||||
|
Style.SetPressed(Pressed);
|
||||||
|
|
||||||
|
Button->SetStyle(Style);
|
||||||
|
}
|
||||||
|
|
||||||
|
UButton* UVocationMainMenuWidget::MakeMenuButton(const FName& Name, const FString& Label, void (UVocationMainMenuWidget::*Callback)())
|
||||||
|
{
|
||||||
|
UButton* Button = WidgetTree->ConstructWidget<UButton>(UButton::StaticClass(), Name);
|
||||||
|
StyleMenuButton(Button);
|
||||||
|
|
||||||
|
USizeBox* Size = WidgetTree->ConstructWidget<USizeBox>(USizeBox::StaticClass(), *FString::Printf(TEXT("%s_Size"), *Name.ToString()));
|
||||||
|
Size->SetWidthOverride(400.f);
|
||||||
|
Size->SetHeightOverride(44.f);
|
||||||
|
Size->SetContent(Button);
|
||||||
|
|
||||||
|
UTextBlock* Text = MakeLabel(*FString::Printf(TEXT("%s_Label"), *Name.ToString()), Label, 18.f);
|
||||||
|
Button->SetContent(Text);
|
||||||
|
|
||||||
|
// Bind via a thin lambda isn't possible with AddDynamic — callers bind after.
|
||||||
|
(void)Callback;
|
||||||
|
return Button;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UVocationMainMenuWidget::BuildUI()
|
||||||
|
{
|
||||||
|
if (!WidgetTree)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
UBorder* Root = WidgetTree->ConstructWidget<UBorder>(UBorder::StaticClass(), TEXT("RootBorder"));
|
||||||
|
Root->SetBrushColor(FLinearColor(0.05f, 0.08f, 0.12f, 0.88f));
|
||||||
|
Root->SetPadding(FMargin(24.f));
|
||||||
|
WidgetTree->RootWidget = Root;
|
||||||
|
|
||||||
|
UOverlay* Overlay = WidgetTree->ConstructWidget<UOverlay>(UOverlay::StaticClass(), TEXT("RootOverlay"));
|
||||||
|
Root->SetContent(Overlay);
|
||||||
|
|
||||||
|
UVerticalBox* CenterColumn = WidgetTree->ConstructWidget<UVerticalBox>(UVerticalBox::StaticClass(), TEXT("CenterColumn"));
|
||||||
|
if (UOverlaySlot* OverlaySlot = Overlay->AddChildToOverlay(CenterColumn))
|
||||||
|
{
|
||||||
|
OverlaySlot->SetHorizontalAlignment(HAlign_Center);
|
||||||
|
OverlaySlot->SetVerticalAlignment(VAlign_Center);
|
||||||
|
}
|
||||||
|
|
||||||
|
TitleText = MakeLabel(TEXT("Title"), TEXT("VocationLife"), 56.f);
|
||||||
|
AddPadded(CenterColumn, TitleText, 4.f);
|
||||||
|
|
||||||
|
SplashText = MakeLabel(TEXT("Splash"), TEXT("A life of many vocations!"), 16.f);
|
||||||
|
SplashText->SetColorAndOpacity(FSlateColor(FLinearColor(1.f, 0.9f, 0.2f, 1.f)));
|
||||||
|
AddPadded(CenterColumn, SplashText, 2.f);
|
||||||
|
|
||||||
|
USpacer* Spacer = WidgetTree->ConstructWidget<USpacer>(USpacer::StaticClass(), TEXT("TitleSpacer"));
|
||||||
|
Spacer->SetSize(FVector2D(1.f, 28.f));
|
||||||
|
AddPadded(CenterColumn, Spacer, 0.f);
|
||||||
|
|
||||||
|
PageSwitcher = WidgetTree->ConstructWidget<UWidgetSwitcher>(UWidgetSwitcher::StaticClass(), TEXT("PageSwitcher"));
|
||||||
|
AddPadded(CenterColumn, PageSwitcher, 8.f);
|
||||||
|
|
||||||
|
// ---- Main page ----
|
||||||
|
UVerticalBox* MainPage = WidgetTree->ConstructWidget<UVerticalBox>(UVerticalBox::StaticClass(), TEXT("MainPage"));
|
||||||
|
PageSwitcher->AddChild(MainPage);
|
||||||
|
|
||||||
|
UButton* BtnSingleplayer = WidgetTree->ConstructWidget<UButton>(UButton::StaticClass(), TEXT("BtnSingleplayer"));
|
||||||
|
StyleMenuButton(BtnSingleplayer);
|
||||||
|
{
|
||||||
|
USizeBox* S = WidgetTree->ConstructWidget<USizeBox>(USizeBox::StaticClass(), TEXT("BtnSingleplayerSize"));
|
||||||
|
S->SetWidthOverride(400.f);
|
||||||
|
S->SetHeightOverride(46.f);
|
||||||
|
S->SetContent(BtnSingleplayer);
|
||||||
|
BtnSingleplayer->SetContent(MakeLabel(TEXT("BtnSingleplayerLbl"), TEXT("Singleplayer"), 18.f));
|
||||||
|
BtnSingleplayer->OnClicked.AddDynamic(this, &UVocationMainMenuWidget::OnSingleplayerClicked);
|
||||||
|
AddPadded(MainPage, S, 5.f);
|
||||||
|
}
|
||||||
|
|
||||||
|
UButton* BtnMultiplayer = WidgetTree->ConstructWidget<UButton>(UButton::StaticClass(), TEXT("BtnMultiplayer"));
|
||||||
|
StyleMenuButton(BtnMultiplayer);
|
||||||
|
{
|
||||||
|
USizeBox* S = WidgetTree->ConstructWidget<USizeBox>(USizeBox::StaticClass(), TEXT("BtnMultiplayerSize"));
|
||||||
|
S->SetWidthOverride(400.f);
|
||||||
|
S->SetHeightOverride(46.f);
|
||||||
|
S->SetContent(BtnMultiplayer);
|
||||||
|
BtnMultiplayer->SetContent(MakeLabel(TEXT("BtnMultiplayerLbl"), TEXT("Multiplayer"), 18.f));
|
||||||
|
BtnMultiplayer->OnClicked.AddDynamic(this, &UVocationMainMenuWidget::OnMultiplayerClicked);
|
||||||
|
AddPadded(MainPage, S, 5.f);
|
||||||
|
}
|
||||||
|
|
||||||
|
UButton* BtnOptions = WidgetTree->ConstructWidget<UButton>(UButton::StaticClass(), TEXT("BtnOptions"));
|
||||||
|
StyleMenuButton(BtnOptions);
|
||||||
|
{
|
||||||
|
USizeBox* S = WidgetTree->ConstructWidget<USizeBox>(USizeBox::StaticClass(), TEXT("BtnOptionsSize"));
|
||||||
|
S->SetWidthOverride(400.f);
|
||||||
|
S->SetHeightOverride(46.f);
|
||||||
|
S->SetContent(BtnOptions);
|
||||||
|
BtnOptions->SetContent(MakeLabel(TEXT("BtnOptionsLbl"), TEXT("Options..."), 18.f));
|
||||||
|
BtnOptions->OnClicked.AddDynamic(this, &UVocationMainMenuWidget::OnOptionsClicked);
|
||||||
|
AddPadded(MainPage, S, 5.f);
|
||||||
|
}
|
||||||
|
|
||||||
|
UButton* BtnQuit = WidgetTree->ConstructWidget<UButton>(UButton::StaticClass(), TEXT("BtnQuit"));
|
||||||
|
StyleMenuButton(BtnQuit);
|
||||||
|
{
|
||||||
|
USizeBox* S = WidgetTree->ConstructWidget<USizeBox>(USizeBox::StaticClass(), TEXT("BtnQuitSize"));
|
||||||
|
S->SetWidthOverride(400.f);
|
||||||
|
S->SetHeightOverride(46.f);
|
||||||
|
S->SetContent(BtnQuit);
|
||||||
|
BtnQuit->SetContent(MakeLabel(TEXT("BtnQuitLbl"), TEXT("Quit Game"), 18.f));
|
||||||
|
BtnQuit->OnClicked.AddDynamic(this, &UVocationMainMenuWidget::OnQuitClicked);
|
||||||
|
AddPadded(MainPage, S, 5.f);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Singleplayer page ----
|
||||||
|
UVerticalBox* SoloPage = WidgetTree->ConstructWidget<UVerticalBox>(UVerticalBox::StaticClass(), TEXT("SoloPage"));
|
||||||
|
PageSwitcher->AddChild(SoloPage);
|
||||||
|
AddPadded(SoloPage, MakeLabel(TEXT("SoloTitle"), TEXT("Singleplayer"), 28.f), 4.f);
|
||||||
|
AddPadded(SoloPage, MakeLabel(TEXT("SoloMapLbl"), TEXT("Map"), 14.f), 2.f);
|
||||||
|
MapCombo = WidgetTree->ConstructWidget<UComboBoxString>(UComboBoxString::StaticClass(), TEXT("MapCombo"));
|
||||||
|
{
|
||||||
|
USizeBox* S = WidgetTree->ConstructWidget<USizeBox>(USizeBox::StaticClass(), TEXT("MapComboSize"));
|
||||||
|
S->SetWidthOverride(400.f);
|
||||||
|
S->SetContent(MapCombo);
|
||||||
|
AddPadded(SoloPage, S, 4.f);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
UButton* Play = WidgetTree->ConstructWidget<UButton>(UButton::StaticClass(), TEXT("PlaySoloBtn"));
|
||||||
|
StyleMenuButton(Play);
|
||||||
|
USizeBox* S = WidgetTree->ConstructWidget<USizeBox>(USizeBox::StaticClass(), TEXT("PlaySoloSize"));
|
||||||
|
S->SetWidthOverride(400.f);
|
||||||
|
S->SetHeightOverride(46.f);
|
||||||
|
S->SetContent(Play);
|
||||||
|
Play->SetContent(MakeLabel(TEXT("PlaySoloLbl"), TEXT("Play World"), 18.f));
|
||||||
|
Play->OnClicked.AddDynamic(this, &UVocationMainMenuWidget::OnPlaySoloClicked);
|
||||||
|
AddPadded(SoloPage, S, 8.f);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
UButton* Back = WidgetTree->ConstructWidget<UButton>(UButton::StaticClass(), TEXT("SoloBackBtn"));
|
||||||
|
StyleMenuButton(Back);
|
||||||
|
USizeBox* S = WidgetTree->ConstructWidget<USizeBox>(USizeBox::StaticClass(), TEXT("SoloBackSize"));
|
||||||
|
S->SetWidthOverride(400.f);
|
||||||
|
S->SetHeightOverride(40.f);
|
||||||
|
S->SetContent(Back);
|
||||||
|
Back->SetContent(MakeLabel(TEXT("SoloBackLbl"), TEXT("Back"), 16.f));
|
||||||
|
Back->OnClicked.AddDynamic(this, &UVocationMainMenuWidget::OnBackClicked);
|
||||||
|
AddPadded(SoloPage, S, 4.f);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Multiplayer page ----
|
||||||
|
UVerticalBox* MpPage = WidgetTree->ConstructWidget<UVerticalBox>(UVerticalBox::StaticClass(), TEXT("MpPage"));
|
||||||
|
PageSwitcher->AddChild(MpPage);
|
||||||
|
AddPadded(MpPage, MakeLabel(TEXT("MpTitle"), TEXT("Multiplayer"), 28.f), 4.f);
|
||||||
|
|
||||||
|
auto AddField = [&](UVerticalBox* Page, const TCHAR* LabelName, const TCHAR* Label, UWidget* Field, const TCHAR* SizeName)
|
||||||
|
{
|
||||||
|
AddPadded(Page, MakeLabel(LabelName, Label, 13.f), 2.f);
|
||||||
|
USizeBox* S = WidgetTree->ConstructWidget<USizeBox>(USizeBox::StaticClass(), SizeName);
|
||||||
|
S->SetWidthOverride(400.f);
|
||||||
|
S->SetContent(Field);
|
||||||
|
AddPadded(Page, S, 2.f);
|
||||||
|
};
|
||||||
|
|
||||||
|
AddPadded(MpPage, MakeLabel(TEXT("MpMapLbl"), TEXT("Map"), 13.f), 2.f);
|
||||||
|
MpMapCombo = WidgetTree->ConstructWidget<UComboBoxString>(UComboBoxString::StaticClass(), TEXT("MpMapCombo"));
|
||||||
|
{
|
||||||
|
USizeBox* S = WidgetTree->ConstructWidget<USizeBox>(USizeBox::StaticClass(), TEXT("MpMapComboSize"));
|
||||||
|
S->SetWidthOverride(400.f);
|
||||||
|
S->SetContent(MpMapCombo);
|
||||||
|
AddPadded(MpPage, S, 2.f);
|
||||||
|
}
|
||||||
|
|
||||||
|
BackendCombo = WidgetTree->ConstructWidget<UComboBoxString>(UComboBoxString::StaticClass(), TEXT("BackendCombo"));
|
||||||
|
BackendCombo->AddOption(TEXT("Auto"));
|
||||||
|
BackendCombo->AddOption(TEXT("Steam"));
|
||||||
|
BackendCombo->AddOption(TEXT("LAN"));
|
||||||
|
BackendCombo->SetSelectedIndex(0);
|
||||||
|
AddField(MpPage, TEXT("BackendLbl"), TEXT("Network"), BackendCombo, TEXT("BackendSize"));
|
||||||
|
|
||||||
|
ServerNameBox = WidgetTree->ConstructWidget<UEditableTextBox>(UEditableTextBox::StaticClass(), TEXT("ServerNameBox"));
|
||||||
|
ServerNameBox->SetText(FText::FromString(TEXT("VocationLife")));
|
||||||
|
AddField(MpPage, TEXT("NameLbl"), TEXT("Server Name"), ServerNameBox, TEXT("NameSize"));
|
||||||
|
|
||||||
|
PortBox = WidgetTree->ConstructWidget<UEditableTextBox>(UEditableTextBox::StaticClass(), TEXT("PortBox"));
|
||||||
|
PortBox->SetText(FText::FromString(TEXT("7777")));
|
||||||
|
AddField(MpPage, TEXT("PortLbl"), TEXT("Host Port"), PortBox, TEXT("PortSize"));
|
||||||
|
|
||||||
|
MaxPlayersBox = WidgetTree->ConstructWidget<UEditableTextBox>(UEditableTextBox::StaticClass(), TEXT("MaxPlayersBox"));
|
||||||
|
MaxPlayersBox->SetText(FText::FromString(TEXT("4")));
|
||||||
|
AddField(MpPage, TEXT("MaxLbl"), TEXT("Max Players"), MaxPlayersBox, TEXT("MaxSize"));
|
||||||
|
|
||||||
|
auto AddMpButton = [&](const TCHAR* Name, const TCHAR* Label, TFunction<void()> Bind)
|
||||||
|
{
|
||||||
|
UButton* B = WidgetTree->ConstructWidget<UButton>(UButton::StaticClass(), Name);
|
||||||
|
StyleMenuButton(B);
|
||||||
|
USizeBox* S = WidgetTree->ConstructWidget<USizeBox>(USizeBox::StaticClass(), *FString::Printf(TEXT("%sSize"), Name));
|
||||||
|
S->SetWidthOverride(400.f);
|
||||||
|
S->SetHeightOverride(42.f);
|
||||||
|
S->SetContent(B);
|
||||||
|
B->SetContent(MakeLabel(*FString::Printf(TEXT("%sLbl"), Name), Label, 16.f));
|
||||||
|
Bind();
|
||||||
|
AddPadded(MpPage, S, 4.f);
|
||||||
|
return B;
|
||||||
|
};
|
||||||
|
|
||||||
|
{
|
||||||
|
UButton* B = WidgetTree->ConstructWidget<UButton>(UButton::StaticClass(), TEXT("HostBtn"));
|
||||||
|
StyleMenuButton(B);
|
||||||
|
USizeBox* S = WidgetTree->ConstructWidget<USizeBox>(USizeBox::StaticClass(), TEXT("HostBtnSize"));
|
||||||
|
S->SetWidthOverride(400.f); S->SetHeightOverride(42.f); S->SetContent(B);
|
||||||
|
B->SetContent(MakeLabel(TEXT("HostBtnLbl"), TEXT("Host Game"), 16.f));
|
||||||
|
B->OnClicked.AddDynamic(this, &UVocationMainMenuWidget::OnHostClicked);
|
||||||
|
AddPadded(MpPage, S, 4.f);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
UButton* B = WidgetTree->ConstructWidget<UButton>(UButton::StaticClass(), TEXT("FindSteamBtn"));
|
||||||
|
StyleMenuButton(B);
|
||||||
|
USizeBox* S = WidgetTree->ConstructWidget<USizeBox>(USizeBox::StaticClass(), TEXT("FindSteamBtnSize"));
|
||||||
|
S->SetWidthOverride(400.f); S->SetHeightOverride(42.f); S->SetContent(B);
|
||||||
|
B->SetContent(MakeLabel(TEXT("FindSteamBtnLbl"), TEXT("Find Steam Games"), 16.f));
|
||||||
|
B->OnClicked.AddDynamic(this, &UVocationMainMenuWidget::OnFindSteamClicked);
|
||||||
|
AddPadded(MpPage, S, 4.f);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
UButton* B = WidgetTree->ConstructWidget<UButton>(UButton::StaticClass(), TEXT("FindLanBtn"));
|
||||||
|
StyleMenuButton(B);
|
||||||
|
USizeBox* S = WidgetTree->ConstructWidget<USizeBox>(USizeBox::StaticClass(), TEXT("FindLanBtnSize"));
|
||||||
|
S->SetWidthOverride(400.f); S->SetHeightOverride(42.f); S->SetContent(B);
|
||||||
|
B->SetContent(MakeLabel(TEXT("FindLanBtnLbl"), TEXT("Find LAN Games"), 16.f));
|
||||||
|
B->OnClicked.AddDynamic(this, &UVocationMainMenuWidget::OnFindLanClicked);
|
||||||
|
AddPadded(MpPage, S, 4.f);
|
||||||
|
}
|
||||||
|
|
||||||
|
SessionCombo = WidgetTree->ConstructWidget<UComboBoxString>(UComboBoxString::StaticClass(), TEXT("SessionCombo"));
|
||||||
|
AddField(MpPage, TEXT("SessionsLbl"), TEXT("Found Sessions"), SessionCombo, TEXT("SessionSize"));
|
||||||
|
{
|
||||||
|
UButton* B = WidgetTree->ConstructWidget<UButton>(UButton::StaticClass(), TEXT("JoinBtn"));
|
||||||
|
StyleMenuButton(B);
|
||||||
|
USizeBox* S = WidgetTree->ConstructWidget<USizeBox>(USizeBox::StaticClass(), TEXT("JoinBtnSize"));
|
||||||
|
S->SetWidthOverride(400.f); S->SetHeightOverride(42.f); S->SetContent(B);
|
||||||
|
B->SetContent(MakeLabel(TEXT("JoinBtnLbl"), TEXT("Join Selected"), 16.f));
|
||||||
|
B->OnClicked.AddDynamic(this, &UVocationMainMenuWidget::OnJoinSelectedClicked);
|
||||||
|
AddPadded(MpPage, S, 4.f);
|
||||||
|
}
|
||||||
|
|
||||||
|
IpBox = WidgetTree->ConstructWidget<UEditableTextBox>(UEditableTextBox::StaticClass(), TEXT("IpBox"));
|
||||||
|
IpBox->SetText(FText::FromString(TEXT("127.0.0.1")));
|
||||||
|
AddField(MpPage, TEXT("IpLbl"), TEXT("Direct Connect IP"), IpBox, TEXT("IpSize"));
|
||||||
|
|
||||||
|
JoinPortBox = WidgetTree->ConstructWidget<UEditableTextBox>(UEditableTextBox::StaticClass(), TEXT("JoinPortBox"));
|
||||||
|
JoinPortBox->SetText(FText::FromString(TEXT("7777")));
|
||||||
|
AddField(MpPage, TEXT("JoinPortLbl"), TEXT("Port"), JoinPortBox, TEXT("JoinPortSize"));
|
||||||
|
{
|
||||||
|
UButton* B = WidgetTree->ConstructWidget<UButton>(UButton::StaticClass(), TEXT("ConnectBtn"));
|
||||||
|
StyleMenuButton(B);
|
||||||
|
USizeBox* S = WidgetTree->ConstructWidget<USizeBox>(USizeBox::StaticClass(), TEXT("ConnectBtnSize"));
|
||||||
|
S->SetWidthOverride(400.f); S->SetHeightOverride(42.f); S->SetContent(B);
|
||||||
|
B->SetContent(MakeLabel(TEXT("ConnectBtnLbl"), TEXT("Connect by IP:Port"), 16.f));
|
||||||
|
B->OnClicked.AddDynamic(this, &UVocationMainMenuWidget::OnConnectIpClicked);
|
||||||
|
AddPadded(MpPage, S, 4.f);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
UButton* B = WidgetTree->ConstructWidget<UButton>(UButton::StaticClass(), TEXT("MpBackBtn"));
|
||||||
|
StyleMenuButton(B);
|
||||||
|
USizeBox* S = WidgetTree->ConstructWidget<USizeBox>(USizeBox::StaticClass(), TEXT("MpBackBtnSize"));
|
||||||
|
S->SetWidthOverride(400.f); S->SetHeightOverride(42.f); S->SetContent(B);
|
||||||
|
B->SetContent(MakeLabel(TEXT("MpBackBtnLbl"), TEXT("Back"), 16.f));
|
||||||
|
B->OnClicked.AddDynamic(this, &UVocationMainMenuWidget::OnBackClicked);
|
||||||
|
AddPadded(MpPage, S, 4.f);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Options page ----
|
||||||
|
UVerticalBox* OptPage = WidgetTree->ConstructWidget<UVerticalBox>(UVerticalBox::StaticClass(), TEXT("OptPage"));
|
||||||
|
PageSwitcher->AddChild(OptPage);
|
||||||
|
AddPadded(OptPage, MakeLabel(TEXT("OptTitle"), TEXT("Options"), 28.f), 4.f);
|
||||||
|
AddPadded(OptPage, MakeLabel(TEXT("GfxLbl"), TEXT("Graphics Preset"), 14.f), 4.f);
|
||||||
|
|
||||||
|
auto AddGfxButton = [&](const TCHAR* Name, const TCHAR* Label)
|
||||||
|
{
|
||||||
|
UButton* B = WidgetTree->ConstructWidget<UButton>(UButton::StaticClass(), Name);
|
||||||
|
StyleMenuButton(B);
|
||||||
|
USizeBox* S = WidgetTree->ConstructWidget<USizeBox>(USizeBox::StaticClass(), *FString::Printf(TEXT("%sSize"), Name));
|
||||||
|
S->SetWidthOverride(400.f);
|
||||||
|
S->SetHeightOverride(40.f);
|
||||||
|
S->SetContent(B);
|
||||||
|
B->SetContent(MakeLabel(*FString::Printf(TEXT("%sLbl"), Name), Label, 16.f));
|
||||||
|
AddPadded(OptPage, S, 4.f);
|
||||||
|
return B;
|
||||||
|
};
|
||||||
|
|
||||||
|
AddGfxButton(TEXT("GfxLow"), TEXT("Low"))->OnClicked.AddDynamic(this, &UVocationMainMenuWidget::OnGraphicsLow);
|
||||||
|
AddGfxButton(TEXT("GfxMed"), TEXT("Medium"))->OnClicked.AddDynamic(this, &UVocationMainMenuWidget::OnGraphicsMedium);
|
||||||
|
AddGfxButton(TEXT("GfxHigh"), TEXT("High"))->OnClicked.AddDynamic(this, &UVocationMainMenuWidget::OnGraphicsHigh);
|
||||||
|
AddGfxButton(TEXT("GfxUltra"), TEXT("Ultra"))->OnClicked.AddDynamic(this, &UVocationMainMenuWidget::OnGraphicsUltra);
|
||||||
|
AddGfxButton(TEXT("GfxRT"), TEXT("Ray Tracing"))->OnClicked.AddDynamic(this, &UVocationMainMenuWidget::OnGraphicsRayTracing);
|
||||||
|
AddGfxButton(TEXT("OptBack"), TEXT("Back"))->OnClicked.AddDynamic(this, &UVocationMainMenuWidget::OnBackClicked);
|
||||||
|
|
||||||
|
StatusText = MakeLabel(TEXT("Status"), TEXT(""), 13.f);
|
||||||
|
StatusText->SetColorAndOpacity(FSlateColor(FLinearColor(0.75f, 0.8f, 0.85f, 1.f)));
|
||||||
|
AddPadded(CenterColumn, StatusText, 12.f);
|
||||||
|
|
||||||
|
VersionText = MakeLabel(TEXT("Version"), TEXT("VocationLife 0.1.0"), 12.f);
|
||||||
|
VersionText->SetJustification(ETextJustify::Left);
|
||||||
|
if (UOverlaySlot* VerSlot = Overlay->AddChildToOverlay(VersionText))
|
||||||
|
{
|
||||||
|
VerSlot->SetHorizontalAlignment(HAlign_Left);
|
||||||
|
VerSlot->SetVerticalAlignment(VAlign_Bottom);
|
||||||
|
VerSlot->SetPadding(FMargin(16.f));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UVocationMainMenuWidget::ShowPage(EMenuPage Page)
|
||||||
|
{
|
||||||
|
if (PageSwitcher)
|
||||||
|
{
|
||||||
|
PageSwitcher->SetActiveWidgetIndex(static_cast<int32>(Page));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UVocationMainMenuWidget::RefreshMaps()
|
||||||
|
{
|
||||||
|
CachedMaps = UVocationServerSettings::GetAvailableMaps();
|
||||||
|
auto Fill = [this](UComboBoxString* Combo)
|
||||||
|
{
|
||||||
|
if (!Combo)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Combo->ClearOptions();
|
||||||
|
for (const FVocationMapOption& Map : CachedMaps)
|
||||||
|
{
|
||||||
|
Combo->AddOption(Map.DisplayName);
|
||||||
|
}
|
||||||
|
if (CachedMaps.Num() > 0)
|
||||||
|
{
|
||||||
|
Combo->SetSelectedIndex(0);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Fill(MapCombo);
|
||||||
|
Fill(MpMapCombo);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UVocationMainMenuWidget::SetStatus(const FString& Message)
|
||||||
|
{
|
||||||
|
if (StatusText)
|
||||||
|
{
|
||||||
|
StatusText->SetText(FText::FromString(Message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UVocationMainMenuWidget::LeaveStartMenu()
|
||||||
|
{
|
||||||
|
if (UVocationGameInstance* GI = GetGameInstance<UVocationGameInstance>())
|
||||||
|
{
|
||||||
|
GI->MarkGameplayStarted();
|
||||||
|
}
|
||||||
|
if (APlayerController* PC = GetOwningPlayer())
|
||||||
|
{
|
||||||
|
PC->bShowMouseCursor = false;
|
||||||
|
PC->SetInputMode(FInputModeGameOnly());
|
||||||
|
}
|
||||||
|
RemoveFromParent();
|
||||||
|
}
|
||||||
|
|
||||||
|
void UVocationMainMenuWidget::OnSingleplayerClicked() { ShowPage(EMenuPage::Singleplayer); }
|
||||||
|
void UVocationMainMenuWidget::OnMultiplayerClicked() { ShowPage(EMenuPage::Multiplayer); }
|
||||||
|
void UVocationMainMenuWidget::OnOptionsClicked() { ShowPage(EMenuPage::Options); }
|
||||||
|
void UVocationMainMenuWidget::OnBackClicked() { ShowPage(EMenuPage::Main); }
|
||||||
|
|
||||||
|
void UVocationMainMenuWidget::OnQuitClicked()
|
||||||
|
{
|
||||||
|
if (APlayerController* PC = GetOwningPlayer())
|
||||||
|
{
|
||||||
|
UKismetSystemLibrary::QuitGame(this, PC, EQuitPreference::Quit, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UVocationMainMenuWidget::OnPlaySoloClicked()
|
||||||
|
{
|
||||||
|
if (UVocationSessionSubsystem* Sessions = GetGameInstance()->GetSubsystem<UVocationSessionSubsystem>())
|
||||||
|
{
|
||||||
|
const int32 MapIndex = MapCombo ? MapCombo->GetSelectedIndex() : 0;
|
||||||
|
const FString MapPath = CachedMaps.IsValidIndex(MapIndex) ? CachedMaps[MapIndex].MapPath : TEXT("/Engine/Maps/Entry");
|
||||||
|
LeaveStartMenu();
|
||||||
|
Sessions->TravelToMap(MapPath, false, 7777);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UVocationMainMenuWidget::OnHostClicked()
|
||||||
|
{
|
||||||
|
UVocationSessionSubsystem* Sessions = GetGameInstance() ? GetGameInstance()->GetSubsystem<UVocationSessionSubsystem>() : nullptr;
|
||||||
|
if (!Sessions)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int32 MapIndex = MpMapCombo ? MpMapCombo->GetSelectedIndex() : 0;
|
||||||
|
const FString MapPath = CachedMaps.IsValidIndex(MapIndex) ? CachedMaps[MapIndex].MapPath : TEXT("/Engine/Maps/Entry");
|
||||||
|
const int32 Port = PortBox ? FCString::Atoi(*PortBox->GetText().ToString()) : 7777;
|
||||||
|
const int32 MaxPlayers = MaxPlayersBox ? FCString::Atoi(*MaxPlayersBox->GetText().ToString()) : 4;
|
||||||
|
const FString Name = ServerNameBox ? ServerNameBox->GetText().ToString() : TEXT("VocationLife");
|
||||||
|
|
||||||
|
EVocationNetBackend Backend = EVocationNetBackend::Auto;
|
||||||
|
const int32 BackendIndex = BackendCombo ? BackendCombo->GetSelectedIndex() : 0;
|
||||||
|
if (BackendIndex == 1) Backend = EVocationNetBackend::Steam;
|
||||||
|
if (BackendIndex == 2) Backend = EVocationNetBackend::LAN;
|
||||||
|
|
||||||
|
SetStatus(TEXT("Hosting..."));
|
||||||
|
LeaveStartMenu();
|
||||||
|
Sessions->HostSession(MaxPlayers, Name, MapPath, Port, Backend, Backend == EVocationNetBackend::LAN);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UVocationMainMenuWidget::OnFindSteamClicked()
|
||||||
|
{
|
||||||
|
if (UVocationSessionSubsystem* Sessions = GetGameInstance()->GetSubsystem<UVocationSessionSubsystem>())
|
||||||
|
{
|
||||||
|
SetStatus(TEXT("Searching Steam..."));
|
||||||
|
Sessions->FindSessions(EVocationNetBackend::Steam, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UVocationMainMenuWidget::OnFindLanClicked()
|
||||||
|
{
|
||||||
|
if (UVocationSessionSubsystem* Sessions = GetGameInstance()->GetSubsystem<UVocationSessionSubsystem>())
|
||||||
|
{
|
||||||
|
SetStatus(TEXT("Searching LAN..."));
|
||||||
|
Sessions->FindSessions(EVocationNetBackend::LAN, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UVocationMainMenuWidget::OnJoinSelectedClicked()
|
||||||
|
{
|
||||||
|
if (UVocationSessionSubsystem* Sessions = GetGameInstance()->GetSubsystem<UVocationSessionSubsystem>())
|
||||||
|
{
|
||||||
|
const int32 Index = SessionCombo ? SessionCombo->GetSelectedIndex() : INDEX_NONE;
|
||||||
|
if (Index == INDEX_NONE)
|
||||||
|
{
|
||||||
|
SetStatus(TEXT("No session selected"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
SetStatus(TEXT("Joining..."));
|
||||||
|
LeaveStartMenu();
|
||||||
|
Sessions->JoinSessionByIndex(Index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UVocationMainMenuWidget::OnConnectIpClicked()
|
||||||
|
{
|
||||||
|
if (UVocationSessionSubsystem* Sessions = GetGameInstance()->GetSubsystem<UVocationSessionSubsystem>())
|
||||||
|
{
|
||||||
|
const FString IP = IpBox ? IpBox->GetText().ToString() : TEXT("127.0.0.1");
|
||||||
|
const int32 Port = JoinPortBox ? FCString::Atoi(*JoinPortBox->GetText().ToString()) : 7777;
|
||||||
|
SetStatus(FString::Printf(TEXT("Connecting %s:%d"), *IP, Port));
|
||||||
|
LeaveStartMenu();
|
||||||
|
Sessions->ConnectByIP(IP, Port);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UVocationMainMenuWidget::OnGraphicsLow()
|
||||||
|
{
|
||||||
|
if (UVocationGameInstance* GI = GetGameInstance<UVocationGameInstance>())
|
||||||
|
{
|
||||||
|
GI->SetGraphicsPreset(EVocationGraphicsPreset::Low, false);
|
||||||
|
SetStatus(TEXT("Graphics: Low"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UVocationMainMenuWidget::OnGraphicsMedium()
|
||||||
|
{
|
||||||
|
if (UVocationGameInstance* GI = GetGameInstance<UVocationGameInstance>())
|
||||||
|
{
|
||||||
|
GI->SetGraphicsPreset(EVocationGraphicsPreset::Medium, false);
|
||||||
|
SetStatus(TEXT("Graphics: Medium"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UVocationMainMenuWidget::OnGraphicsHigh()
|
||||||
|
{
|
||||||
|
if (UVocationGameInstance* GI = GetGameInstance<UVocationGameInstance>())
|
||||||
|
{
|
||||||
|
GI->SetGraphicsPreset(EVocationGraphicsPreset::High, false);
|
||||||
|
SetStatus(TEXT("Graphics: High"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UVocationMainMenuWidget::OnGraphicsUltra()
|
||||||
|
{
|
||||||
|
if (UVocationGameInstance* GI = GetGameInstance<UVocationGameInstance>())
|
||||||
|
{
|
||||||
|
GI->SetGraphicsPreset(EVocationGraphicsPreset::Ultra, false);
|
||||||
|
SetStatus(TEXT("Graphics: Ultra"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UVocationMainMenuWidget::OnGraphicsRayTracing()
|
||||||
|
{
|
||||||
|
if (UVocationGameInstance* GI = GetGameInstance<UVocationGameInstance>())
|
||||||
|
{
|
||||||
|
GI->SetGraphicsPreset(EVocationGraphicsPreset::RayTracing, true);
|
||||||
|
SetStatus(TEXT("Graphics: Ray Tracing"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UVocationMainMenuWidget::OnSessionSearchDetailed(const TArray<FVocationSessionSearchEntry>& Sessions)
|
||||||
|
{
|
||||||
|
CachedSessions = Sessions;
|
||||||
|
if (!SessionCombo)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
SessionCombo->ClearOptions();
|
||||||
|
for (const FVocationSessionSearchEntry& Entry : Sessions)
|
||||||
|
{
|
||||||
|
SessionCombo->AddOption(FString::Printf(TEXT("%s [%s] %d/%d"),
|
||||||
|
*Entry.DisplayName,
|
||||||
|
*Entry.MapName,
|
||||||
|
Entry.MaxPlayers - Entry.OpenSlots,
|
||||||
|
Entry.MaxPlayers));
|
||||||
|
}
|
||||||
|
if (Sessions.Num() > 0)
|
||||||
|
{
|
||||||
|
SessionCombo->SetSelectedIndex(0);
|
||||||
|
SetStatus(FString::Printf(TEXT("Found %d session(s)"), Sessions.Num()));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
SetStatus(TEXT("No sessions found"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UVocationMainMenuWidget::OnSessionJoined(bool bSuccess)
|
||||||
|
{
|
||||||
|
SetStatus(bSuccess ? TEXT("Connected") : TEXT("Connection failed"));
|
||||||
|
if (bSuccess)
|
||||||
|
{
|
||||||
|
LeaveStartMenu();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -5,6 +5,7 @@
|
|||||||
#include "VocationHUD.h"
|
#include "VocationHUD.h"
|
||||||
#include "VocationSessionSubsystem.h"
|
#include "VocationSessionSubsystem.h"
|
||||||
#include "VocationGameInstance.h"
|
#include "VocationGameInstance.h"
|
||||||
|
#include "VocationMainMenuWidget.h"
|
||||||
#include "EnhancedInputComponent.h"
|
#include "EnhancedInputComponent.h"
|
||||||
#include "EnhancedInputSubsystems.h"
|
#include "EnhancedInputSubsystems.h"
|
||||||
#include "InputAction.h"
|
#include "InputAction.h"
|
||||||
@ -29,7 +30,7 @@ void AVocationPlayerController::BeginPlay()
|
|||||||
LocalPlayer->GetSubsystem<UEnhancedInputLocalPlayerSubsystem>())
|
LocalPlayer->GetSubsystem<UEnhancedInputLocalPlayerSubsystem>())
|
||||||
{
|
{
|
||||||
Subsystem->AddMappingContext(SharedMappingContext, SharedMappingPriority);
|
Subsystem->AddMappingContext(SharedMappingContext, SharedMappingPriority);
|
||||||
Subsystem->AddMappingContext(FirstPersonMappingContext, ModeMappingPriority);
|
Subsystem->AddMappingContext(TopDownMappingContext, ModeMappingPriority);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -42,6 +43,12 @@ void AVocationPlayerController::BeginPlay()
|
|||||||
{
|
{
|
||||||
GI->SetGraphicsPreset(GI->GetGraphicsPreset(), GI->IsRayTracingEnabled());
|
GI->SetGraphicsPreset(GI->GetGraphicsPreset(), GI->IsRayTracingEnabled());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Start menu only once per game session — not reopenable in-game (M reserved for map).
|
||||||
|
if (IsLocalController() && !IsRunningDedicatedServer())
|
||||||
|
{
|
||||||
|
ShowStartMenuIfNeeded();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void AVocationPlayerController::SetupInputComponent()
|
void AVocationPlayerController::SetupInputComponent()
|
||||||
@ -110,11 +117,39 @@ void AVocationPlayerController::ClearInteractionPrompt()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void AVocationPlayerController::ShowStartMenuIfNeeded()
|
||||||
|
{
|
||||||
|
UVocationGameInstance* GI = GetGameInstance<UVocationGameInstance>();
|
||||||
|
if (!GI || GI->HasGameplayStarted())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (MultiplayerMenu && MultiplayerMenu->IsInViewport())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
MultiplayerMenu = CreateWidget<UVocationMainMenuWidget>(this, UVocationMainMenuWidget::StaticClass());
|
||||||
|
if (MultiplayerMenu)
|
||||||
|
{
|
||||||
|
MultiplayerMenu->AddToViewport(100);
|
||||||
|
bShowMouseCursor = true;
|
||||||
|
FInputModeUIOnly Mode;
|
||||||
|
Mode.SetLockMouseToViewportBehavior(EMouseLockMode::DoNotLock);
|
||||||
|
SetInputMode(Mode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void AVocationPlayerController::HostCoopGame()
|
void AVocationPlayerController::HostCoopGame()
|
||||||
{
|
{
|
||||||
if (UVocationSessionSubsystem* Sessions = GetGameInstance()->GetSubsystem<UVocationSessionSubsystem>())
|
if (UVocationSessionSubsystem* Sessions = GetGameInstance()->GetSubsystem<UVocationSessionSubsystem>())
|
||||||
{
|
{
|
||||||
Sessions->HostSession(4, TEXT("VocationLife"));
|
if (UVocationGameInstance* GI = GetGameInstance<UVocationGameInstance>())
|
||||||
|
{
|
||||||
|
GI->MarkGameplayStarted();
|
||||||
|
}
|
||||||
|
Sessions->HostSession(4, TEXT("VocationLife"), TEXT("/Engine/Maps/Entry"), 7777, EVocationNetBackend::Auto, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -175,7 +210,7 @@ void AVocationPlayerController::CreateRuntimeInputAssets()
|
|||||||
SharedMappingContext->MapKey(JumpAction, EKeys::SpaceBar);
|
SharedMappingContext->MapKey(JumpAction, EKeys::SpaceBar);
|
||||||
SharedMappingContext->MapKey(InteractAction, EKeys::E);
|
SharedMappingContext->MapKey(InteractAction, EKeys::E);
|
||||||
SharedMappingContext->MapKey(AttackAction, EKeys::LeftMouseButton);
|
SharedMappingContext->MapKey(AttackAction, EKeys::LeftMouseButton);
|
||||||
SharedMappingContext->MapKey(ToggleCameraAction, EKeys::V);
|
SharedMappingContext->MapKey(ToggleCameraAction, EKeys::C);
|
||||||
SharedMappingContext->MapKey(InventoryAction, EKeys::I);
|
SharedMappingContext->MapKey(InventoryAction, EKeys::I);
|
||||||
SharedMappingContext->MapKey(PauseAction, EKeys::Escape);
|
SharedMappingContext->MapKey(PauseAction, EKeys::Escape);
|
||||||
SharedMappingContext->MapKey(JumpAction, EKeys::Gamepad_FaceButton_Bottom);
|
SharedMappingContext->MapKey(JumpAction, EKeys::Gamepad_FaceButton_Bottom);
|
||||||
|
|||||||
39
Source/VocationLife/Private/VocationServerSettings.cpp
Normal file
39
Source/VocationLife/Private/VocationServerSettings.cpp
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
// Copyright VocationLife Project. All Rights Reserved.
|
||||||
|
|
||||||
|
#include "VocationServerSettings.h"
|
||||||
|
#include "Misc/ConfigCacheIni.h"
|
||||||
|
#include "Misc/Paths.h"
|
||||||
|
|
||||||
|
void UVocationServerSettings::LoadFromDisk()
|
||||||
|
{
|
||||||
|
const FString ConfigPath = FPaths::ProjectConfigDir() / TEXT("ServerConfig.ini");
|
||||||
|
LoadConfig(GetClass(), *ConfigPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UVocationServerSettings::SaveToDisk()
|
||||||
|
{
|
||||||
|
const FString ConfigPath = FPaths::ProjectConfigDir() / TEXT("ServerConfig.ini");
|
||||||
|
SaveConfig(CPF_Config, *ConfigPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
TArray<FVocationMapOption> UVocationServerSettings::GetAvailableMaps()
|
||||||
|
{
|
||||||
|
TArray<FVocationMapOption> Maps;
|
||||||
|
|
||||||
|
FVocationMapOption Entry;
|
||||||
|
Entry.DisplayName = TEXT("Test Entry (Auto World)");
|
||||||
|
Entry.MapPath = TEXT("/Engine/Maps/Entry");
|
||||||
|
Maps.Add(Entry);
|
||||||
|
|
||||||
|
FVocationMapOption Intro;
|
||||||
|
Intro.DisplayName = TEXT("Intro Room (Template)");
|
||||||
|
Intro.MapPath = TEXT("/Game/DemoTemplate/_Core/Lvl_IntroRoom");
|
||||||
|
Maps.Add(Intro);
|
||||||
|
|
||||||
|
FVocationMapOption FirstPerson;
|
||||||
|
FirstPerson.DisplayName = TEXT("First Person Level");
|
||||||
|
FirstPerson.MapPath = TEXT("/Game/FirstPerson/Lvl_FirstPerson");
|
||||||
|
Maps.Add(FirstPerson);
|
||||||
|
|
||||||
|
return Maps;
|
||||||
|
}
|
||||||
@ -2,54 +2,132 @@
|
|||||||
|
|
||||||
#include "VocationSessionSubsystem.h"
|
#include "VocationSessionSubsystem.h"
|
||||||
#include "OnlineSubsystem.h"
|
#include "OnlineSubsystem.h"
|
||||||
|
#include "OnlineSubsystemUtils.h"
|
||||||
|
#include "OnlineSubsystemNames.h"
|
||||||
#include "OnlineSessionSettings.h"
|
#include "OnlineSessionSettings.h"
|
||||||
#include "Interfaces/OnlineSessionInterface.h"
|
#include "Interfaces/OnlineSessionInterface.h"
|
||||||
|
#include "Online/OnlineSessionNames.h"
|
||||||
|
#include "Kismet/GameplayStatics.h"
|
||||||
|
#include "GameFramework/PlayerController.h"
|
||||||
|
#include "Engine/World.h"
|
||||||
|
#include "Engine/LocalPlayer.h"
|
||||||
|
|
||||||
static const FName VOCATION_SESSION_NAME = TEXT("VocationLifeSession");
|
static const FName VOCATION_SESSION_NAME = TEXT("VocationLifeSession");
|
||||||
|
static const FName VOCATION_SETTING_SESSION_NAME = TEXT("SESSION_NAME");
|
||||||
|
static const FName VOCATION_SETTING_MAPNAME = TEXT("VLMAPNAME");
|
||||||
|
static const FName VOCATION_SEARCH_PRESENCE = TEXT("PRESENCESEARCH");
|
||||||
|
|
||||||
void UVocationSessionSubsystem::HostSession(int32 MaxPlayers, const FString& SessionName)
|
void UVocationSessionSubsystem::Initialize(FSubsystemCollectionBase& Collection)
|
||||||
{
|
{
|
||||||
IOnlineSubsystem* OnlineSubsystem = IOnlineSubsystem::Get();
|
Super::Initialize(Collection);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UVocationSessionSubsystem::IsSteamAvailable() const
|
||||||
|
{
|
||||||
|
if (IOnlineSubsystem* Steam = IOnlineSubsystem::Get(STEAM_SUBSYSTEM))
|
||||||
|
{
|
||||||
|
return Steam->GetSessionInterface().IsValid();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
FString UVocationSessionSubsystem::GetActiveSubsystemName() const
|
||||||
|
{
|
||||||
|
if (IOnlineSubsystem* Subsystem = IOnlineSubsystem::Get())
|
||||||
|
{
|
||||||
|
return Subsystem->GetSubsystemName().ToString();
|
||||||
|
}
|
||||||
|
return TEXT("None");
|
||||||
|
}
|
||||||
|
|
||||||
|
TArray<FVocationMapOption> UVocationSessionSubsystem::GetMapList() const
|
||||||
|
{
|
||||||
|
return UVocationServerSettings::GetAvailableMaps();
|
||||||
|
}
|
||||||
|
|
||||||
|
IOnlineSubsystem* UVocationSessionSubsystem::ResolveSubsystem(EVocationNetBackend Backend) const
|
||||||
|
{
|
||||||
|
switch (Backend)
|
||||||
|
{
|
||||||
|
case EVocationNetBackend::Steam:
|
||||||
|
return IOnlineSubsystem::Get(STEAM_SUBSYSTEM);
|
||||||
|
case EVocationNetBackend::LAN:
|
||||||
|
return IOnlineSubsystem::Get(NULL_SUBSYSTEM);
|
||||||
|
case EVocationNetBackend::Auto:
|
||||||
|
default:
|
||||||
|
if (IsSteamAvailable())
|
||||||
|
{
|
||||||
|
return IOnlineSubsystem::Get(STEAM_SUBSYSTEM);
|
||||||
|
}
|
||||||
|
return IOnlineSubsystem::Get();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UVocationSessionSubsystem::HostSession(
|
||||||
|
int32 MaxPlayers,
|
||||||
|
const FString& SessionName,
|
||||||
|
const FString& MapPath,
|
||||||
|
int32 Port,
|
||||||
|
EVocationNetBackend Backend,
|
||||||
|
bool bLANOnly)
|
||||||
|
{
|
||||||
|
PendingMapPath = MapPath.IsEmpty() ? TEXT("/Engine/Maps/Entry") : MapPath;
|
||||||
|
PendingPort = Port > 0 ? Port : 7777;
|
||||||
|
|
||||||
|
IOnlineSubsystem* OnlineSubsystem = ResolveSubsystem(Backend);
|
||||||
if (!OnlineSubsystem)
|
if (!OnlineSubsystem)
|
||||||
{
|
{
|
||||||
OnSessionJoined.Broadcast(false);
|
// Fallback: start listen server without OSS
|
||||||
|
TravelToMap(PendingMapPath, true, PendingPort);
|
||||||
|
bIsHosting = true;
|
||||||
|
OnHostReady.Broadcast(true, FString::Printf(TEXT("%s?listen"), *PendingMapPath));
|
||||||
|
OnSessionJoined.Broadcast(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
IOnlineSessionPtr Sessions = OnlineSubsystem->GetSessionInterface();
|
IOnlineSessionPtr Sessions = OnlineSubsystem->GetSessionInterface();
|
||||||
if (!Sessions.IsValid())
|
if (!Sessions.IsValid())
|
||||||
{
|
{
|
||||||
OnSessionJoined.Broadcast(false);
|
TravelToMap(PendingMapPath, true, PendingPort);
|
||||||
|
bIsHosting = true;
|
||||||
|
OnHostReady.Broadcast(true, FString::Printf(TEXT("%s?listen"), *PendingMapPath));
|
||||||
|
OnSessionJoined.Broadcast(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Sessions->DestroySession(VOCATION_SESSION_NAME);
|
||||||
|
|
||||||
CreateSessionDelegateHandle = Sessions->AddOnCreateSessionCompleteDelegate_Handle(
|
CreateSessionDelegateHandle = Sessions->AddOnCreateSessionCompleteDelegate_Handle(
|
||||||
FOnCreateSessionCompleteDelegate::CreateUObject(this, &UVocationSessionSubsystem::OnCreateSessionComplete));
|
FOnCreateSessionCompleteDelegate::CreateUObject(this, &UVocationSessionSubsystem::OnCreateSessionComplete));
|
||||||
|
|
||||||
TSharedRef<FOnlineSessionSettings> SessionSettings = MakeShared<FOnlineSessionSettings>();
|
const bool bUseLAN = bLANOnly || OnlineSubsystem->GetSubsystemName() == NULL_SUBSYSTEM;
|
||||||
SessionSettings->bIsLANMatch = true;
|
|
||||||
SessionSettings->NumPublicConnections = MaxPlayers;
|
|
||||||
SessionSettings->bShouldAdvertise = true;
|
|
||||||
SessionSettings->bUsesPresence = true;
|
|
||||||
SessionSettings->Set(FName(TEXT("SESSION_NAME")), SessionName, EOnlineDataAdvertisementType::ViaOnlineServiceAndPing);
|
|
||||||
|
|
||||||
const ULocalPlayer* LocalPlayer = GetWorld() && GetWorld()->GetFirstLocalPlayerFromController()
|
FOnlineSessionSettings SessionSettings;
|
||||||
? GetWorld()->GetFirstLocalPlayerFromController()
|
SessionSettings.bIsLANMatch = bUseLAN;
|
||||||
: nullptr;
|
SessionSettings.NumPublicConnections = MaxPlayers;
|
||||||
|
SessionSettings.bShouldAdvertise = true;
|
||||||
|
SessionSettings.bAllowJoinInProgress = true;
|
||||||
|
SessionSettings.bAllowJoinViaPresence = !bUseLAN;
|
||||||
|
SessionSettings.bUsesPresence = !bUseLAN;
|
||||||
|
SessionSettings.bUseLobbiesIfAvailable = !bUseLAN;
|
||||||
|
SessionSettings.Set(VOCATION_SETTING_SESSION_NAME, SessionName, EOnlineDataAdvertisementType::ViaOnlineServiceAndPing);
|
||||||
|
SessionSettings.Set(VOCATION_SETTING_MAPNAME, PendingMapPath, EOnlineDataAdvertisementType::ViaOnlineServiceAndPing);
|
||||||
|
|
||||||
if (!Sessions->CreateSession(0, VOCATION_SESSION_NAME, *SessionSettings))
|
if (!Sessions->CreateSession(0, VOCATION_SESSION_NAME, SessionSettings))
|
||||||
{
|
{
|
||||||
Sessions->ClearOnCreateSessionCompleteDelegate_Handle(CreateSessionDelegateHandle);
|
Sessions->ClearOnCreateSessionCompleteDelegate_Handle(CreateSessionDelegateHandle);
|
||||||
|
OnHostReady.Broadcast(false, TEXT(""));
|
||||||
OnSessionJoined.Broadcast(false);
|
OnSessionJoined.Broadcast(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void UVocationSessionSubsystem::FindSessions()
|
void UVocationSessionSubsystem::FindSessions(EVocationNetBackend Backend, bool bLANOnly)
|
||||||
{
|
{
|
||||||
IOnlineSubsystem* OnlineSubsystem = IOnlineSubsystem::Get();
|
IOnlineSubsystem* OnlineSubsystem = ResolveSubsystem(Backend);
|
||||||
if (!OnlineSubsystem)
|
if (!OnlineSubsystem)
|
||||||
{
|
{
|
||||||
OnSessionSearchComplete.Broadcast({});
|
OnSessionSearchComplete.Broadcast({});
|
||||||
|
OnSessionSearchDetailed.Broadcast({});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -57,6 +135,7 @@ void UVocationSessionSubsystem::FindSessions()
|
|||||||
if (!Sessions.IsValid())
|
if (!Sessions.IsValid())
|
||||||
{
|
{
|
||||||
OnSessionSearchComplete.Broadcast({});
|
OnSessionSearchComplete.Broadcast({});
|
||||||
|
OnSessionSearchDetailed.Broadcast({});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,20 +143,33 @@ void UVocationSessionSubsystem::FindSessions()
|
|||||||
FOnFindSessionsCompleteDelegate::CreateUObject(this, &UVocationSessionSubsystem::OnFindSessionsComplete));
|
FOnFindSessionsCompleteDelegate::CreateUObject(this, &UVocationSessionSubsystem::OnFindSessionsComplete));
|
||||||
|
|
||||||
SessionSearch = MakeShared<FOnlineSessionSearch>();
|
SessionSearch = MakeShared<FOnlineSessionSearch>();
|
||||||
SessionSearch->bIsLanQuery = true;
|
SessionSearch->bIsLanQuery = bLANOnly || OnlineSubsystem->GetSubsystemName() == NULL_SUBSYSTEM;
|
||||||
SessionSearch->MaxSearchResults = 20;
|
SessionSearch->MaxSearchResults = 50;
|
||||||
SessionSearch->QuerySettings.Set(FName(TEXT("PRESENCESEARCH")), true, EOnlineComparisonOp::Equals);
|
if (!SessionSearch->bIsLanQuery)
|
||||||
|
{
|
||||||
|
SessionSearch->QuerySettings.Set(VOCATION_SEARCH_PRESENCE, true, EOnlineComparisonOp::Equals);
|
||||||
|
}
|
||||||
|
|
||||||
if (!Sessions->FindSessions(0, SessionSearch.ToSharedRef()))
|
if (!Sessions->FindSessions(0, SessionSearch.ToSharedRef()))
|
||||||
{
|
{
|
||||||
Sessions->ClearOnFindSessionsCompleteDelegate_Handle(FindSessionsDelegateHandle);
|
Sessions->ClearOnFindSessionsCompleteDelegate_Handle(FindSessionsDelegateHandle);
|
||||||
OnSessionSearchComplete.Broadcast({});
|
OnSessionSearchComplete.Broadcast({});
|
||||||
|
OnSessionSearchDetailed.Broadcast({});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void UVocationSessionSubsystem::JoinSessionByIndex(int32 SessionIndex)
|
void UVocationSessionSubsystem::JoinSessionByIndex(int32 SessionIndex)
|
||||||
{
|
{
|
||||||
IOnlineSubsystem* OnlineSubsystem = IOnlineSubsystem::Get();
|
IOnlineSubsystem* OnlineSubsystem = IOnlineSubsystem::Get();
|
||||||
|
if (IsSteamAvailable())
|
||||||
|
{
|
||||||
|
// Prefer the same subsystem that found the session if Steam is up
|
||||||
|
if (IOnlineSubsystem* Steam = IOnlineSubsystem::Get(STEAM_SUBSYSTEM))
|
||||||
|
{
|
||||||
|
OnlineSubsystem = Steam;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!OnlineSubsystem || !SessionSearch.IsValid())
|
if (!OnlineSubsystem || !SessionSearch.IsValid())
|
||||||
{
|
{
|
||||||
OnSessionJoined.Broadcast(false);
|
OnSessionJoined.Broadcast(false);
|
||||||
@ -101,15 +193,66 @@ void UVocationSessionSubsystem::JoinSessionByIndex(int32 SessionIndex)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void UVocationSessionSubsystem::ConnectByIP(const FString& Address, int32 Port)
|
||||||
|
{
|
||||||
|
UWorld* World = GetWorld();
|
||||||
|
if (!World)
|
||||||
|
{
|
||||||
|
OnSessionJoined.Broadcast(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
APlayerController* PC = World->GetFirstPlayerController();
|
||||||
|
if (!PC)
|
||||||
|
{
|
||||||
|
OnSessionJoined.Broadcast(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int32 UsePort = Port > 0 ? Port : 7777;
|
||||||
|
FString TravelURL = Address;
|
||||||
|
if (!TravelURL.Contains(TEXT(":")))
|
||||||
|
{
|
||||||
|
TravelURL = FString::Printf(TEXT("%s:%d"), *Address, UsePort);
|
||||||
|
}
|
||||||
|
|
||||||
|
PC->ClientTravel(TravelURL, TRAVEL_Absolute);
|
||||||
|
OnSessionJoined.Broadcast(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UVocationSessionSubsystem::TravelToMap(const FString& MapPath, bool bAsListenServer, int32 Port)
|
||||||
|
{
|
||||||
|
UWorld* World = GetWorld();
|
||||||
|
if (!World)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
FString Options;
|
||||||
|
if (bAsListenServer)
|
||||||
|
{
|
||||||
|
Options = FString::Printf(TEXT("listen?Port=%d"), Port > 0 ? Port : 7777);
|
||||||
|
}
|
||||||
|
|
||||||
|
UGameplayStatics::OpenLevel(World, FName(*MapPath), true, Options);
|
||||||
|
}
|
||||||
|
|
||||||
void UVocationSessionSubsystem::DestroySession()
|
void UVocationSessionSubsystem::DestroySession()
|
||||||
{
|
{
|
||||||
if (IOnlineSubsystem* OnlineSubsystem = IOnlineSubsystem::Get())
|
auto DestroyOn = [](IOnlineSubsystem* Subsystem)
|
||||||
{
|
{
|
||||||
if (IOnlineSessionPtr Sessions = OnlineSubsystem->GetSessionInterface())
|
if (Subsystem)
|
||||||
|
{
|
||||||
|
if (IOnlineSessionPtr Sessions = Subsystem->GetSessionInterface())
|
||||||
{
|
{
|
||||||
Sessions->DestroySession(VOCATION_SESSION_NAME);
|
Sessions->DestroySession(VOCATION_SESSION_NAME);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
DestroyOn(IOnlineSubsystem::Get());
|
||||||
|
DestroyOn(IOnlineSubsystem::Get(STEAM_SUBSYSTEM));
|
||||||
|
DestroyOn(IOnlineSubsystem::Get(NULL_SUBSYSTEM));
|
||||||
bIsHosting = false;
|
bIsHosting = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -122,45 +265,125 @@ void UVocationSessionSubsystem::OnCreateSessionComplete(FName SessionName, bool
|
|||||||
Sessions->ClearOnCreateSessionCompleteDelegate_Handle(CreateSessionDelegateHandle);
|
Sessions->ClearOnCreateSessionCompleteDelegate_Handle(CreateSessionDelegateHandle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (IOnlineSubsystem* Steam = IOnlineSubsystem::Get(STEAM_SUBSYSTEM))
|
||||||
|
{
|
||||||
|
if (IOnlineSessionPtr Sessions = Steam->GetSessionInterface())
|
||||||
|
{
|
||||||
|
Sessions->ClearOnCreateSessionCompleteDelegate_Handle(CreateSessionDelegateHandle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bIsHosting = bWasSuccessful;
|
bIsHosting = bWasSuccessful;
|
||||||
|
if (bWasSuccessful)
|
||||||
|
{
|
||||||
|
TravelToMap(PendingMapPath, true, PendingPort);
|
||||||
|
OnHostReady.Broadcast(true, FString::Printf(TEXT("%s?listen"), *PendingMapPath));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
OnHostReady.Broadcast(false, TEXT(""));
|
||||||
|
}
|
||||||
OnSessionJoined.Broadcast(bWasSuccessful);
|
OnSessionJoined.Broadcast(bWasSuccessful);
|
||||||
}
|
}
|
||||||
|
|
||||||
void UVocationSessionSubsystem::OnFindSessionsComplete(bool bWasSuccessful)
|
void UVocationSessionSubsystem::OnFindSessionsComplete(bool bWasSuccessful)
|
||||||
{
|
{
|
||||||
TArray<FString> SessionNames;
|
TArray<FString> SessionNames;
|
||||||
|
TArray<FVocationSessionSearchEntry> Detailed;
|
||||||
|
|
||||||
if (bWasSuccessful && SessionSearch.IsValid())
|
if (bWasSuccessful && SessionSearch.IsValid())
|
||||||
{
|
{
|
||||||
for (const FOnlineSessionSearchResult& Result : SessionSearch->SearchResults)
|
for (const FOnlineSessionSearchResult& Result : SessionSearch->SearchResults)
|
||||||
{
|
{
|
||||||
FString Name;
|
FVocationSessionSearchEntry Entry;
|
||||||
Result.Session.SessionSettings.Get(FName(TEXT("SESSION_NAME")), Name);
|
Result.Session.SessionSettings.Get(VOCATION_SETTING_SESSION_NAME, Entry.DisplayName);
|
||||||
SessionNames.Add(Name.IsEmpty() ? TEXT("VocationLife Server") : Name);
|
Result.Session.SessionSettings.Get(VOCATION_SETTING_MAPNAME, Entry.MapName);
|
||||||
|
Entry.MaxPlayers = Result.Session.SessionSettings.NumPublicConnections;
|
||||||
|
Entry.OpenSlots = Result.Session.NumOpenPublicConnections;
|
||||||
|
Entry.bIsLAN = Result.Session.SessionSettings.bIsLANMatch;
|
||||||
|
if (Entry.DisplayName.IsEmpty())
|
||||||
|
{
|
||||||
|
Entry.DisplayName = TEXT("VocationLife Server");
|
||||||
|
}
|
||||||
|
SessionNames.Add(Entry.DisplayName);
|
||||||
|
Detailed.Add(Entry);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (IOnlineSubsystem* OnlineSubsystem = IOnlineSubsystem::Get())
|
auto ClearFind = [](IOnlineSubsystem* Subsystem, FDelegateHandle& Handle)
|
||||||
{
|
{
|
||||||
if (IOnlineSessionPtr Sessions = OnlineSubsystem->GetSessionInterface())
|
if (Subsystem)
|
||||||
{
|
{
|
||||||
Sessions->ClearOnFindSessionsCompleteDelegate_Handle(FindSessionsDelegateHandle);
|
if (IOnlineSessionPtr Sessions = Subsystem->GetSessionInterface())
|
||||||
|
{
|
||||||
|
Sessions->ClearOnFindSessionsCompleteDelegate_Handle(Handle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
ClearFind(IOnlineSubsystem::Get(), FindSessionsDelegateHandle);
|
||||||
|
ClearFind(IOnlineSubsystem::Get(STEAM_SUBSYSTEM), FindSessionsDelegateHandle);
|
||||||
|
|
||||||
OnSessionSearchComplete.Broadcast(SessionNames);
|
OnSessionSearchComplete.Broadcast(SessionNames);
|
||||||
|
OnSessionSearchDetailed.Broadcast(Detailed);
|
||||||
}
|
}
|
||||||
|
|
||||||
void UVocationSessionSubsystem::OnJoinSessionComplete(FName SessionName, EOnJoinSessionCompleteResult::Type Result)
|
void UVocationSessionSubsystem::OnJoinSessionComplete(FName SessionName, EOnJoinSessionCompleteResult::Type Result)
|
||||||
{
|
{
|
||||||
if (IOnlineSubsystem* OnlineSubsystem = IOnlineSubsystem::Get())
|
auto ClearJoin = [](IOnlineSubsystem* Subsystem, FDelegateHandle& Handle)
|
||||||
{
|
{
|
||||||
if (IOnlineSessionPtr Sessions = OnlineSubsystem->GetSessionInterface())
|
if (Subsystem)
|
||||||
{
|
{
|
||||||
Sessions->ClearOnJoinSessionCompleteDelegate_Handle(JoinSessionDelegateHandle);
|
if (IOnlineSessionPtr Sessions = Subsystem->GetSessionInterface())
|
||||||
|
{
|
||||||
|
Sessions->ClearOnJoinSessionCompleteDelegate_Handle(Handle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
ClearJoin(IOnlineSubsystem::Get(), JoinSessionDelegateHandle);
|
||||||
|
ClearJoin(IOnlineSubsystem::Get(STEAM_SUBSYSTEM), JoinSessionDelegateHandle);
|
||||||
|
|
||||||
const bool bSuccess = Result == EOnJoinSessionCompleteResult::Success;
|
const bool bSuccess = Result == EOnJoinSessionCompleteResult::Success;
|
||||||
|
if (bSuccess)
|
||||||
|
{
|
||||||
|
TravelAfterSuccessfulJoin();
|
||||||
|
}
|
||||||
OnSessionJoined.Broadcast(bSuccess);
|
OnSessionJoined.Broadcast(bSuccess);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void UVocationSessionSubsystem::TravelAfterSuccessfulJoin()
|
||||||
|
{
|
||||||
|
UWorld* World = GetWorld();
|
||||||
|
if (!World)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
APlayerController* PC = World->GetFirstPlayerController();
|
||||||
|
if (!PC)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
FString ConnectInfo;
|
||||||
|
IOnlineSubsystem* Subsystems[] = {
|
||||||
|
IOnlineSubsystem::Get(STEAM_SUBSYSTEM),
|
||||||
|
IOnlineSubsystem::Get(),
|
||||||
|
IOnlineSubsystem::Get(NULL_SUBSYSTEM)
|
||||||
|
};
|
||||||
|
|
||||||
|
for (IOnlineSubsystem* Subsystem : Subsystems)
|
||||||
|
{
|
||||||
|
if (!Subsystem)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (IOnlineSessionPtr Sessions = Subsystem->GetSessionInterface())
|
||||||
|
{
|
||||||
|
if (Sessions->GetResolvedConnectString(VOCATION_SESSION_NAME, ConnectInfo))
|
||||||
|
{
|
||||||
|
PC->ClientTravel(ConnectInfo, TRAVEL_Absolute);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -12,7 +12,6 @@
|
|||||||
#include "Components/SkyLightComponent.h"
|
#include "Components/SkyLightComponent.h"
|
||||||
#include "EngineUtils.h"
|
#include "EngineUtils.h"
|
||||||
#include "Kismet/GameplayStatics.h"
|
#include "Kismet/GameplayStatics.h"
|
||||||
#include "UObject/ConstructorHelpers.h"
|
|
||||||
|
|
||||||
AVocationTestWorldBuilder::AVocationTestWorldBuilder()
|
AVocationTestWorldBuilder::AVocationTestWorldBuilder()
|
||||||
{
|
{
|
||||||
@ -36,15 +35,15 @@ void AVocationTestWorldBuilder::BuildTestWorld()
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
static ConstructorHelpers::FObjectFinder<UStaticMesh> PlaneMesh(TEXT("/Engine/BasicShapes/Plane.Plane"));
|
UStaticMesh* PlaneMesh = LoadObject<UStaticMesh>(nullptr, TEXT("/Engine/BasicShapes/Plane.Plane"));
|
||||||
if (PlaneMesh.Succeeded())
|
if (PlaneMesh)
|
||||||
{
|
{
|
||||||
FActorSpawnParameters SpawnParams;
|
FActorSpawnParameters SpawnParams;
|
||||||
SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
|
SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
|
||||||
|
|
||||||
if (AStaticMeshActor* Floor = World->SpawnActor<AStaticMeshActor>(FVector::ZeroVector, FRotator::ZeroRotator, SpawnParams))
|
if (AStaticMeshActor* Floor = World->SpawnActor<AStaticMeshActor>(FVector::ZeroVector, FRotator::ZeroRotator, SpawnParams))
|
||||||
{
|
{
|
||||||
Floor->GetStaticMeshComponent()->SetStaticMesh(PlaneMesh.Object);
|
Floor->GetStaticMeshComponent()->SetStaticMesh(PlaneMesh);
|
||||||
Floor->SetActorScale3D(FVector(20.f, 20.f, 1.f));
|
Floor->SetActorScale3D(FVector(20.f, 20.f, 1.f));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
382
Source/VocationLife/Private/VocationWebAdminSubsystem.cpp
Normal file
382
Source/VocationLife/Private/VocationWebAdminSubsystem.cpp
Normal file
@ -0,0 +1,382 @@
|
|||||||
|
// Copyright VocationLife Project. All Rights Reserved.
|
||||||
|
|
||||||
|
#include "VocationWebAdminSubsystem.h"
|
||||||
|
#include "Sockets.h"
|
||||||
|
#include "SocketSubsystem.h"
|
||||||
|
#include "IPAddress.h"
|
||||||
|
#include "Misc/CommandLine.h"
|
||||||
|
#include "Engine/Engine.h"
|
||||||
|
|
||||||
|
void UVocationWebAdminSubsystem::Initialize(FSubsystemCollectionBase& Collection)
|
||||||
|
{
|
||||||
|
Super::Initialize(Collection);
|
||||||
|
|
||||||
|
ServerSettings = NewObject<UVocationServerSettings>(this);
|
||||||
|
ServerSettings->LoadFromDisk();
|
||||||
|
|
||||||
|
if (IsRunningDedicatedServer() || FParse::Param(FCommandLine::Get(), TEXT("WebAdmin")))
|
||||||
|
{
|
||||||
|
StartAdminServer(ServerSettings->AdminHttpPort);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UVocationWebAdminSubsystem::Deinitialize()
|
||||||
|
{
|
||||||
|
StopAdminServer();
|
||||||
|
Super::Deinitialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
void UVocationWebAdminSubsystem::Tick(float DeltaTime)
|
||||||
|
{
|
||||||
|
if (bIsRunning)
|
||||||
|
{
|
||||||
|
AcceptConnections();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TStatId UVocationWebAdminSubsystem::GetStatId() const
|
||||||
|
{
|
||||||
|
RETURN_QUICK_DECLARE_CYCLE_STAT(UVocationWebAdminSubsystem, STATGROUP_Tickables);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UVocationWebAdminSubsystem::IsTickable() const
|
||||||
|
{
|
||||||
|
return bIsRunning;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UVocationWebAdminSubsystem::StartAdminServer(int32 Port)
|
||||||
|
{
|
||||||
|
StopAdminServer();
|
||||||
|
|
||||||
|
BoundPort = Port > 0 ? Port : 8080;
|
||||||
|
ISocketSubsystem* SocketSubsystem = ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM);
|
||||||
|
if (!SocketSubsystem)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ListenSocket = MakeShareable(SocketSubsystem->CreateSocket(NAME_Stream, TEXT("VocationWebAdmin"), false));
|
||||||
|
if (!ListenSocket.IsValid())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ListenSocket->SetReuseAddr(true);
|
||||||
|
ListenSocket->SetNonBlocking(true);
|
||||||
|
|
||||||
|
TSharedRef<FInternetAddr> Addr = SocketSubsystem->CreateInternetAddr();
|
||||||
|
bool bIsValid = false;
|
||||||
|
Addr->SetIp(TEXT("0.0.0.0"), bIsValid);
|
||||||
|
Addr->SetPort(BoundPort);
|
||||||
|
|
||||||
|
if (!ListenSocket->Bind(*Addr) || !ListenSocket->Listen(8))
|
||||||
|
{
|
||||||
|
ListenSocket->Close();
|
||||||
|
ListenSocket.Reset();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bIsRunning = true;
|
||||||
|
UE_LOG(LogTemp, Log, TEXT("VocationLife Web Admin listening on port %d"), BoundPort);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UVocationWebAdminSubsystem::StopAdminServer()
|
||||||
|
{
|
||||||
|
if (ListenSocket.IsValid())
|
||||||
|
{
|
||||||
|
ListenSocket->Close();
|
||||||
|
ListenSocket.Reset();
|
||||||
|
}
|
||||||
|
bIsRunning = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UVocationWebAdminSubsystem::AcceptConnections()
|
||||||
|
{
|
||||||
|
if (!ListenSocket.IsValid())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool bPending = false;
|
||||||
|
if (!ListenSocket->HasPendingConnection(bPending) || !bPending)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
FSocket* Client = ListenSocket->Accept(TEXT("VocationWebAdminClient"));
|
||||||
|
if (Client)
|
||||||
|
{
|
||||||
|
HandleClient(Client);
|
||||||
|
Client->Close();
|
||||||
|
ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM)->DestroySocket(Client);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UVocationWebAdminSubsystem::ParseHttpRequest(const FString& Raw, FString& OutMethod, FString& OutPath, FString& OutBody)
|
||||||
|
{
|
||||||
|
TArray<FString> Lines;
|
||||||
|
Raw.ParseIntoArrayLines(Lines, false);
|
||||||
|
if (Lines.Num() == 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
TArray<FString> RequestParts;
|
||||||
|
Lines[0].ParseIntoArrayWS(RequestParts);
|
||||||
|
if (RequestParts.Num() < 2)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
OutMethod = RequestParts[0];
|
||||||
|
OutPath = RequestParts[1];
|
||||||
|
|
||||||
|
const int32 HeaderEnd = Raw.Find(TEXT("\r\n\r\n"));
|
||||||
|
if (HeaderEnd != INDEX_NONE)
|
||||||
|
{
|
||||||
|
OutBody = Raw.Mid(HeaderEnd + 4);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
FString UVocationWebAdminSubsystem::UrlDecode(const FString& Encoded)
|
||||||
|
{
|
||||||
|
FString Result;
|
||||||
|
Result.Reserve(Encoded.Len());
|
||||||
|
for (int32 i = 0; i < Encoded.Len(); ++i)
|
||||||
|
{
|
||||||
|
const TCHAR C = Encoded[i];
|
||||||
|
if (C == TEXT('+'))
|
||||||
|
{
|
||||||
|
Result.AppendChar(TEXT(' '));
|
||||||
|
}
|
||||||
|
else if (C == TEXT('%') && i + 2 < Encoded.Len())
|
||||||
|
{
|
||||||
|
const int32 Hi = FParse::HexDigit(Encoded[i + 1]);
|
||||||
|
const int32 Lo = FParse::HexDigit(Encoded[i + 2]);
|
||||||
|
if (Hi >= 0 && Lo >= 0)
|
||||||
|
{
|
||||||
|
Result.AppendChar(static_cast<TCHAR>((Hi << 4) | Lo));
|
||||||
|
i += 2;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Result.AppendChar(C);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Result.AppendChar(C);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Result;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UVocationWebAdminSubsystem::ApplyFormBody(const FString& Body, FString& OutMessage)
|
||||||
|
{
|
||||||
|
if (!ServerSettings)
|
||||||
|
{
|
||||||
|
OutMessage = TEXT("No settings object");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
TArray<FString> Pairs;
|
||||||
|
Body.ParseIntoArray(Pairs, TEXT("&"), true);
|
||||||
|
|
||||||
|
FString SubmittedPassword;
|
||||||
|
TSet<FString> SeenKeys;
|
||||||
|
for (const FString& Pair : Pairs)
|
||||||
|
{
|
||||||
|
FString Key, Value;
|
||||||
|
if (!Pair.Split(TEXT("="), &Key, &Value))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Key = UrlDecode(Key);
|
||||||
|
Value = UrlDecode(Value);
|
||||||
|
SeenKeys.Add(Key);
|
||||||
|
|
||||||
|
if (Key == TEXT("password"))
|
||||||
|
{
|
||||||
|
SubmittedPassword = Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (SubmittedPassword != ServerSettings->AdminPassword)
|
||||||
|
{
|
||||||
|
OutMessage = TEXT("Invalid admin password");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unchecked HTML checkboxes are omitted from POST bodies.
|
||||||
|
ServerSettings->bLANOnly = SeenKeys.Contains(TEXT("bLANOnly"));
|
||||||
|
ServerSettings->bUseSteam = SeenKeys.Contains(TEXT("bUseSteam"));
|
||||||
|
ServerSettings->GameRules.bFriendlyFire = SeenKeys.Contains(TEXT("bFriendlyFire"));
|
||||||
|
ServerSettings->GameRules.bAllowVocationSwitch = SeenKeys.Contains(TEXT("bAllowVocationSwitch"));
|
||||||
|
|
||||||
|
for (const FString& Pair : Pairs)
|
||||||
|
{
|
||||||
|
FString Key, Value;
|
||||||
|
if (!Pair.Split(TEXT("="), &Key, &Value))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Key = UrlDecode(Key);
|
||||||
|
Value = UrlDecode(Value);
|
||||||
|
|
||||||
|
if (Key == TEXT("ServerName"))
|
||||||
|
{
|
||||||
|
ServerSettings->ServerName = Value;
|
||||||
|
}
|
||||||
|
else if (Key == TEXT("GamePort"))
|
||||||
|
{
|
||||||
|
const int32 NewPort = FCString::Atoi(*Value);
|
||||||
|
if (NewPort > 0 && NewPort != ServerSettings->GamePort)
|
||||||
|
{
|
||||||
|
ServerSettings->GamePort = NewPort;
|
||||||
|
bPendingPortRestartNotice = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (Key == TEXT("AdminHttpPort"))
|
||||||
|
{
|
||||||
|
ServerSettings->AdminHttpPort = FCString::Atoi(*Value);
|
||||||
|
}
|
||||||
|
else if (Key == TEXT("MaxPlayers"))
|
||||||
|
{
|
||||||
|
ServerSettings->MaxPlayers = FMath::Clamp(FCString::Atoi(*Value), 1, 64);
|
||||||
|
}
|
||||||
|
else if (Key == TEXT("DefaultMapPath"))
|
||||||
|
{
|
||||||
|
ServerSettings->DefaultMapPath = Value;
|
||||||
|
}
|
||||||
|
else if (Key == TEXT("DayLengthMinutes"))
|
||||||
|
{
|
||||||
|
ServerSettings->GameRules.DayLengthMinutes = FCString::Atof(*Value);
|
||||||
|
}
|
||||||
|
else if (Key == TEXT("MiningRespawnSeconds"))
|
||||||
|
{
|
||||||
|
ServerSettings->GameRules.MiningRespawnSeconds = FCString::Atof(*Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ServerSettings->SaveToDisk();
|
||||||
|
OutMessage = bPendingPortRestartNotice
|
||||||
|
? TEXT("Saved. Game port changes apply after server restart.")
|
||||||
|
: TEXT("Settings saved.");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
FString UVocationWebAdminSubsystem::BuildJsonStatus() const
|
||||||
|
{
|
||||||
|
if (!ServerSettings)
|
||||||
|
{
|
||||||
|
return TEXT("{}");
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Escape = [](const FString& In) -> FString
|
||||||
|
{
|
||||||
|
return In.Replace(TEXT("\\"), TEXT("\\\\")).Replace(TEXT("\""), TEXT("\\\""));
|
||||||
|
};
|
||||||
|
|
||||||
|
return FString::Printf(
|
||||||
|
TEXT("{\"serverName\":\"%s\",\"gamePort\":%d,\"adminHttpPort\":%d,\"maxPlayers\":%d,\"map\":\"%s\",\"lanOnly\":%s,\"useSteam\":%s,\"friendlyFire\":%s}"),
|
||||||
|
*Escape(ServerSettings->ServerName),
|
||||||
|
ServerSettings->GamePort,
|
||||||
|
ServerSettings->AdminHttpPort,
|
||||||
|
ServerSettings->MaxPlayers,
|
||||||
|
*Escape(ServerSettings->DefaultMapPath),
|
||||||
|
ServerSettings->bLANOnly ? TEXT("true") : TEXT("false"),
|
||||||
|
ServerSettings->bUseSteam ? TEXT("true") : TEXT("false"),
|
||||||
|
ServerSettings->GameRules.bFriendlyFire ? TEXT("true") : TEXT("false"));
|
||||||
|
}
|
||||||
|
|
||||||
|
FString UVocationWebAdminSubsystem::BuildHtmlPage() const
|
||||||
|
{
|
||||||
|
const FVocationGameRules& Rules = ServerSettings->GameRules;
|
||||||
|
FString Html;
|
||||||
|
Html += TEXT("<!DOCTYPE html><html><head><meta charset=\"utf-8\"><title>VocationLife Server Admin</title>");
|
||||||
|
Html += TEXT("<style>body{font-family:system-ui,sans-serif;background:#1a1f24;color:#e8ecef;margin:2rem;}");
|
||||||
|
Html += TEXT("form{background:#252b33;padding:1.5rem;border-radius:12px;max-width:640px;}");
|
||||||
|
Html += TEXT("label{display:block;margin:.75rem 0 .25rem;}");
|
||||||
|
Html += TEXT("input[type=text],input[type=number],input[type=password]{width:100%;padding:.5rem;border-radius:6px;border:1px solid #3a4450;background:#12161a;color:#fff;}");
|
||||||
|
Html += TEXT("button{margin-top:1rem;padding:.6rem 1.2rem;border:0;border-radius:8px;background:#3d8bfd;color:#fff;font-weight:600;cursor:pointer;}");
|
||||||
|
Html += TEXT(".hint{opacity:.7;font-size:.9rem;margin-top:1rem;}h1{margin-top:0;}</style></head><body>");
|
||||||
|
Html += TEXT("<h1>VocationLife Server Admin</h1><form method=\"POST\" action=\"/settings\">");
|
||||||
|
Html += TEXT("<label>Admin Password</label><input type=\"password\" name=\"password\" required>");
|
||||||
|
Html += FString::Printf(TEXT("<label>Server Name</label><input type=\"text\" name=\"ServerName\" value=\"%s\">"), *ServerSettings->ServerName);
|
||||||
|
Html += FString::Printf(TEXT("<label>Game Port</label><input type=\"number\" name=\"GamePort\" value=\"%d\">"), ServerSettings->GamePort);
|
||||||
|
Html += FString::Printf(TEXT("<label>Admin HTTP Port</label><input type=\"number\" name=\"AdminHttpPort\" value=\"%d\">"), ServerSettings->AdminHttpPort);
|
||||||
|
Html += FString::Printf(TEXT("<label>Max Players</label><input type=\"number\" name=\"MaxPlayers\" value=\"%d\">"), ServerSettings->MaxPlayers);
|
||||||
|
Html += FString::Printf(TEXT("<label>Default Map Path</label><input type=\"text\" name=\"DefaultMapPath\" value=\"%s\">"), *ServerSettings->DefaultMapPath);
|
||||||
|
Html += FString::Printf(TEXT("<label><input type=\"checkbox\" name=\"bLANOnly\" %s> LAN only</label>"), ServerSettings->bLANOnly ? TEXT("checked") : TEXT(""));
|
||||||
|
Html += FString::Printf(TEXT("<label><input type=\"checkbox\" name=\"bUseSteam\" %s> Prefer Steam</label>"), ServerSettings->bUseSteam ? TEXT("checked") : TEXT(""));
|
||||||
|
Html += TEXT("<hr><h2>Game Rules</h2>");
|
||||||
|
Html += FString::Printf(TEXT("<label><input type=\"checkbox\" name=\"bFriendlyFire\" %s> Friendly Fire</label>"), Rules.bFriendlyFire ? TEXT("checked") : TEXT(""));
|
||||||
|
Html += FString::Printf(TEXT("<label><input type=\"checkbox\" name=\"bAllowVocationSwitch\" %s> Allow Vocation Switch</label>"), Rules.bAllowVocationSwitch ? TEXT("checked") : TEXT(""));
|
||||||
|
Html += FString::Printf(TEXT("<label>Day Length (minutes)</label><input type=\"number\" step=\"0.1\" name=\"DayLengthMinutes\" value=\"%.1f\">"), Rules.DayLengthMinutes);
|
||||||
|
Html += FString::Printf(TEXT("<label>Mining Respawn (seconds)</label><input type=\"number\" step=\"0.1\" name=\"MiningRespawnSeconds\" value=\"%.1f\">"), Rules.MiningRespawnSeconds);
|
||||||
|
Html += TEXT("<button type=\"submit\">Save Settings</button></form>");
|
||||||
|
Html += FString::Printf(TEXT("<p class=\"hint\">Game port changes require a server restart. Admin panel port: %d</p></body></html>"), BoundPort);
|
||||||
|
return Html;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UVocationWebAdminSubsystem::HandleClient(FSocket* ClientSocket)
|
||||||
|
{
|
||||||
|
if (!ClientSocket)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
TArray<uint8> Data;
|
||||||
|
Data.SetNumUninitialized(8192);
|
||||||
|
int32 BytesRead = 0;
|
||||||
|
if (!ClientSocket->Recv(Data.GetData(), Data.Num(), BytesRead) || BytesRead <= 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Data.SetNum(BytesRead);
|
||||||
|
const FUTF8ToTCHAR Converter(reinterpret_cast<const ANSICHAR*>(Data.GetData()), BytesRead);
|
||||||
|
const FString Request(Converter.Length(), Converter.Get());
|
||||||
|
|
||||||
|
FString Method, Path, Body;
|
||||||
|
if (!ParseHttpRequest(Request, Method, Path, Body))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
FString Status = TEXT("200 OK");
|
||||||
|
FString ContentType = TEXT("text/html; charset=utf-8");
|
||||||
|
FString ResponseBody;
|
||||||
|
|
||||||
|
if (Path.StartsWith(TEXT("/api/status")))
|
||||||
|
{
|
||||||
|
ContentType = TEXT("application/json");
|
||||||
|
ResponseBody = BuildJsonStatus();
|
||||||
|
}
|
||||||
|
else if (Method == TEXT("POST") && Path.StartsWith(TEXT("/settings")))
|
||||||
|
{
|
||||||
|
FString Message;
|
||||||
|
ApplyFormBody(Body, Message);
|
||||||
|
ResponseBody = FString::Printf(
|
||||||
|
TEXT("<!DOCTYPE html><html><body style='font-family:sans-serif;background:#1a1f24;color:#fff;padding:2rem'><p>%s</p><p><a href='/' style='color:#3d8bfd'>Back</a></p></body></html>"),
|
||||||
|
*Message);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ResponseBody = BuildHtmlPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
const FTCHARToUTF8 BodyUtf8(*ResponseBody);
|
||||||
|
const FString Header = FString::Printf(
|
||||||
|
TEXT("HTTP/1.1 %s\r\nContent-Type: %s\r\nContent-Length: %d\r\nConnection: close\r\n\r\n"),
|
||||||
|
*Status,
|
||||||
|
*ContentType,
|
||||||
|
BodyUtf8.Length());
|
||||||
|
|
||||||
|
FTCHARToUTF8 HeaderUtf8(*Header);
|
||||||
|
int32 Sent = 0;
|
||||||
|
ClientSocket->Send(reinterpret_cast<const uint8*>(HeaderUtf8.Get()), HeaderUtf8.Length(), Sent);
|
||||||
|
ClientSocket->Send(reinterpret_cast<const uint8*>(BodyUtf8.Get()), BodyUtf8.Length(), Sent);
|
||||||
|
}
|
||||||
@ -110,10 +110,10 @@ protected:
|
|||||||
void UpdateInteractionPrompt();
|
void UpdateInteractionPrompt();
|
||||||
|
|
||||||
UPROPERTY(ReplicatedUsing = OnRep_CameraMode, BlueprintReadOnly, Category = "VocationLife|Camera")
|
UPROPERTY(ReplicatedUsing = OnRep_CameraMode, BlueprintReadOnly, Category = "VocationLife|Camera")
|
||||||
EVocationCameraMode CurrentCameraMode = EVocationCameraMode::FirstPerson;
|
EVocationCameraMode CurrentCameraMode = EVocationCameraMode::TopDown;
|
||||||
|
|
||||||
UPROPERTY(ReplicatedUsing = OnRep_CameraMode)
|
UPROPERTY(ReplicatedUsing = OnRep_CameraMode)
|
||||||
EVocationCameraMode TargetCameraMode = EVocationCameraMode::FirstPerson;
|
EVocationCameraMode TargetCameraMode = EVocationCameraMode::TopDown;
|
||||||
|
|
||||||
UPROPERTY()
|
UPROPERTY()
|
||||||
float CameraBlendAlpha = 1.f;
|
float CameraBlendAlpha = 1.f;
|
||||||
|
|||||||
@ -48,6 +48,13 @@ public:
|
|||||||
UFUNCTION(BlueprintCallable, Category = "VocationLife|Graphics")
|
UFUNCTION(BlueprintCallable, Category = "VocationLife|Graphics")
|
||||||
bool IsRayTracingEnabled() const { return bRayTracingEnabled; }
|
bool IsRayTracingEnabled() const { return bRayTracingEnabled; }
|
||||||
|
|
||||||
|
/** Once true, the start/multiplayer menu will not open again this session. */
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "VocationLife|UI")
|
||||||
|
void MarkGameplayStarted();
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "VocationLife|UI")
|
||||||
|
bool HasGameplayStarted() const { return bHasEnteredGameplay; }
|
||||||
|
|
||||||
UPROPERTY(BlueprintAssignable, Category = "VocationLife")
|
UPROPERTY(BlueprintAssignable, Category = "VocationLife")
|
||||||
FOnVocationDataReady OnDataReady;
|
FOnVocationDataReady OnDataReady;
|
||||||
|
|
||||||
@ -68,4 +75,7 @@ protected:
|
|||||||
|
|
||||||
UPROPERTY()
|
UPROPERTY()
|
||||||
bool bRayTracingEnabled = false;
|
bool bRayTracingEnabled = false;
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
bool bHasEnteredGameplay = false;
|
||||||
};
|
};
|
||||||
|
|||||||
89
Source/VocationLife/Public/VocationMainMenuWidget.h
Normal file
89
Source/VocationLife/Public/VocationMainMenuWidget.h
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
// Copyright VocationLife Project. All Rights Reserved.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "Blueprint/UserWidget.h"
|
||||||
|
#include "VocationSessionSubsystem.h"
|
||||||
|
#include "VocationMainMenuWidget.generated.h"
|
||||||
|
|
||||||
|
class UEditableTextBox;
|
||||||
|
class UComboBoxString;
|
||||||
|
class UButton;
|
||||||
|
class UTextBlock;
|
||||||
|
class UVerticalBox;
|
||||||
|
class UWidgetSwitcher;
|
||||||
|
class UBorder;
|
||||||
|
class USizeBox;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Minecraft-style start menu: vertical button stack with submenus.
|
||||||
|
*/
|
||||||
|
UCLASS()
|
||||||
|
class VOCATIONLIFE_API UVocationMainMenuWidget : public UUserWidget
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
virtual void NativeConstruct() override;
|
||||||
|
virtual void NativeDestruct() override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
enum class EMenuPage : uint8
|
||||||
|
{
|
||||||
|
Main = 0,
|
||||||
|
Singleplayer = 1,
|
||||||
|
Multiplayer = 2,
|
||||||
|
Options = 3
|
||||||
|
};
|
||||||
|
|
||||||
|
void BuildUI();
|
||||||
|
void RefreshMaps();
|
||||||
|
void SetStatus(const FString& Message);
|
||||||
|
void ShowPage(EMenuPage Page);
|
||||||
|
UButton* MakeMenuButton(const FName& Name, const FString& Label, void (UVocationMainMenuWidget::*Callback)());
|
||||||
|
UTextBlock* MakeLabel(const FName& Name, const FString& Text, float Size = 18.f) const;
|
||||||
|
void StyleMenuButton(UButton* Button) const;
|
||||||
|
void LeaveStartMenu();
|
||||||
|
|
||||||
|
UFUNCTION() void OnSingleplayerClicked();
|
||||||
|
UFUNCTION() void OnMultiplayerClicked();
|
||||||
|
UFUNCTION() void OnOptionsClicked();
|
||||||
|
UFUNCTION() void OnQuitClicked();
|
||||||
|
UFUNCTION() void OnBackClicked();
|
||||||
|
|
||||||
|
UFUNCTION() void OnPlaySoloClicked();
|
||||||
|
UFUNCTION() void OnHostClicked();
|
||||||
|
UFUNCTION() void OnFindSteamClicked();
|
||||||
|
UFUNCTION() void OnFindLanClicked();
|
||||||
|
UFUNCTION() void OnJoinSelectedClicked();
|
||||||
|
UFUNCTION() void OnConnectIpClicked();
|
||||||
|
|
||||||
|
UFUNCTION() void OnGraphicsLow();
|
||||||
|
UFUNCTION() void OnGraphicsMedium();
|
||||||
|
UFUNCTION() void OnGraphicsHigh();
|
||||||
|
UFUNCTION() void OnGraphicsUltra();
|
||||||
|
UFUNCTION() void OnGraphicsRayTracing();
|
||||||
|
|
||||||
|
UFUNCTION() void OnSessionSearchDetailed(const TArray<FVocationSessionSearchEntry>& Sessions);
|
||||||
|
UFUNCTION() void OnSessionJoined(bool bSuccess);
|
||||||
|
|
||||||
|
UPROPERTY() TObjectPtr<UWidgetSwitcher> PageSwitcher;
|
||||||
|
UPROPERTY() TObjectPtr<UTextBlock> TitleText;
|
||||||
|
UPROPERTY() TObjectPtr<UTextBlock> SplashText;
|
||||||
|
UPROPERTY() TObjectPtr<UTextBlock> StatusText;
|
||||||
|
UPROPERTY() TObjectPtr<UTextBlock> VersionText;
|
||||||
|
|
||||||
|
UPROPERTY() TObjectPtr<UComboBoxString> MapCombo;
|
||||||
|
UPROPERTY() TObjectPtr<UComboBoxString> MpMapCombo;
|
||||||
|
UPROPERTY() TObjectPtr<UComboBoxString> BackendCombo;
|
||||||
|
UPROPERTY() TObjectPtr<UComboBoxString> SessionCombo;
|
||||||
|
UPROPERTY() TObjectPtr<UEditableTextBox> ServerNameBox;
|
||||||
|
UPROPERTY() TObjectPtr<UEditableTextBox> PortBox;
|
||||||
|
UPROPERTY() TObjectPtr<UEditableTextBox> IpBox;
|
||||||
|
UPROPERTY() TObjectPtr<UEditableTextBox> JoinPortBox;
|
||||||
|
UPROPERTY() TObjectPtr<UEditableTextBox> MaxPlayersBox;
|
||||||
|
|
||||||
|
TArray<FVocationMapOption> CachedMaps;
|
||||||
|
TArray<FVocationSessionSearchEntry> CachedSessions;
|
||||||
|
};
|
||||||
@ -32,6 +32,9 @@ public:
|
|||||||
UFUNCTION(BlueprintCallable, Category = "VocationLife|UI")
|
UFUNCTION(BlueprintCallable, Category = "VocationLife|UI")
|
||||||
void ClearInteractionPrompt();
|
void ClearInteractionPrompt();
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "VocationLife|UI")
|
||||||
|
void ShowStartMenuIfNeeded();
|
||||||
|
|
||||||
UFUNCTION(BlueprintCallable, Category = "VocationLife|Session")
|
UFUNCTION(BlueprintCallable, Category = "VocationLife|Session")
|
||||||
void HostCoopGame();
|
void HostCoopGame();
|
||||||
|
|
||||||
@ -107,6 +110,9 @@ protected:
|
|||||||
UPROPERTY()
|
UPROPERTY()
|
||||||
TObjectPtr<AVocationHUD> VocationHUD;
|
TObjectPtr<AVocationHUD> VocationHUD;
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
TObjectPtr<class UVocationMainMenuWidget> MultiplayerMenu;
|
||||||
|
|
||||||
UPROPERTY(EditDefaultsOnly, Category = "VocationLife|Input")
|
UPROPERTY(EditDefaultsOnly, Category = "VocationLife|Input")
|
||||||
int32 SharedMappingPriority = 0;
|
int32 SharedMappingPriority = 0;
|
||||||
|
|
||||||
|
|||||||
87
Source/VocationLife/Public/VocationServerSettings.h
Normal file
87
Source/VocationLife/Public/VocationServerSettings.h
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
// Copyright VocationLife Project. All Rights Reserved.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "UObject/Object.h"
|
||||||
|
#include "VocationServerSettings.generated.h"
|
||||||
|
|
||||||
|
USTRUCT(BlueprintType)
|
||||||
|
struct FVocationMapOption
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Map")
|
||||||
|
FString DisplayName;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Map")
|
||||||
|
FString MapPath;
|
||||||
|
};
|
||||||
|
|
||||||
|
USTRUCT(BlueprintType)
|
||||||
|
struct FVocationGameRules
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Rules")
|
||||||
|
bool bFriendlyFire = false;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Rules")
|
||||||
|
float DayLengthMinutes = 20.f;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Rules")
|
||||||
|
bool bAllowVocationSwitch = true;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Rules")
|
||||||
|
int32 StartingOre = 0;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Rules")
|
||||||
|
float MiningRespawnSeconds = 30.f;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runtime + config-backed dedicated server settings.
|
||||||
|
* Loaded from Config/ServerConfig.ini and mutable via the web admin.
|
||||||
|
*/
|
||||||
|
UCLASS(Config = ServerConfig, DefaultConfig)
|
||||||
|
class VOCATIONLIFE_API UVocationServerSettings : public UObject
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UPROPERTY(Config, EditAnywhere, BlueprintReadWrite, Category = "Server")
|
||||||
|
FString ServerName = TEXT("VocationLife Dedicated Server");
|
||||||
|
|
||||||
|
UPROPERTY(Config, EditAnywhere, BlueprintReadWrite, Category = "Server")
|
||||||
|
int32 MaxPlayers = 4;
|
||||||
|
|
||||||
|
UPROPERTY(Config, EditAnywhere, BlueprintReadWrite, Category = "Server")
|
||||||
|
int32 GamePort = 7777;
|
||||||
|
|
||||||
|
UPROPERTY(Config, EditAnywhere, BlueprintReadWrite, Category = "Server")
|
||||||
|
int32 AdminHttpPort = 8080;
|
||||||
|
|
||||||
|
UPROPERTY(Config, EditAnywhere, BlueprintReadWrite, Category = "Server")
|
||||||
|
bool bLANOnly = false;
|
||||||
|
|
||||||
|
UPROPERTY(Config, EditAnywhere, BlueprintReadWrite, Category = "Server")
|
||||||
|
bool bUseSteam = true;
|
||||||
|
|
||||||
|
UPROPERTY(Config, EditAnywhere, BlueprintReadWrite, Category = "Server")
|
||||||
|
FString AdminPassword = TEXT("vocation");
|
||||||
|
|
||||||
|
UPROPERTY(Config, EditAnywhere, BlueprintReadWrite, Category = "Server")
|
||||||
|
FString DefaultMapPath = TEXT("/Engine/Maps/Entry");
|
||||||
|
|
||||||
|
UPROPERTY(Config, EditAnywhere, BlueprintReadWrite, Category = "Server")
|
||||||
|
FVocationGameRules GameRules;
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "VocationLife|Server")
|
||||||
|
void LoadFromDisk();
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "VocationLife|Server")
|
||||||
|
void SaveToDisk();
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "VocationLife|Server")
|
||||||
|
static TArray<FVocationMapOption> GetAvailableMaps();
|
||||||
|
};
|
||||||
@ -5,10 +5,44 @@
|
|||||||
#include "CoreMinimal.h"
|
#include "CoreMinimal.h"
|
||||||
#include "Subsystems/GameInstanceSubsystem.h"
|
#include "Subsystems/GameInstanceSubsystem.h"
|
||||||
#include "Interfaces/OnlineSessionInterface.h"
|
#include "Interfaces/OnlineSessionInterface.h"
|
||||||
|
#include "VocationServerSettings.h"
|
||||||
#include "VocationSessionSubsystem.generated.h"
|
#include "VocationSessionSubsystem.generated.h"
|
||||||
|
|
||||||
|
class IOnlineSubsystem;
|
||||||
|
|
||||||
|
UENUM(BlueprintType)
|
||||||
|
enum class EVocationNetBackend : uint8
|
||||||
|
{
|
||||||
|
Auto UMETA(DisplayName = "Auto (Steam if available)"),
|
||||||
|
Steam UMETA(DisplayName = "Steam"),
|
||||||
|
LAN UMETA(DisplayName = "LAN / Null")
|
||||||
|
};
|
||||||
|
|
||||||
|
USTRUCT(BlueprintType)
|
||||||
|
struct FVocationSessionSearchEntry
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintReadOnly, Category = "Session")
|
||||||
|
FString DisplayName;
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintReadOnly, Category = "Session")
|
||||||
|
FString MapName;
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintReadOnly, Category = "Session")
|
||||||
|
int32 MaxPlayers = 0;
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintReadOnly, Category = "Session")
|
||||||
|
int32 OpenSlots = 0;
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintReadOnly, Category = "Session")
|
||||||
|
bool bIsLAN = false;
|
||||||
|
};
|
||||||
|
|
||||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnSessionSearchComplete, const TArray<FString>&, SessionNames);
|
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnSessionSearchComplete, const TArray<FString>&, SessionNames);
|
||||||
|
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnSessionSearchDetailed, const TArray<FVocationSessionSearchEntry>&, Sessions);
|
||||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnSessionJoined, bool, bSuccess);
|
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnSessionJoined, bool, bSuccess);
|
||||||
|
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnHostReady, bool, bSuccess, const FString&, TravelURL);
|
||||||
|
|
||||||
UCLASS()
|
UCLASS()
|
||||||
class VOCATIONLIFE_API UVocationSessionSubsystem : public UGameInstanceSubsystem
|
class VOCATIONLIFE_API UVocationSessionSubsystem : public UGameInstanceSubsystem
|
||||||
@ -16,31 +50,62 @@ class VOCATIONLIFE_API UVocationSessionSubsystem : public UGameInstanceSubsystem
|
|||||||
GENERATED_BODY()
|
GENERATED_BODY()
|
||||||
|
|
||||||
public:
|
public:
|
||||||
UFUNCTION(BlueprintCallable, Category = "VocationLife|Session")
|
virtual void Initialize(FSubsystemCollectionBase& Collection) override;
|
||||||
void HostSession(int32 MaxPlayers = 4, const FString& SessionName = TEXT("VocationLife"));
|
|
||||||
|
|
||||||
UFUNCTION(BlueprintCallable, Category = "VocationLife|Session")
|
UFUNCTION(BlueprintCallable, Category = "VocationLife|Session")
|
||||||
void FindSessions();
|
void HostSession(
|
||||||
|
int32 MaxPlayers = 4,
|
||||||
|
const FString& SessionName = TEXT("VocationLife"),
|
||||||
|
const FString& MapPath = TEXT("/Engine/Maps/Entry"),
|
||||||
|
int32 Port = 7777,
|
||||||
|
EVocationNetBackend Backend = EVocationNetBackend::Auto,
|
||||||
|
bool bLANOnly = false);
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "VocationLife|Session")
|
||||||
|
void FindSessions(EVocationNetBackend Backend = EVocationNetBackend::Auto, bool bLANOnly = false);
|
||||||
|
|
||||||
UFUNCTION(BlueprintCallable, Category = "VocationLife|Session")
|
UFUNCTION(BlueprintCallable, Category = "VocationLife|Session")
|
||||||
void JoinSessionByIndex(int32 SessionIndex);
|
void JoinSessionByIndex(int32 SessionIndex);
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "VocationLife|Session")
|
||||||
|
void ConnectByIP(const FString& Address, int32 Port = 7777);
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "VocationLife|Session")
|
||||||
|
void TravelToMap(const FString& MapPath, bool bAsListenServer = false, int32 Port = 7777);
|
||||||
|
|
||||||
UFUNCTION(BlueprintCallable, Category = "VocationLife|Session")
|
UFUNCTION(BlueprintCallable, Category = "VocationLife|Session")
|
||||||
void DestroySession();
|
void DestroySession();
|
||||||
|
|
||||||
UFUNCTION(BlueprintCallable, Category = "VocationLife|Session")
|
UFUNCTION(BlueprintCallable, Category = "VocationLife|Session")
|
||||||
bool IsHosting() const { return bIsHosting; }
|
bool IsHosting() const { return bIsHosting; }
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "VocationLife|Session")
|
||||||
|
bool IsSteamAvailable() const;
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "VocationLife|Session")
|
||||||
|
FString GetActiveSubsystemName() const;
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "VocationLife|Session")
|
||||||
|
TArray<FVocationMapOption> GetMapList() const;
|
||||||
|
|
||||||
UPROPERTY(BlueprintAssignable, Category = "VocationLife|Session")
|
UPROPERTY(BlueprintAssignable, Category = "VocationLife|Session")
|
||||||
FOnSessionSearchComplete OnSessionSearchComplete;
|
FOnSessionSearchComplete OnSessionSearchComplete;
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintAssignable, Category = "VocationLife|Session")
|
||||||
|
FOnSessionSearchDetailed OnSessionSearchDetailed;
|
||||||
|
|
||||||
UPROPERTY(BlueprintAssignable, Category = "VocationLife|Session")
|
UPROPERTY(BlueprintAssignable, Category = "VocationLife|Session")
|
||||||
FOnSessionJoined OnSessionJoined;
|
FOnSessionJoined OnSessionJoined;
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintAssignable, Category = "VocationLife|Session")
|
||||||
|
FOnHostReady OnHostReady;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
IOnlineSubsystem* ResolveSubsystem(EVocationNetBackend Backend) const;
|
||||||
void OnCreateSessionComplete(FName SessionName, bool bWasSuccessful);
|
void OnCreateSessionComplete(FName SessionName, bool bWasSuccessful);
|
||||||
void OnFindSessionsComplete(bool bWasSuccessful);
|
void OnFindSessionsComplete(bool bWasSuccessful);
|
||||||
void OnJoinSessionComplete(FName SessionName, EOnJoinSessionCompleteResult::Type Result);
|
void OnJoinSessionComplete(FName SessionName, EOnJoinSessionCompleteResult::Type Result);
|
||||||
|
void TravelAfterSuccessfulJoin();
|
||||||
|
|
||||||
FDelegateHandle CreateSessionDelegateHandle;
|
FDelegateHandle CreateSessionDelegateHandle;
|
||||||
FDelegateHandle FindSessionsDelegateHandle;
|
FDelegateHandle FindSessionsDelegateHandle;
|
||||||
@ -48,4 +113,6 @@ protected:
|
|||||||
|
|
||||||
TSharedPtr<class FOnlineSessionSearch> SessionSearch;
|
TSharedPtr<class FOnlineSessionSearch> SessionSearch;
|
||||||
bool bIsHosting = false;
|
bool bIsHosting = false;
|
||||||
|
FString PendingMapPath = TEXT("/Engine/Maps/Entry");
|
||||||
|
int32 PendingPort = 7777;
|
||||||
};
|
};
|
||||||
|
|||||||
60
Source/VocationLife/Public/VocationWebAdminSubsystem.h
Normal file
60
Source/VocationLife/Public/VocationWebAdminSubsystem.h
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
// Copyright VocationLife Project. All Rights Reserved.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "Subsystems/GameInstanceSubsystem.h"
|
||||||
|
#include "VocationServerSettings.h"
|
||||||
|
#include "VocationWebAdminSubsystem.generated.h"
|
||||||
|
|
||||||
|
class FSocket;
|
||||||
|
class FTcpListener;
|
||||||
|
class FInternetAddr;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lightweight HTTP admin panel for dedicated / headless servers.
|
||||||
|
* Default: http://<server-ip>:8080
|
||||||
|
*/
|
||||||
|
UCLASS()
|
||||||
|
class VOCATIONLIFE_API UVocationWebAdminSubsystem : public UGameInstanceSubsystem, public FTickableGameObject
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
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 IsTickableInEditor() const override { return false; }
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "VocationLife|WebAdmin")
|
||||||
|
bool StartAdminServer(int32 Port = 8080);
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "VocationLife|WebAdmin")
|
||||||
|
void StopAdminServer();
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "VocationLife|WebAdmin")
|
||||||
|
bool IsAdminRunning() const { return bIsRunning; }
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "VocationLife|WebAdmin")
|
||||||
|
UVocationServerSettings* GetServerSettings() const { return ServerSettings; }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void AcceptConnections();
|
||||||
|
void HandleClient(FSocket* ClientSocket);
|
||||||
|
FString BuildHtmlPage() const;
|
||||||
|
FString BuildJsonStatus() const;
|
||||||
|
bool ApplyFormBody(const FString& Body, FString& OutMessage);
|
||||||
|
static bool ParseHttpRequest(const FString& Raw, FString& OutMethod, FString& OutPath, FString& OutBody);
|
||||||
|
static FString UrlDecode(const FString& Encoded);
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
TObjectPtr<UVocationServerSettings> ServerSettings;
|
||||||
|
|
||||||
|
TSharedPtr<FSocket> ListenSocket;
|
||||||
|
bool bIsRunning = false;
|
||||||
|
int32 BoundPort = 8080;
|
||||||
|
bool bPendingPortRestartNotice = false;
|
||||||
|
};
|
||||||
@ -21,7 +21,9 @@ public class VocationLife : ModuleRules
|
|||||||
"OnlineSubsystem",
|
"OnlineSubsystem",
|
||||||
"OnlineSubsystemUtils",
|
"OnlineSubsystemUtils",
|
||||||
"Json",
|
"Json",
|
||||||
"JsonUtilities"
|
"JsonUtilities",
|
||||||
|
"Sockets",
|
||||||
|
"Networking"
|
||||||
});
|
});
|
||||||
|
|
||||||
if (Target.Platform == UnrealTargetPlatform.Win64 ||
|
if (Target.Platform == UnrealTargetPlatform.Win64 ||
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user