Merge remote-tracking branch 'upstream/main' into Dual-Stack-IPv6-Support

This commit is contained in:
Brendon 2026-03-15 12:56:08 -07:00
commit 60cf7534fd
225 changed files with 41124 additions and 2296 deletions

8
.gitattributes vendored
View file

@ -1,8 +0,0 @@
*.png filter=lfs diff=lfs merge=lfs -text
*.jpg filter=lfs diff=lfs merge=lfs -text
*.ogg filter=lfs diff=lfs merge=lfs -text
*.binka filter=lfs diff=lfs merge=lfs -text
*.arc filter=lfs diff=lfs merge=lfs -text
*.ttf filter=lfs diff=lfs merge=lfs -text
*.bin filter=lfs diff=lfs merge=lfs -text
*.ico filter=lfs diff=lfs merge=lfs -text

BIN
.github/banner.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

160
.github/workflows/docker-nightly.yml vendored Normal file
View file

@ -0,0 +1,160 @@
name: Docker Nightly Dedicated Server
on:
workflow_dispatch:
push:
branches:
- "main"
- 'feature/dedicated-server'
paths-ignore:
- ".gitignore"
- "*.md"
- ".github/*.md"
permissions:
contents: read
packages: write
concurrency:
group: docker-nightly-dedicated-server
cancel-in-progress: true
jobs:
build-runtime:
name: Build Dedicated Server Runtime
runs-on: windows-latest
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup msbuild
uses: microsoft/setup-msbuild@v2
- name: Build Dedicated Server Runtime Only
shell: pwsh
run: |
MSBuild.exe Minecraft.World\Minecraft.World.vcxproj /p:Configuration=Release /p:Platform=x64 /m
MSBuild.exe Minecraft.Server\Minecraft.Server.vcxproj /p:Configuration=Release /p:Platform=x64 /m
- name: Stage dedicated server runtime
shell: pwsh
run: |
$serverOut = "Minecraft.Server/x64/Minecraft.Server/Release"
$stage = ".artifacts/dedicated-server-runtime"
if (Test-Path $stage) {
Remove-Item -Path $stage -Recurse -Force
}
New-Item -ItemType Directory -Path (Join-Path $stage "Windows64") -Force | Out-Null
# Minimum required runtime files
$required = @(
"Minecraft.Server.exe",
"iggy_w64.dll",
"Common"
)
foreach ($entry in $required) {
$src = Join-Path $serverOut $entry
if (-not (Test-Path $src)) {
throw "Missing required runtime path: $src"
}
}
# Copy required files
Copy-Item -Path (Join-Path $serverOut "Minecraft.Server.exe") -Destination (Join-Path $stage "Minecraft.Server.exe") -Force
Copy-Item -Path (Join-Path $serverOut "iggy_w64.dll") -Destination (Join-Path $stage "iggy_w64.dll") -Force
Copy-Item -Path (Join-Path $serverOut "Common") -Destination (Join-Path $stage "Common") -Recurse -Force
if (Test-Path (Join-Path $serverOut "Windows64")) {
Copy-Item -Path (Join-Path $serverOut "Windows64/*") -Destination (Join-Path $stage "Windows64") -Recurse -Force
} else {
Write-Host "Windows64 directory is not present in build output; staging without it."
}
Get-ChildItem -Path $stage -Recurse -File | Select-Object -First 20 | ForEach-Object {
Write-Host "Staged: $($_.FullName)"
}
- name: Upload dedicated server runtime to artifacts
uses: actions/upload-artifact@v4
with:
name: dedicated-server-runtime
if-no-files-found: error
path: |
.artifacts/dedicated-server-runtime/**
docker-publish:
name: Build and Push Docker Image
runs-on: ubuntu-latest
needs: build-runtime
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Download dedicated server runtime from artifacts
uses: actions/download-artifact@v4
with:
name: dedicated-server-runtime
path: .artifacts/runtime
- name: Prepare Docker runtime directory
shell: bash
run: |
set -euo pipefail
rm -rf runtime
mkdir -p runtime
cp .artifacts/runtime/Minecraft.Server.exe runtime/Minecraft.Server.exe
cp .artifacts/runtime/iggy_w64.dll runtime/iggy_w64.dll
cp -R .artifacts/runtime/Common runtime/Common
mkdir -p runtime/Windows64
if [[ -d ".artifacts/runtime/Windows64" ]]; then
cp -R .artifacts/runtime/Windows64/. runtime/Windows64/
fi
test -f runtime/Minecraft.Server.exe
test -f runtime/iggy_w64.dll
test -d runtime/Common
test -d runtime/Windows64
- name: Compute image name
id: image
shell: bash
run: |
owner="$(echo "${{ vars.CONTAINER_REGISTRY_OWNER || github.repository_owner }}" | tr '[:upper:]' '[:lower:]')"
image_tag="nightly"
# if [[ "${{ github.ref }}" != "refs/heads/main" ]]; then
# image_tag="nightly-test"
# fi
echo "owner=$owner" >> "$GITHUB_OUTPUT"
echo "image=ghcr.io/$owner/minecraft-lce-dedicated-server" >> "$GITHUB_OUTPUT"
echo "image_tag=$image_tag" >> "$GITHUB_OUTPUT"
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ steps.image.outputs.image }}
tags: |
type=raw,value=${{ steps.image.outputs.image_tag }}
- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ secrets.GHCR_USERNAME || github.actor }}
password: ${{ secrets.GHCR_TOKEN || secrets.GITHUB_TOKEN }}
- name: Build and push image
uses: docker/build-push-action@v6
with:
context: .
file: docker/dedicated-server/Dockerfile
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
MC_RUNTIME_DIR=runtime

View file

@ -1,15 +1,19 @@
name: Nightly Release
name: Nightly Releases
on:
workflow_dispatch:
push:
branches:
- 'main'
- 'feature/dedicated-server'
paths-ignore:
- '.gitignore'
- '*.md'
- '.github/*.md'
permissions:
contents: write
jobs:
build:
name: Build Windows64
@ -28,13 +32,16 @@ jobs:
- name: Zip Build
run: 7z a -r LCEWindows64.zip ./x64/Release/*
- name: Update release
- name: Zip Dedicated Server Build
run: 7z a -r LCEServerWindows64.zip ./x64/Minecraft.Server/Release/*
- name: Update Client release
uses: andelf/nightly-release@main
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: nightly
name: Nightly Release
name: Nightly Client Release
body: |
Requires at least Windows 7 and DirectX 11 compatible GPU to run. Compiled with MSVC v14.44.35207 in Release mode with Whole Program Optimization, as well as `/O2 /Ot /Oi /Ob3 /GF /fp:precise`.
@ -44,3 +51,19 @@ jobs:
LCEWindows64.zip
./x64/Release/Minecraft.Client.exe
./x64/Release/Minecraft.Client.pdb
- name: Update Dedicated Server release
uses: andelf/nightly-release@main
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: nightly-dedicated-server
name: Nightly Dedicated Server Release
body: |
Dedicated Server runtime for Windows64.
Download `LCEServerWindows64.zip` and extract it to a folder where you'd like to keep the server runtime.
files: |
LCEServerWindows64.zip
./x64/Minecraft.Server/Release/Minecraft.Server.exe
./x64/Minecraft.Server/Release/Minecraft.Server.pdb

16
.gitignore vendored
View file

@ -379,11 +379,8 @@ MigrationBackup/
FodyWeavers.xsd
# VS Code files for those working on multiple tools
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.vscode/
!.vscode/*.example.json
*.code-workspace
# Local History for Visual Studio Code
@ -423,6 +420,12 @@ Minecraft.World/x64_Debug/
Minecraft.World/Release/
Minecraft.World/x64_Release/
Minecraft.Server/x64/
Minecraft.Server/Debug/
Minecraft.Server/x64_Debug/
Minecraft.Server/Release/
Minecraft.Server/x64_Release/
build/*
# Existing build output files
@ -434,6 +437,9 @@ build/*
# Local saves
Minecraft.Client/Saves/
tmp*/
_server_asset_probe/
server-data/
# Visual Studio Per-User Config
*.user
/out

View file

@ -92,6 +92,91 @@ target_link_libraries(MinecraftClient PRIVATE
>
)
set(MINECRAFT_SERVER_SOURCES ${MINECRAFT_CLIENT_SOURCES})
list(APPEND MINECRAFT_SERVER_SOURCES
"${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Server/Windows64/ServerMain.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Server/Access/Access.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Server/Access/BanManager.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Server/Access/WhitelistManager.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Server/Console/ServerCli.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Server/Console/ServerCliInput.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Server/Console/ServerCliParser.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Server/Console/ServerCliEngine.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Server/Console/ServerCliRegistry.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Server/Console/commands/ban/CliCommandBan.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Server/Console/commands/ban-ip/CliCommandBanIp.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Server/Console/commands/ban-list/CliCommandBanList.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Server/Console/commands/help/CliCommandHelp.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Server/Console/commands/pardon/CliCommandPardon.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Server/Console/commands/pardon-ip/CliCommandPardonIp.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Server/Console/commands/stop/CliCommandStop.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Server/Console/commands/list/CliCommandList.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Server/Console/commands/tp/CliCommandTp.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Server/Console/commands/whitelist/CliCommandWhitelist.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Server/Console/commands/gamemode/CliCommandGamemode.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Server/Console/commands/time/CliCommandTime.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Server/Console/commands/weather/CliCommandWeather.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Server/Console/commands/give/CliCommandGive.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Server/Console/commands/enchant/CliCommandEnchant.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Server/Console/commands/kill/CliCommandKill.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Server/Console/commands/defaultgamemode/CliCommandDefaultGamemode.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Server/Console/commands/experience/CliCommandExperience.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Server/Common/FileUtils.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Server/Common/StringUtils.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Server/ServerLogger.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Server/ServerLogManager.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Server/ServerProperties.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Server/vendor/linenoise/linenoise.c"
"${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Server/WorldManager.cpp"
)
add_executable(MinecraftServer ${MINECRAFT_SERVER_SOURCES})
target_include_directories(MinecraftServer PRIVATE
"${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Client"
"${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Client/Windows64/Iggy/include"
"${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Client/Xbox/Sentient/Include"
"${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.World/x64headers"
"${CMAKE_CURRENT_SOURCE_DIR}/include/"
"${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Server/Windows64"
)
target_compile_definitions(MinecraftServer PRIVATE
$<$<CONFIG:Debug>:_LARGE_WORLDS;_DEBUG_MENUS_ENABLED;_DEBUG;_CRT_NON_CONFORMING_SWPRINTFS;_CRT_SECURE_NO_WARNINGS;_WINDOWS64;MINECRAFT_SERVER_BUILD>
$<$<NOT:$<CONFIG:Debug>>:_LARGE_WORLDS;_DEBUG_MENUS_ENABLED;_CRT_NON_CONFORMING_SWPRINTFS;_CRT_SECURE_NO_WARNINGS;_WINDOWS64;MINECRAFT_SERVER_BUILD>
)
if(MSVC)
configure_msvc_target(MinecraftServer)
target_link_options(MinecraftServer PRIVATE
$<$<CONFIG:Release>:/LTCG /INCREMENTAL:NO>
)
endif()
set_target_properties(MinecraftServer PROPERTIES
OUTPUT_NAME "Minecraft.Server"
VS_DEBUGGER_WORKING_DIRECTORY "$<TARGET_FILE_DIR:MinecraftServer>"
VS_DEBUGGER_COMMAND_ARGUMENTS "-port 25565 -bind 0.0.0.0 -name DedicatedServer"
)
target_link_libraries(MinecraftServer PRIVATE
MinecraftWorld
d3d11
XInput9_1_0
wsock32
legacy_stdio_definitions
"${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Client/Windows64/Iggy/lib/iggy_w64.lib"
"${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Client/Windows64/Iggy/lib/iggyperfmon_w64.lib"
"${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Client/Windows64/Iggy/lib/iggyexpruntime_w64.lib"
$<$<CONFIG:Debug>:
"${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Client/Windows64/4JLibs/libs/4J_Input_d.lib"
"${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Client/Windows64/4JLibs/libs/4J_Storage_d.lib"
"${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Client/Windows64/4JLibs/libs/4J_Render_PC_d.lib"
>
$<$<NOT:$<CONFIG:Debug>>:
"${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Client/Windows64/4JLibs/libs/4J_Input.lib"
"${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Client/Windows64/4JLibs/libs/4J_Storage.lib"
"${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Client/Windows64/4JLibs/libs/4J_Render_PC.lib"
>
)
if(CMAKE_HOST_WIN32)
message(STATUS "Starting redist copy...")
execute_process(
@ -147,4 +232,13 @@ else()
message(FATAL_ERROR "Redist and asset copying is only supported on Windows (Robocopy) and Unix systems (rsync).")
endif()
set_property(DIRECTORY PROPERTY VS_STARTUP_PROJECT MinecraftClient)
add_custom_command(TARGET MinecraftServer POST_BUILD
COMMAND "${CMAKE_COMMAND}"
-DPROJECT_SOURCE_DIR="${CMAKE_CURRENT_SOURCE_DIR}"
-DOUTPUT_DIR="$<TARGET_FILE_DIR:MinecraftServer>"
-DCONFIGURATION=$<CONFIG>
-P "${CMAKE_CURRENT_SOURCE_DIR}/cmake/CopyServerAssets.cmake"
VERBATIM
)
set_property(DIRECTORY PROPERTY VS_STARTUP_PROJECT MinecraftServer)

View file

@ -3,7 +3,9 @@
## Visual Studio (`.sln`)
1. Open `MinecraftConsoles.sln` in Visual Studio 2022.
2. Set `Minecraft.Client` as the Startup Project.
2. Set Startup Project:
- Client: `Minecraft.Client`
- Dedicated server: `Minecraft.Server`
3. Select configuration:
- `Debug` (recommended), or
- `Release`
@ -12,6 +14,17 @@
- `Build > Build Solution` (or `Ctrl+Shift+B`)
- Start debugging with `F5`.
### Dedicated server debug arguments
- Default debugger arguments for `Minecraft.Server`:
- `-port 25565 -bind 0.0.0.0 -name DedicatedServer`
- You can override arguments in:
- `Project Properties > Debugging > Command Arguments`
- `Minecraft.Server` post-build copies only the dedicated-server asset set:
- `Common/Media/MediaWindows64.arc`
- `Common/res`
- `Windows64/GameHDD`
## CMake (Windows x64)
Configure (use your VS Community instance explicitly):
@ -32,6 +45,18 @@ Build Release:
cmake --build build --config Release --target MinecraftClient
```
Build Dedicated Server (Debug):
```powershell
cmake --build build --config Debug --target MinecraftServer
```
Build Dedicated Server (Release):
```powershell
cmake --build build --config Release --target MinecraftServer
```
Run executable:
```powershell
@ -39,6 +64,13 @@ cd .\build\Debug
.\MinecraftClient.exe
```
Run dedicated server:
```powershell
cd .\build\Debug
.\Minecraft.Server.exe -port 25565 -bind 0.0.0.0 -name DedicatedServer
```
Notes:
- The CMake build is Windows-only and x64-only.
- Contributors on macOS or Linux need a Windows machine or VM to build the project. Running the game via Wine is separate from having a supported build environment.

View file

@ -46,6 +46,13 @@ However, we would accept changes that...
- Having workable multi-platform compilation for ARM, Consoles, Linux
- Being a good base for further expansion and modding of LCE, such as backports and "modpacks".
# Scope of PRs
All Pull Requests should fully document the changes they include in their file changes. They should also be limited to one general topic and not touch all over the codebase unless its justifiable.
For example, we would not accept a PR that reworks UI, multiplayer code, and furnace ticking even if its a "fixup" PR as its too difficult to review a ton of code changes that are all irrelevant from each other. However, a PR focused on adding a bunch of commands or fixes several crashes that are otherwise irrelevant to each other would be accepted.
If your PR includes any undocumented changes it will be closed.
# Use of AI and LLMs
We currently do not accept any new code into the project that was written largely, entirely, or even noticably by an LLM. All contributions should be made by humans that understand the codebase.

View file

@ -739,9 +739,9 @@ void Chunk::rebuild_SPU()
{
// 4J - get tile from those copied into our local array in earlier optimisation
unsigned char tileId = pOutData->getTile(x,y,z);
if (tileId > 0)
if (tileId > 0 && tileId != 0xff)
{
if (currentLayer == 0 && Tile::tiles[tileId]->isEntityTile())
if (currentLayer == 0 && Tile::tiles[tileId] && Tile::tiles[tileId]->isEntityTile())
{
shared_ptr<TileEntity> et = region.getTileEntity(x, y, z);
if (TileEntityRenderDispatcher::instance->hasRenderer(et))
@ -754,6 +754,7 @@ void Chunk::rebuild_SPU()
{
Tile *tile = Tile::tiles[tileId];
if (!tile) continue;
int renderLayer = tile->getRenderLayer();
if (renderLayer != currentLayer)

View file

@ -149,8 +149,56 @@ bool ClientConnection::isPrimaryConnection() const
return g_NetworkManager.IsHost() || m_userIndex == ProfileManager.GetPrimaryPad();
}
ClientConnection* ClientConnection::findPrimaryConnection() const
{
if (level == nullptr) return nullptr;
int primaryPad = ProfileManager.GetPrimaryPad();
MultiPlayerLevel* mpLevel = (MultiPlayerLevel*)level;
for (ClientConnection* conn : mpLevel->connections)
{
if (conn != this && conn->m_userIndex == primaryPad)
return conn;
}
return nullptr;
}
bool ClientConnection::shouldProcessForEntity(int entityId) const
{
if (g_NetworkManager.IsHost()) return true;
if (m_userIndex == ProfileManager.GetPrimaryPad()) return true;
ClientConnection* primary = findPrimaryConnection();
if (primary == nullptr) return true;
return !primary->isTrackingEntity(entityId);
}
bool ClientConnection::shouldProcessForPosition(int blockX, int blockZ) const
{
if (g_NetworkManager.IsHost()) return true;
if (m_userIndex == ProfileManager.GetPrimaryPad()) return true;
ClientConnection* primary = findPrimaryConnection();
if (primary == nullptr) return true;
return !primary->m_visibleChunks.count(chunkKey(blockX >> 4, blockZ >> 4));
}
bool ClientConnection::anyOtherConnectionHasChunk(int x, int z) const
{
if (level == nullptr) return false;
MultiPlayerLevel* mpLevel = (MultiPlayerLevel*)level;
int64_t key = chunkKey(x, z);
for (ClientConnection* conn : mpLevel->connections)
{
if (conn != this && conn->m_visibleChunks.count(key))
return true;
}
return false;
}
ClientConnection::~ClientConnection()
{
m_trackedEntityIds.clear();
m_visibleChunks.clear();
delete connection;
delete random;
delete savedDataStorage;
@ -664,6 +712,7 @@ void ClientConnection::handleAddEntity(shared_ptr<AddEntityPacket> packet)
}
e->entityId = packet->id;
level->putEntity(packet->id, e);
m_trackedEntityIds.insert(packet->id);
if (packet->data > -1) // 4J - changed "no data" value to be -1, we can have a valid entity id of 0
{
@ -712,6 +761,7 @@ void ClientConnection::handleAddExperienceOrb(shared_ptr<AddExperienceOrbPacket>
e->xRot = 0;
e->entityId = packet->id;
level->putEntity(packet->id, e);
m_trackedEntityIds.insert(packet->id);
}
void ClientConnection::handleAddGlobalEntity(shared_ptr<AddGlobalEntityPacket> packet)
@ -738,13 +788,13 @@ void ClientConnection::handleAddPainting(shared_ptr<AddPaintingPacket> packet)
{
shared_ptr<Painting> painting = std::make_shared<Painting>(level, packet->x, packet->y, packet->z, packet->dir, packet->motive);
level->putEntity(packet->id, painting);
m_trackedEntityIds.insert(packet->id);
}
void ClientConnection::handleSetEntityMotion(shared_ptr<SetEntityMotionPacket> packet)
{
if (!isPrimaryConnection())
if (!shouldProcessForEntity(packet->id))
{
// Secondary connection: only accept motion for our own local player (knockback)
if (minecraft->localplayers[m_userIndex] == NULL ||
packet->id != minecraft->localplayers[m_userIndex]->entityId)
return;
@ -939,6 +989,7 @@ void ClientConnection::handleAddPlayer(shared_ptr<AddPlayerPacket> packet)
app.DebugPrintf("Custom cape for player %ls is %ls\n",player->name.c_str(),player->customTextureUrl2.c_str());
level->putEntity(packet->id, player);
m_trackedEntityIds.insert(packet->id);
vector<shared_ptr<SynchedEntityData::DataItem> > *unpackedData = packet->getUnpackedData();
if (unpackedData != nullptr)
@ -979,7 +1030,7 @@ void ClientConnection::handleSetCarriedItem(shared_ptr<SetCarriedItemPacket> pac
void ClientConnection::handleMoveEntity(shared_ptr<MoveEntityPacket> packet)
{
if (!isPrimaryConnection()) return;
if (!shouldProcessForEntity(packet->id)) return;
shared_ptr<Entity> e = getEntity(packet->id);
if (e == nullptr) return;
e->xp += packet->xa;
@ -1009,7 +1060,7 @@ void ClientConnection::handleRotateMob(shared_ptr<RotateHeadPacket> packet)
void ClientConnection::handleMoveEntitySmall(shared_ptr<MoveEntityPacketSmall> packet)
{
if (!isPrimaryConnection()) return;
if (!shouldProcessForEntity(packet->id)) return;
shared_ptr<Entity> e = getEntity(packet->id);
if (e == nullptr) return;
e->xp += packet->xa;
@ -1068,6 +1119,7 @@ void ClientConnection::handleRemoveEntity(shared_ptr<RemoveEntitiesPacket> packe
#endif
for (int i = 0; i < packet->ids.length; i++)
{
m_trackedEntityIds.erase(packet->ids[i]);
level->removeEntity(packet->ids[i]);
}
}
@ -1136,19 +1188,35 @@ void ClientConnection::handleChunkVisibilityArea(shared_ptr<ChunkVisibilityAreaP
{
if (level == NULL) return;
for(int z = packet->m_minZ; z <= packet->m_maxZ; ++z)
{
for(int x = packet->m_minX; x <= packet->m_maxX; ++x)
{
m_visibleChunks.insert(chunkKey(x, z));
level->setChunkVisible(x, z, true);
}
}
}
void ClientConnection::handleChunkVisibility(shared_ptr<ChunkVisibilityPacket> packet)
{
if (level == NULL) return;
level->setChunkVisible(packet->x, packet->z, packet->visible);
if (packet->visible)
{
m_visibleChunks.insert(chunkKey(packet->x, packet->z));
level->setChunkVisible(packet->x, packet->z, true);
}
else
{
m_visibleChunks.erase(chunkKey(packet->x, packet->z));
if (!anyOtherConnectionHasChunk(packet->x, packet->z))
{
level->setChunkVisible(packet->x, packet->z, false);
}
}
}
void ClientConnection::handleChunkTilesUpdate(shared_ptr<ChunkTilesUpdatePacket> packet)
{
if (!isPrimaryConnection()) return;
// 4J - changed to encode level in packet
MultiPlayerLevel *dimensionLevel = (MultiPlayerLevel *)minecraft->levels[packet->levelIdx];
if( dimensionLevel )
@ -1218,7 +1286,6 @@ void ClientConnection::handleChunkTilesUpdate(shared_ptr<ChunkTilesUpdatePacket>
void ClientConnection::handleBlockRegionUpdate(shared_ptr<BlockRegionUpdatePacket> packet)
{
if (!isPrimaryConnection()) return;
// 4J - changed to encode level in packet
MultiPlayerLevel *dimensionLevel = (MultiPlayerLevel *)minecraft->levels[packet->levelIdx];
if( dimensionLevel )
@ -1279,7 +1346,6 @@ void ClientConnection::handleBlockRegionUpdate(shared_ptr<BlockRegionUpdatePacke
void ClientConnection::handleTileUpdate(shared_ptr<TileUpdatePacket> packet)
{
if (!isPrimaryConnection()) return;
// 4J added - using a block of 255 to signify that this is a packet for destroying a tile, where we need to inform the level renderer that we are about to do so.
// This is used in creative mode as the point where a tile is first destroyed at the client end of things. Packets formed like this are potentially sent from
// ServerPlayerGameMode::destroyBlock
@ -1394,7 +1460,7 @@ void ClientConnection::send(shared_ptr<Packet> packet)
void ClientConnection::handleTakeItemEntity(shared_ptr<TakeItemEntityPacket> packet)
{
if (!isPrimaryConnection()) return;
if (!shouldProcessForEntity(packet->itemId)) return;
shared_ptr<Entity> from = getEntity(packet->itemId);
shared_ptr<LivingEntity> to = dynamic_pointer_cast<LivingEntity>(getEntity(packet->playerId));
@ -2414,6 +2480,8 @@ void ClientConnection::close()
// If it's already done, then we don't need to do anything here. And in fact trying to do something could cause a crash
if(done) return;
done = true;
m_trackedEntityIds.clear();
m_visibleChunks.clear();
connection->flush();
connection->close(DisconnectPacket::eDisconnect_Closed);
}
@ -2453,6 +2521,7 @@ void ClientConnection::handleAddMob(shared_ptr<AddMobPacket> packet)
mob->yd = packet->yd / 8000.0f;
mob->zd = packet->zd / 8000.0f;
level->putEntity(packet->id, mob);
m_trackedEntityIds.insert(packet->id);
vector<shared_ptr<SynchedEntityData::DataItem> > *unpackedData = packet->getUnpackedData();
if (unpackedData != nullptr)
@ -2792,6 +2861,9 @@ void ClientConnection::handleRespawn(shared_ptr<RespawnPacket> packet)
// so it doesn't leak into the new dimension
level->playStreamingMusic(L"", 0, 0, 0);
m_trackedEntityIds.clear();
m_visibleChunks.clear();
// Remove client connection from this level
level->removeClientConnection(this, false);
@ -2899,8 +2971,7 @@ void ClientConnection::handleRespawn(shared_ptr<RespawnPacket> packet)
void ClientConnection::handleExplosion(shared_ptr<ExplodePacket> packet)
{
// World modification (block destruction) must only happen once
if (isPrimaryConnection())
if (shouldProcessForPosition((int)packet->x, (int)packet->z))
{
if(!packet->m_bKnockbackOnly)
{
@ -3244,7 +3315,6 @@ void ClientConnection::handleTileEditorOpen(shared_ptr<TileEditorOpenPacket> pac
void ClientConnection::handleSignUpdate(shared_ptr<SignUpdatePacket> packet)
{
if (!isPrimaryConnection()) return;
app.DebugPrintf("ClientConnection::handleSignUpdate - ");
if (minecraft->level->hasChunkAt(packet->x, packet->y, packet->z))
{
@ -3278,7 +3348,6 @@ void ClientConnection::handleSignUpdate(shared_ptr<SignUpdatePacket> packet)
void ClientConnection::handleTileEntityData(shared_ptr<TileEntityDataPacket> packet)
{
if (!isPrimaryConnection()) return;
if (minecraft->level->hasChunkAt(packet->x, packet->y, packet->z))
{
shared_ptr<TileEntity> te = minecraft->level->getTileEntity(packet->x, packet->y, packet->z);
@ -3331,7 +3400,6 @@ void ClientConnection::handleContainerClose(shared_ptr<ContainerClosePacket> pac
void ClientConnection::handleTileEvent(shared_ptr<TileEventPacket> packet)
{
if (!isPrimaryConnection()) return;
PIXBeginNamedEvent(0,"Handle tile event\n");
minecraft->level->tileEvent(packet->x, packet->y, packet->z, packet->tile, packet->b0, packet->b1);
PIXEndNamedEvent();
@ -3339,7 +3407,6 @@ void ClientConnection::handleTileEvent(shared_ptr<TileEventPacket> packet)
void ClientConnection::handleTileDestruction(shared_ptr<TileDestructionPacket> packet)
{
if (!isPrimaryConnection()) return;
minecraft->level->destroyTileProgress(packet->getEntityId(), packet->getX(), packet->getY(), packet->getZ(), packet->getState());
}
@ -3421,7 +3488,6 @@ void ClientConnection::handleGameEvent(shared_ptr<GameEventPacket> gameEventPack
void ClientConnection::handleComplexItemData(shared_ptr<ComplexItemDataPacket> packet)
{
if (!isPrimaryConnection()) return;
if (packet->itemType == Item::map->id)
{
MapItem::getSavedData(packet->itemId, minecraft->level)->handleComplexItemData(packet->data);
@ -3436,7 +3502,7 @@ void ClientConnection::handleComplexItemData(shared_ptr<ComplexItemDataPacket> p
void ClientConnection::handleLevelEvent(shared_ptr<LevelEventPacket> packet)
{
if (!isPrimaryConnection()) return;
if (!shouldProcessForPosition(packet->x, packet->z)) return;
if (packet->type == LevelEvent::SOUND_DRAGON_DEATH)
{
for(unsigned int i = 0; i < XUSER_MAX_COUNT; ++i)
@ -3456,8 +3522,6 @@ void ClientConnection::handleLevelEvent(shared_ptr<LevelEventPacket> packet)
{
minecraft->level->levelEvent(packet->type, packet->x, packet->y, packet->z, packet->data);
}
minecraft->level->levelEvent(packet->type, packet->x, packet->y, packet->z, packet->data);
}
void ClientConnection::handleAwardStat(shared_ptr<AwardStatPacket> packet)
@ -3660,7 +3724,6 @@ void ClientConnection::handlePlayerAbilities(shared_ptr<PlayerAbilitiesPacket> p
void ClientConnection::handleSoundEvent(shared_ptr<LevelSoundPacket> packet)
{
if (!isPrimaryConnection()) return;
minecraft->level->playLocalSound(packet->getX(), packet->getY(), packet->getZ(), packet->getSound(), packet->getVolume(), packet->getPitch(), false);
}
@ -3973,7 +4036,6 @@ void ClientConnection::handleSetPlayerTeamPacket(shared_ptr<SetPlayerTeamPacket>
void ClientConnection::handleParticleEvent(shared_ptr<LevelParticlesPacket> packet)
{
if (!isPrimaryConnection()) return;
for (int i = 0; i < packet->getCount(); i++)
{
double xVarience = random->nextGaussian() * packet->getXDist();

View file

@ -1,4 +1,5 @@
#pragma once
#include <unordered_set>
#include "..\Minecraft.World\net.minecraft.network.h"
class Minecraft;
class MultiPlayerLevel;
@ -44,6 +45,20 @@ public:
private:
DWORD m_userIndex; // 4J Added
bool isPrimaryConnection() const;
std::unordered_set<int> m_trackedEntityIds;
std::unordered_set<int64_t> m_visibleChunks;
static int64_t chunkKey(int x, int z) { return ((int64_t)x << 32) | ((int64_t)z & 0xFFFFFFFF); }
ClientConnection* findPrimaryConnection() const;
bool shouldProcessForEntity(int entityId) const;
bool shouldProcessForPosition(int blockX, int blockZ) const;
bool anyOtherConnectionHasChunk(int x, int z) const;
public:
bool isTrackingEntity(int entityId) const { return m_trackedEntityIds.count(entityId) > 0; }
public:
SavedDataStorage *savedDataStorage;
ClientConnection(Minecraft *minecraft, const wstring& ip, int port);

View file

@ -1,4 +1,5 @@
#include "stdafx.h"
#include "ClientConstants.h"
const wstring ClientConstants::VERSION_STRING = wstring(L"Minecraft LCE ") + VER_FILEVERSION_STR_W;//+ SharedConstants::VERSION_STRING;
const wstring ClientConstants::VERSION_STRING = wstring(L"Minecraft LCE ") + VER_FILEVERSION_STR_W;//+ SharedConstants::VERSION_STRING;
const wstring ClientConstants::BRANCH_STRING = VER_BRANCHVERSION_STR_W;

View file

@ -13,6 +13,7 @@ class ClientConstants
// INTERNAL DEVELOPMENT SETTINGS
public:
static const wstring VERSION_STRING;
static const wstring BRANCH_STRING;
static const bool DEADMAU5_CAMERA_CHEATS = false;
};

View file

@ -260,9 +260,9 @@ void SoundEngine::updateMiniAudio()
continue;
}
float finalVolume = s->info.volume * m_MasterEffectsVolume;
if (finalVolume > 1.0f)
finalVolume = 1.0f;
float finalVolume = s->info.volume * m_MasterEffectsVolume * SFX_VOLUME_MULTIPLIER;
if (finalVolume > SFX_MAX_GAIN)
finalVolume = SFX_MAX_GAIN;
ma_sound_set_volume(&s->sound, finalVolume);
ma_sound_set_pitch(&s->sound, s->info.pitch);
@ -557,10 +557,13 @@ void SoundEngine::play(int iSound, float x, float y, float z, float volume, floa
}
ma_sound_set_spatialization_enabled(&s->sound, MA_TRUE);
ma_sound_set_min_distance(&s->sound, SFX_3D_MIN_DISTANCE);
ma_sound_set_max_distance(&s->sound, SFX_3D_MAX_DISTANCE);
ma_sound_set_rolloff(&s->sound, SFX_3D_ROLLOFF);
float finalVolume = volume * m_MasterEffectsVolume;
if (finalVolume > 1.0f)
finalVolume = 1.0f;
float finalVolume = volume * m_MasterEffectsVolume * SFX_VOLUME_MULTIPLIER;
if (finalVolume > SFX_MAX_GAIN)
finalVolume = SFX_MAX_GAIN;
ma_sound_set_volume(&s->sound, finalVolume);
ma_sound_set_pitch(&s->sound, pitch);

View file

@ -6,6 +6,12 @@ using namespace std;
#include "miniaudio.h"
constexpr float SFX_3D_MIN_DISTANCE = 1.0f;
constexpr float SFX_3D_MAX_DISTANCE = 16.0f;
constexpr float SFX_3D_ROLLOFF = 0.5f;
constexpr float SFX_VOLUME_MULTIPLIER = 1.5f;
constexpr float SFX_MAX_GAIN = 1.5f;
enum eMUSICFILES
{
eStream_Overworld_Calm1 = 0,

View file

@ -1,6 +1,7 @@
#pragma once
#define VER_PRODUCTBUILD 560
#define VER_PRODUCTVERSION_STR_W L"DEV (unknown)"
#define VER_PRODUCTVERSION_STR_W L"DEV (unknown version)"
#define VER_FILEVERSION_STR_W VER_PRODUCTVERSION_STR_W
#define VER_BRANCHVERSION_STR_W L"UNKNOWN BRANCH"
#define VER_NETWORK VER_PRODUCTBUILD

View file

@ -1,21 +1,32 @@
#include "stdafx.h"
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
#include "..\..\Minecraft.Server\ServerLogManager.h"
#endif
//--------------------------------------------------------------------------------------
// Name: DebugSpewV()
// Desc: Internal helper function
//--------------------------------------------------------------------------------------
#ifndef _CONTENT_PACKAGE
static VOID DebugSpewV( const CHAR* strFormat, const va_list pArgList )
static VOID DebugSpewV( const CHAR* strFormat, va_list pArgList )
{
#if defined __PS3__ || defined __ORBIS__ || defined __PSVITA__
assert(0);
assert(0);
#else
CHAR str[2048];
// Use the secure CRT to avoid buffer overruns. Specify a count of
// _TRUNCATE so that too long strings will be silently truncated
// rather than triggering an error.
_vsnprintf_s( str, _TRUNCATE, strFormat, pArgList );
OutputDebugStringA( str );
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
// Dedicated server routes legacy debug spew through ServerLogger to preserve CLI prompt handling.
if (ServerRuntime::ServerLogManager::ShouldForwardClientDebugLogs())
{
ServerRuntime::ServerLogManager::ForwardClientDebugSpewLogV(strFormat, pArgList);
return;
}
#endif
CHAR str[2048];
// Use the secure CRT to avoid buffer overruns. Specify a count of
// _TRUNCATE so that too long strings will be silently truncated
// rather than triggering an error.
_vsnprintf_s( str, _TRUNCATE, strFormat, pArgList );
OutputDebugStringA( str );
#endif
}
#endif
@ -31,10 +42,9 @@ VOID CDECL DebugPrintf( const CHAR* strFormat, ... )
#endif
{
#ifndef _CONTENT_PACKAGE
va_list pArgList;
va_start( pArgList, strFormat );
DebugSpewV( strFormat, pArgList );
va_end( pArgList );
va_list pArgList;
va_start( pArgList, strFormat );
DebugSpewV( strFormat, pArgList );
va_end( pArgList );
#endif
}

View file

@ -38,6 +38,9 @@
#include "GameRules\ConsoleSchematicFile.h"
#include "..\User.h"
#include "..\..\Minecraft.World\LevelData.h"
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
#include "..\..\Minecraft.Server\ServerLogManager.h"
#endif
#include "..\..\Minecraft.World\net.minecraft.world.entity.player.h"
#include "..\EntityRenderDispatcher.h"
#include "..\..\Minecraft.World\compression.h"
@ -240,12 +243,21 @@ void CMinecraftApp::DebugPrintf(const char *szFormat, ...)
{
#ifndef _FINAL_BUILD
char buf[1024];
va_list ap;
va_start(ap, szFormat);
vsnprintf(buf, sizeof(buf), szFormat, ap);
va_end(ap);
OutputDebugStringA(buf);
va_list ap;
va_start(ap, szFormat);
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
// Dedicated server routes client debug spew through ServerLogger so CLI output stays prompt-safe.
if (ServerRuntime::ServerLogManager::ShouldForwardClientDebugLogs())
{
ServerRuntime::ServerLogManager::ForwardClientAppDebugLogV(szFormat, ap);
va_end(ap);
return;
}
#endif
char buf[1024];
vsnprintf(buf, sizeof(buf), szFormat, ap);
va_end(ap);
OutputDebugStringA(buf);
#endif
}
@ -253,53 +265,62 @@ void CMinecraftApp::DebugPrintf(const char *szFormat, ...)
void CMinecraftApp::DebugPrintf(int user, const char *szFormat, ...)
{
#ifndef _FINAL_BUILD
if(user == USER_NONE)
return;
char buf[1024];
va_list ap;
va_start(ap, szFormat);
vsnprintf(buf, sizeof(buf), szFormat, ap);
va_end(ap);
if(user == USER_NONE)
return;
va_list ap;
va_start(ap, szFormat);
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
// Dedicated server routes client debug spew through ServerLogger so CLI output stays prompt-safe.
if (ServerRuntime::ServerLogManager::ShouldForwardClientDebugLogs())
{
ServerRuntime::ServerLogManager::ForwardClientUserDebugLogV(user, szFormat, ap);
va_end(ap);
return;
}
#endif
char buf[1024];
vsnprintf(buf, sizeof(buf), szFormat, ap);
va_end(ap);
#ifdef __PS3__
unsigned int writelen;
sys_tty_write(SYS_TTYP_USER1 + ( user - 1 ), buf, strlen(buf), &writelen );
unsigned int writelen;
sys_tty_write(SYS_TTYP_USER1 + ( user - 1 ), buf, strlen(buf), &writelen );
#elif defined __PSVITA__
switch(user)
{
case 0:
{
SceUID tty2 = sceIoOpen("tty2:", SCE_O_WRONLY, 0);
if(tty2>=0)
{
std::string string1(buf);
sceIoWrite(tty2, string1.c_str(), string1.length());
sceIoClose(tty2);
}
}
break;
case 1:
{
SceUID tty3 = sceIoOpen("tty3:", SCE_O_WRONLY, 0);
if(tty3>=0)
{
std::string string1(buf);
sceIoWrite(tty3, string1.c_str(), string1.length());
sceIoClose(tty3);
}
}
break;
default:
OutputDebugStringA(buf);
break;
}
switch(user)
{
case 0:
{
SceUID tty2 = sceIoOpen("tty2:", SCE_O_WRONLY, 0);
if(tty2>=0)
{
std::string string1(buf);
sceIoWrite(tty2, string1.c_str(), string1.length());
sceIoClose(tty2);
}
}
break;
case 1:
{
SceUID tty3 = sceIoOpen("tty3:", SCE_O_WRONLY, 0);
if(tty3>=0)
{
std::string string1(buf);
sceIoWrite(tty3, string1.c_str(), string1.length());
sceIoClose(tty3);
}
}
break;
default:
OutputDebugStringA(buf);
break;
}
#else
OutputDebugStringA(buf);
OutputDebugStringA(buf);
#endif
#ifndef _XBOX
if(user == USER_UI)
{
ui.logDebugString(buf);
}
if(user == USER_UI)
{
ui.logDebugString(buf);
}
#endif
#endif
}

View file

@ -344,6 +344,7 @@ void GameRuleManager::writeRuleFile(DataOutputStream *dos)
// Write schematic files.
unordered_map<wstring, ConsoleSchematicFile *> *files;
files = getLevelGenerationOptions()->getUnfinishedSchematicFiles();
dos->writeInt((int)files->size());
for ( auto& it : *files )
{
const wstring& filename = it.first;
@ -497,17 +498,36 @@ bool GameRuleManager::readRuleFile(LevelGenerationOptions *lgo, byte *dIn, UINT
}*/
// subfile
// Old saves didn't write a numFiles count before the schematic entries.
// Detect this: a real count is small, but a UTF filename prefix reads as a large int.
UINT numFiles = contentDis->readInt();
for (UINT i = 0; i < numFiles; i++)
if (lgo->isFromSave() && numFiles > 100)
{
wstring sFilename = contentDis->readUTF();
int length = contentDis->readInt();
byteArray ba( length );
contentDis->read(ba);
levelGenerator->loadSchematicFile(sFilename, ba.data, ba.length);
contentDis->skip(-4);
while (true)
{
int peek = contentDis->readInt();
if (peek <= 100) { contentDis->skip(-4); break; }
contentDis->skip(-4);
wstring sFilename = contentDis->readUTF();
int length = contentDis->readInt();
byteArray ba( length );
contentDis->read(ba);
levelGenerator->loadSchematicFile(sFilename, ba.data, ba.length);
}
}
else
{
for (UINT i = 0; i < numFiles; i++)
{
wstring sFilename = contentDis->readUTF();
int length = contentDis->readInt();
byteArray ba( length );
contentDis->read(ba);
levelGenerator->loadSchematicFile(sFilename, ba.data, ba.length);
}
}
LEVEL_GEN_ID lgoID = LEVEL_GEN_ID_NULL;

View file

@ -455,6 +455,74 @@ unordered_map<wstring, ConsoleSchematicFile *> *LevelGenerationOptions::getUnfin
void LevelGenerationOptions::loadBaseSaveData()
{
#ifdef _WINDOWS64
int gameRulesCount = m_parentDLCPack ? m_parentDLCPack->getDLCItemsCount(DLCManager::e_DLCType_GameRulesHeader) : 0;
wstring baseSave = getBaseSavePath();
wstring packName = baseSave.substr(0, baseSave.find(L'.'));
for (int i = 0; i < gameRulesCount; ++i)
{
DLCGameRulesHeader* dlcFile = static_cast<DLCGameRulesHeader*>(m_parentDLCPack->getFile(DLCManager::e_DLCType_GameRulesHeader, i));
if (!dlcFile->getGrfPath().empty())
{
File grf(L"Windows64Media\\DLC\\" + packName + L"\\Data\\" + dlcFile->getGrfPath());
if (grf.exists())
{
wstring path = grf.getPath();
HANDLE fileHandle = CreateFileW(path.c_str(), GENERIC_READ, 0, nullptr, OPEN_EXISTING, FILE_FLAG_SEQUENTIAL_SCAN, nullptr);
if (fileHandle != INVALID_HANDLE_VALUE)
{
DWORD dwFileSize = grf.length();
DWORD bytesRead;
PBYTE pbData = new BYTE[dwFileSize];
BOOL bSuccess = ReadFile(fileHandle, pbData, dwFileSize, &bytesRead, nullptr);
CloseHandle(fileHandle);
if (bSuccess)
{
dlcFile->setGrfData(pbData, dwFileSize, m_stringTable);
app.m_gameRules.setLevelGenerationOptions(dlcFile->lgo);
}
delete[] pbData;
}
}
}
}
if (requiresBaseSave() && !getBaseSavePath().empty())
{
File save(L"Windows64Media\\DLC\\" + packName + L"\\Data\\" + baseSave);
if (save.exists())
{
wstring path = save.getPath();
HANDLE fileHandle = CreateFileW(path.c_str(), GENERIC_READ, 0, nullptr, OPEN_EXISTING, FILE_FLAG_SEQUENTIAL_SCAN, nullptr);
if (fileHandle != INVALID_HANDLE_VALUE)
{
DWORD dwFileSize = GetFileSize(fileHandle, nullptr);
DWORD bytesRead;
PBYTE pbData = new BYTE[dwFileSize];
BOOL bSuccess = ReadFile(fileHandle, pbData, dwFileSize, &bytesRead, nullptr);
CloseHandle(fileHandle);
if (bSuccess)
setBaseSaveData(pbData, dwFileSize);
else
delete[] pbData;
}
}
}
setLoadedData();
app.SetAction(ProfileManager.GetPrimaryPad(), eAppAction_ReloadTexturePack);
#else
int mountIndex = -1;
if(m_parentDLCPack != nullptr) mountIndex = m_parentDLCPack->GetDLCMountIndex();
@ -481,6 +549,7 @@ void LevelGenerationOptions::loadBaseSaveData()
setLoadedData();
app.SetAction(ProfileManager.GetPrimaryPad(), eAppAction_ReloadTexturePack);
}
#endif
}
int LevelGenerationOptions::packMounted(LPVOID pParam,int iPad,DWORD dwErr,DWORD dwLicenceMask)

View file

@ -200,10 +200,12 @@ bool CGameNetworkManager::StartNetworkGame(Minecraft *minecraft, LPVOID lpParame
#endif
int64_t seed = 0;
bool dedicatedNoLocalHostPlayer = false;
if (lpParameter != nullptr)
{
NetworkGameInitData *param = static_cast<NetworkGameInitData *>(lpParameter);
seed = param->seed;
dedicatedNoLocalHostPlayer = param->dedicatedNoLocalHostPlayer;
app.setLevelGenerationOptions(param->levelGen);
if(param->levelGen != nullptr)
@ -359,9 +361,19 @@ bool CGameNetworkManager::StartNetworkGame(Minecraft *minecraft, LPVOID lpParame
// PRIMARY PLAYER
vector<ClientConnection *> createdConnections;
ClientConnection *connection;
ClientConnection *connection = nullptr;
if( g_NetworkManager.IsHost() )
if( g_NetworkManager.IsHost() && dedicatedNoLocalHostPlayer )
{
app.DebugPrintf("Dedicated server mode: skipping local host client connection\n");
// Keep telemetry behavior consistent with the host path.
INT multiplayerInstanceId = TelemetryManager->GenerateMultiplayerInstanceId();
TelemetryManager->SetMultiplayerInstanceId(multiplayerInstanceId);
app.SetGameMode( eMode_Multiplayer );
}
else if( g_NetworkManager.IsHost() )
{
connection = new ClientConnection(minecraft, nullptr);
}
@ -390,16 +402,18 @@ bool CGameNetworkManager::StartNetworkGame(Minecraft *minecraft, LPVOID lpParame
connection = new ClientConnection(minecraft, socket);
}
if( !connection->createdOk )
if (connection != nullptr)
{
assert(false);
delete connection;
connection = nullptr;
MinecraftServer::HaltServer();
return false;
}
if( !connection->createdOk )
{
assert(false);
delete connection;
connection = nullptr;
MinecraftServer::HaltServer();
return false;
}
connection->send(std::make_shared<PreLoginPacket>(minecraft->user->name));
connection->send(std::make_shared<PreLoginPacket>(minecraft->user->name));
// Tick connection until we're ready to go. The stages involved in this are:
// (1) Creating the ClientConnection sends a prelogin packet to the server
@ -434,9 +448,9 @@ bool CGameNetworkManager::StartNetworkGame(Minecraft *minecraft, LPVOID lpParame
connection->close();
}
if( connection->isStarted() && !connection->isClosed() )
{
createdConnections.push_back( connection );
if( connection->isStarted() && !connection->isClosed() )
{
createdConnections.push_back( connection );
int primaryPad = ProfileManager.GetPrimaryPad();
app.SetRichPresenceContext(primaryPad,CONTEXT_GAME_STATE_BLANK);
@ -533,13 +547,14 @@ bool CGameNetworkManager::StartNetworkGame(Minecraft *minecraft, LPVOID lpParame
}
}
app.SetGameMode( eMode_Multiplayer );
}
else if ( connection->isClosed() || !IsInSession())
{
app.SetGameMode( eMode_Multiplayer );
}
else if ( connection->isClosed() || !IsInSession())
{
// assert(false);
MinecraftServer::HaltServer();
return false;
MinecraftServer::HaltServer();
return false;
}
}
@ -942,13 +957,18 @@ int CGameNetworkManager::ServerThreadProc( void* lpParameter )
app.SetGameHostOption(eGameHostOption_All,param->settings);
// 4J Stu - If we are loading a DLC save that's separate from the texture pack, load
if( param->levelGen != nullptr && (param->texturePackId == 0 || param->levelGen->getRequiredTexturePackId() != param->texturePackId) )
if (param != nullptr && param->levelGen != nullptr && param->levelGen->isFromDLC())
{
while((Minecraft::GetInstance()->skins->needsUIUpdate() || ui.IsReloadingSkin()))
{
Sleep(1);
}
param->levelGen->loadBaseSaveData();
while (!param->levelGen->hasLoadedData())
{
Sleep(1);
}
}
}

View file

@ -240,7 +240,13 @@ void CPlatformNetworkManagerStub::DoWork()
qnetPlayer->m_resolvedXuid = INVALID_XUID;
qnetPlayer->m_gamertag[0] = 0;
qnetPlayer->SetCustomDataValue(0);
if (IQNet::s_playerCount > 1)
// Recalculate s_playerCount as the highest active slot + 1.
// A blind decrement would hide players at higher-indexed slots when a
// lower-indexed player disconnects first: GetPlayerBySmallId scans
// [0, s_playerCount) so any slot at or above the decremented count
// becomes invisible, causing its disconnect to be missed (ghost player).
while (IQNet::s_playerCount > 1 &&
IQNet::m_player[IQNet::s_playerCount - 1].GetCustomDataValue() == 0)
IQNet::s_playerCount--;
}
// NOTE: Do NOT call PushFreeSmallId here. The old PlayerConnection's
@ -257,6 +263,25 @@ void CPlatformNetworkManagerStub::DoWork()
SystemFlagRemoveBySmallId(disconnectedSmallId);
}
}
// Client-side host disconnect detection:
// if TCP is gone, propagate through normal network-disconnect flow so UI returns to menus.
// The processing from the Xbox version will be reused.
if (_iQNetStubState == QNET_STATE_GAME_PLAY && !m_pIQNet->IsHost() && !m_bLeavingGame)
{
if (!WinsockNetLayer::IsConnected())
{
if (!m_bLeaveGameOnTick)
{
m_bLeaveGameOnTick = true;
g_NetworkManager.HandleDisconnect(false);
}
}
else
{
m_bLeaveGameOnTick = false;
}
}
#endif
}
@ -356,6 +381,7 @@ bool CPlatformNetworkManagerStub::LeaveGame(bool bMigrateHost)
if( m_bLeavingGame ) return true;
m_bLeavingGame = true;
m_bLeaveGameOnTick = false;
#ifdef _WINDOWS64
WinsockNetLayer::StopAdvertising();
@ -404,6 +430,7 @@ void CPlatformNetworkManagerStub::HostGame(int localUsersMask, bool bOnlineGame,
localUsersMask |= GetLocalPlayerMask( g_NetworkManager.GetPrimaryPad() );
m_bLeavingGame = false;
m_bLeaveGameOnTick = false;
m_pIQNet->HostGame();
@ -433,9 +460,23 @@ void CPlatformNetworkManagerStub::HostGame(int localUsersMask, bool bOnlineGame,
if (WinsockNetLayer::IsActive())
{
const wchar_t* hostName = IQNet::m_player[0].m_gamertag;
unsigned int settings = app.GetGameHostOption(eGameHostOption_All);
WinsockNetLayer::StartAdvertising(port, hostName, settings, 0, 0, MINECRAFT_NET_VERSION);
// For Dedicated Server, refer to `lan-advertise` in `server.properties`
bool enableLanAdvertising = true;
if (g_Win64DedicatedServer)
{
enableLanAdvertising = g_Win64DedicatedServerLanAdvertise;
}
if (enableLanAdvertising)
{
const wchar_t* hostName = IQNet::m_player[0].m_gamertag;
unsigned int settings = app.GetGameHostOption(eGameHostOption_All);
WinsockNetLayer::StartAdvertising(port, hostName, settings, 0, 0, MINECRAFT_NET_VERSION);
}
else
{
WinsockNetLayer::StopAdvertising();
}
}
#endif
//#endif
@ -463,6 +504,7 @@ int CPlatformNetworkManagerStub::JoinGame(FriendSessionInfo* searchResult, int l
return CGameNetworkManager::JOINGAME_FAIL_GENERAL;
m_bLeavingGame = false;
m_bLeaveGameOnTick = false;
IQNet::s_isHosting = false;
m_pIQNet->ClientJoinGame();

View file

@ -51,7 +51,10 @@ private:
struct GammaCBData
{
float gamma;
float pad[3];
float pad;
float uvOffsetX, uvOffsetY;
float uvScaleX, uvScaleY;
float pad2[2];
};
static const char* g_gammaVSCode;

View file

@ -1298,10 +1298,8 @@ void IUIScene_AbstractContainerMenu::onMouseTick()
}
}
vPointerPos.x = floor(vPointerPos.x);
vPointerPos.x += ( static_cast<int>(vPointerPos.x)%2);
vPointerPos.y = floor(vPointerPos.y);
vPointerPos.y += ( static_cast<int>(vPointerPos.y)%2);
vPointerPos.x = static_cast<float>(floor(vPointerPos.x + 0.5f));
vPointerPos.y = static_cast<float>(floor(vPointerPos.y + 0.5f));
m_pointerPos = vPointerPos;
adjustPointerForSafeZone();

View file

@ -195,8 +195,8 @@ void IUIScene_HUD::renderPlayerHealth()
// Update health
bool blink = pMinecraft->localplayers[iPad]->invulnerableTime / 3 % 2 == 1;
if (pMinecraft->localplayers[iPad]->invulnerableTime < 10) blink = false;
int currentHealth = pMinecraft->localplayers[iPad]->getHealth();
int oldHealth = pMinecraft->localplayers[iPad]->lastHealth;
int currentHealth = static_cast<int>(ceil(pMinecraft->localplayers[iPad]->getHealth()));
int oldHealth = static_cast<int>(ceil(pMinecraft->localplayers[iPad]->lastHealth));
bool bHasPoison = pMinecraft->localplayers[iPad]->hasEffect(MobEffect::poison);
bool bHasWither = pMinecraft->localplayers[iPad]->hasEffect(MobEffect::wither);
AttributeInstance *maxHealthAttribute = pMinecraft->localplayers[iPad]->getAttribute(SharedMonsterAttributes::MAX_HEALTH);

View file

@ -143,13 +143,17 @@ void UIComponent_Chat::render(S32 width, S32 height, C4JRender::eViewportType vi
F32 scale;
ComputeTileScale(tileWidth, tileHeight, m_movieWidth, m_movieHeight, needsYTile, scale, tileYStart);
IggyPlayerSetDisplaySize( getMovie(), (S32)(m_movieWidth * scale), (S32)(m_movieHeight * scale) );
S32 contentOffX, contentOffY;
ComputeSplitContentOffset(viewport, m_movieWidth, m_movieHeight, scale, tileWidth, tileHeight, tileYStart, contentOffX, contentOffY);
xPos += contentOffX;
yPos += contentOffY;
ui.setupRenderPosition(xPos, yPos);
// For vertical split, scale down to fit the full SWF height when the
// window is shorter than the movie (same fix as HUD).
if(!needsYTile && m_movieHeight > 0)
{
F32 scaleH = (F32)tileHeight / (F32)m_movieHeight;
if(scaleH < scale)
scale = scaleH;
}
IggyPlayerSetDisplaySize( getMovie(), (S32)(m_movieWidth * scale), (S32)(m_movieHeight * scale) );
IggyPlayerDrawTilesStart ( getMovie() );

View file

@ -93,18 +93,22 @@ void UIComponent_Tooltips::updateSafeZone()
case C4JRender::VIEWPORT_TYPE_SPLIT_TOP:
safeTop = getSafeZoneHalfHeight();
safeLeft = getSafeZoneHalfWidth();
break;
case C4JRender::VIEWPORT_TYPE_SPLIT_BOTTOM:
safeBottom = getSafeZoneHalfHeight();
safeTop = getSafeZoneHalfHeight();
safeLeft = getSafeZoneHalfWidth();
break;
case C4JRender::VIEWPORT_TYPE_SPLIT_LEFT:
safeLeft = getSafeZoneHalfWidth();
safeTop = getSafeZoneHalfHeight();
safeBottom = getSafeZoneHalfHeight();
safeLeft = getSafeZoneHalfWidth();
break;
case C4JRender::VIEWPORT_TYPE_SPLIT_RIGHT:
safeRight = getSafeZoneHalfWidth();
safeTop = getSafeZoneHalfHeight();
safeBottom = getSafeZoneHalfHeight();
break;
case C4JRender::VIEWPORT_TYPE_QUADRANT_TOP_LEFT:
safeTop = getSafeZoneHalfHeight();
@ -112,22 +116,22 @@ void UIComponent_Tooltips::updateSafeZone()
break;
case C4JRender::VIEWPORT_TYPE_QUADRANT_TOP_RIGHT:
safeTop = getSafeZoneHalfHeight();
safeRight = getSafeZoneHalfWidth();
break;
case C4JRender::VIEWPORT_TYPE_QUADRANT_BOTTOM_LEFT:
safeBottom = getSafeZoneHalfHeight();
safeTop = getSafeZoneHalfHeight();
safeLeft = getSafeZoneHalfWidth();
break;
case C4JRender::VIEWPORT_TYPE_QUADRANT_BOTTOM_RIGHT:
safeBottom = getSafeZoneHalfHeight();
safeRight = getSafeZoneHalfWidth();
safeTop = getSafeZoneHalfHeight();
break;
case C4JRender::VIEWPORT_TYPE_FULLSCREEN:
default:
safeTop = getSafeZoneHalfHeight();
safeBottom = getSafeZoneHalfHeight();
safeLeft = getSafeZoneHalfWidth();
safeRight = getSafeZoneHalfWidth();
break;
}
setSafeZone(safeTop, safeBottom, safeLeft, safeRight);
@ -247,13 +251,17 @@ void UIComponent_Tooltips::render(S32 width, S32 height, C4JRender::eViewportTyp
F32 scale;
ComputeTileScale(tileWidth, tileHeight, m_movieWidth, m_movieHeight, needsYTile, scale, tileYStart);
IggyPlayerSetDisplaySize( getMovie(), (S32)(m_movieWidth * scale), (S32)(m_movieHeight * scale) );
S32 contentOffX, contentOffY;
ComputeSplitContentOffset(viewport, m_movieWidth, m_movieHeight, scale, tileWidth, tileHeight, tileYStart, contentOffX, contentOffY);
xPos += contentOffX;
yPos += contentOffY;
ui.setupRenderPosition(xPos, yPos);
// For vertical split, scale down to fit the full SWF height when the
// window is shorter than the movie (same fix as HUD).
if(!needsYTile && m_movieHeight > 0)
{
F32 scaleH = (F32)tileHeight / (F32)m_movieHeight;
if(scaleH < scale)
scale = scaleH;
}
IggyPlayerSetDisplaySize( getMovie(), (S32)(m_movieWidth * scale), (S32)(m_movieHeight * scale) );
IggyPlayerDrawTilesStart ( getMovie() );

View file

@ -1,6 +1,7 @@
#include "stdafx.h"
#include "UI.h"
#include "UIComponent_TutorialPopup.h"
#include "UISplitScreenHelpers.h"
#include "..\..\Common\Tutorial\Tutorial.h"
#include "..\..\..\Minecraft.World\StringHelpers.h"
#include "..\..\MultiplayerLocalPlayer.h"
@ -474,27 +475,17 @@ void UIComponent_TutorialPopup::render(S32 width, S32 height, C4JRender::eViewpo
{
if(viewport != C4JRender::VIEWPORT_TYPE_FULLSCREEN)
{
S32 xPos = 0;
S32 yPos = 0;
switch( viewport )
{
case C4JRender::VIEWPORT_TYPE_SPLIT_BOTTOM:
xPos = static_cast<S32>(ui.getScreenWidth() / 2);
yPos = static_cast<S32>(ui.getScreenHeight() / 2);
break;
case C4JRender::VIEWPORT_TYPE_QUADRANT_BOTTOM_LEFT:
yPos = static_cast<S32>(ui.getScreenHeight() / 2);
break;
case C4JRender::VIEWPORT_TYPE_SPLIT_TOP:
case C4JRender::VIEWPORT_TYPE_SPLIT_RIGHT:
case C4JRender::VIEWPORT_TYPE_QUADRANT_TOP_RIGHT:
xPos = static_cast<S32>(ui.getScreenWidth() / 2);
break;
case C4JRender::VIEWPORT_TYPE_QUADRANT_BOTTOM_RIGHT:
xPos = static_cast<S32>(ui.getScreenWidth() / 2);
yPos = static_cast<S32>(ui.getScreenHeight() / 2);
break;
}
// Derive the viewport origin and fit a 16:9 box inside it (same as UIScene::render),
// then apply safezone nudges so the popup stays clear of screen edges.
F32 originX, originY, viewW, viewH;
GetViewportRect(ui.getScreenWidth(), ui.getScreenHeight(), viewport, originX, originY, viewW, viewH);
S32 fitW, fitH, offsetX, offsetY;
Fit16x9(viewW, viewH, fitW, fitH, offsetX, offsetY);
S32 xPos = static_cast<S32>(originX) + offsetX;
S32 yPos = static_cast<S32>(originY) + offsetY;
//Adjust for safezone
switch( viewport )
{
@ -505,6 +496,7 @@ void UIComponent_TutorialPopup::render(S32 width, S32 height, C4JRender::eViewpo
case C4JRender::VIEWPORT_TYPE_QUADRANT_TOP_RIGHT:
yPos += getSafeZoneHalfHeight();
break;
default: break;
}
switch( viewport )
{
@ -515,10 +507,11 @@ void UIComponent_TutorialPopup::render(S32 width, S32 height, C4JRender::eViewpo
case C4JRender::VIEWPORT_TYPE_QUADRANT_BOTTOM_RIGHT:
xPos -= getSafeZoneHalfWidth();
break;
default: break;
}
ui.setupRenderPosition(xPos, yPos);
IggyPlayerSetDisplaySize( getMovie(), width, height );
IggyPlayerSetDisplaySize( getMovie(), fitW, fitH );
IggyPlayerDraw( getMovie() );
}
else

View file

@ -522,33 +522,9 @@ void UIController::tick()
void UIController::loadSkins()
{
wstring platformSkinPath = L"";
#ifdef __PS3__
platformSkinPath = L"skinPS3.swf";
#elif defined __PSVITA__
platformSkinPath = L"skinVita.swf";
#elif defined _WINDOWS64
// Windows64/Durango/Orbis always load HD skin libraries unconditionally,
// which import "platformskinHD.swf" by name. The platform skin must
// therefore always be the HD variant regardless of screen resolution.
platformSkinPath = L"skinHDWin.swf";
#elif defined _DURANGO
platformSkinPath = L"skinHDDurango.swf";
#elif defined __ORBIS__
platformSkinPath = L"skinHDOrbis.swf";
m_iggyLibraries[eLibrary_Platform] = loadSkin(L"skinPS3.swf", L"platformskin.swf");
#endif
// Every platform has one of these, so nothing shared.
// On HD platforms (Win64/Durango/Orbis) the skin libraries always import
// "platformskinHD.swf", so we must register under that name at any resolution.
#if defined(_WINDOWS64) || defined(_DURANGO) || defined(__ORBIS__)
m_iggyLibraries[eLibrary_Platform] = loadSkin(platformSkinPath, L"platformskinHD.swf");
#else
m_iggyLibraries[eLibrary_Platform] = loadSkin(platformSkinPath, L"platformskin.swf");
#endif
#if defined(__PS3__) || defined(__PSVITA__)
m_iggyLibraries[eLibrary_GraphicsDefault] = loadSkin(L"skinGraphics.swf", L"skinGraphics.swf");
m_iggyLibraries[eLibrary_GraphicsHUD] = loadSkin(L"skinGraphicsHud.swf", L"skinGraphicsHud.swf");
m_iggyLibraries[eLibrary_GraphicsInGame] = loadSkin(L"skinGraphicsInGame.swf", L"skinGraphicsInGame.swf");
@ -559,13 +535,28 @@ void UIController::loadSkins()
m_iggyLibraries[eLibrary_HUD] = loadSkin(L"skinHud.swf", L"skinHud.swf");
m_iggyLibraries[eLibrary_Tooltips] = loadSkin(L"skinTooltips.swf", L"skinTooltips.swf");
m_iggyLibraries[eLibrary_Default] = loadSkin(L"skin.swf", L"skin.swf");
#endif
#if ( defined(_WINDOWS64) || defined(_DURANGO) || defined(__ORBIS__) )
#elif defined __PSVITA__
m_iggyLibraries[eLibrary_Platform] = loadSkin(L"skinVita.swf", L"platformskin.swf");
#if defined(_WINDOWS64)
// 4J Stu - Load the 720/480 skins so that we have something to fallback on during development
#ifndef _FINAL_BUILD
m_iggyLibraries[eLibrary_GraphicsDefault] = loadSkin(L"skinGraphics.swf", L"skinGraphics.swf");
m_iggyLibraries[eLibrary_GraphicsHUD] = loadSkin(L"skinGraphicsHud.swf", L"skinGraphicsHud.swf");
m_iggyLibraries[eLibrary_GraphicsInGame] = loadSkin(L"skinGraphicsInGame.swf", L"skinGraphicsInGame.swf");
m_iggyLibraries[eLibrary_GraphicsTooltips] = loadSkin(L"skinGraphicsTooltips.swf", L"skinGraphicsTooltips.swf");
m_iggyLibraries[eLibrary_GraphicsLabels] = loadSkin(L"skinGraphicsLabels.swf", L"skinGraphicsLabels.swf");
m_iggyLibraries[eLibrary_Labels] = loadSkin(L"skinLabels.swf", L"skinLabels.swf");
m_iggyLibraries[eLibrary_InGame] = loadSkin(L"skinInGame.swf", L"skinInGame.swf");
m_iggyLibraries[eLibrary_HUD] = loadSkin(L"skinHud.swf", L"skinHud.swf");
m_iggyLibraries[eLibrary_Tooltips] = loadSkin(L"skinTooltips.swf", L"skinTooltips.swf");
m_iggyLibraries[eLibrary_Default] = loadSkin(L"skin.swf", L"skin.swf");
#elif defined _WINDOWS64
// HD platform skin — required by skinHD*.swf (1080p scene SWFs)
m_iggyLibraries[eLibrary_Platform] = loadSkin(L"skinHDWin.swf", L"platformskinHD.swf");
// Non-HD platform skin — required by skin*.swf (720p/480p scene SWFs)
m_iggyLibraries[eLibraryFallback_Platform] = loadSkin(L"skinWin.swf", L"platformskin.swf");
// Non-HD skin set (720p/480p scenes import these)
m_iggyLibraries[eLibraryFallback_GraphicsDefault] = loadSkin(L"skinGraphics.swf", L"skinGraphics.swf");
m_iggyLibraries[eLibraryFallback_GraphicsHUD] = loadSkin(L"skinGraphicsHud.swf", L"skinGraphicsHud.swf");
m_iggyLibraries[eLibraryFallback_GraphicsInGame] = loadSkin(L"skinGraphicsInGame.swf", L"skinGraphicsInGame.swf");
@ -576,8 +567,21 @@ void UIController::loadSkins()
m_iggyLibraries[eLibraryFallback_HUD] = loadSkin(L"skinHud.swf", L"skinHud.swf");
m_iggyLibraries[eLibraryFallback_Tooltips] = loadSkin(L"skinTooltips.swf", L"skinTooltips.swf");
m_iggyLibraries[eLibraryFallback_Default] = loadSkin(L"skin.swf", L"skin.swf");
#endif
#endif
// HD skin set (1080p scenes import these)
m_iggyLibraries[eLibrary_GraphicsDefault] = loadSkin(L"skinHDGraphics.swf", L"skinHDGraphics.swf");
m_iggyLibraries[eLibrary_GraphicsHUD] = loadSkin(L"skinHDGraphicsHud.swf", L"skinHDGraphicsHud.swf");
m_iggyLibraries[eLibrary_GraphicsInGame] = loadSkin(L"skinHDGraphicsInGame.swf", L"skinHDGraphicsInGame.swf");
m_iggyLibraries[eLibrary_GraphicsTooltips] = loadSkin(L"skinHDGraphicsTooltips.swf", L"skinHDGraphicsTooltips.swf");
m_iggyLibraries[eLibrary_GraphicsLabels] = loadSkin(L"skinHDGraphicsLabels.swf", L"skinHDGraphicsLabels.swf");
m_iggyLibraries[eLibrary_Labels] = loadSkin(L"skinHDLabels.swf", L"skinHDLabels.swf");
m_iggyLibraries[eLibrary_InGame] = loadSkin(L"skinHDInGame.swf", L"skinHDInGame.swf");
m_iggyLibraries[eLibrary_HUD] = loadSkin(L"skinHDHud.swf", L"skinHDHud.swf");
m_iggyLibraries[eLibrary_Tooltips] = loadSkin(L"skinHDTooltips.swf", L"skinHDTooltips.swf");
m_iggyLibraries[eLibrary_Default] = loadSkin(L"skinHD.swf", L"skinHD.swf");
#elif defined _DURANGO
m_iggyLibraries[eLibrary_Platform] = loadSkin(L"skinHDDurango.swf", L"platformskinHD.swf");
m_iggyLibraries[eLibrary_GraphicsDefault] = loadSkin(L"skinHDGraphics.swf", L"skinHDGraphics.swf");
m_iggyLibraries[eLibrary_GraphicsHUD] = loadSkin(L"skinHDGraphicsHud.swf", L"skinHDGraphicsHud.swf");
@ -589,7 +593,21 @@ void UIController::loadSkins()
m_iggyLibraries[eLibrary_HUD] = loadSkin(L"skinHDHud.swf", L"skinHDHud.swf");
m_iggyLibraries[eLibrary_Tooltips] = loadSkin(L"skinHDTooltips.swf", L"skinHDTooltips.swf");
m_iggyLibraries[eLibrary_Default] = loadSkin(L"skinHD.swf", L"skinHD.swf");
#endif // HD platforms
#elif defined __ORBIS__
m_iggyLibraries[eLibrary_Platform] = loadSkin(L"skinHDOrbis.swf", L"platformskinHD.swf");
m_iggyLibraries[eLibrary_GraphicsDefault] = loadSkin(L"skinHDGraphics.swf", L"skinHDGraphics.swf");
m_iggyLibraries[eLibrary_GraphicsHUD] = loadSkin(L"skinHDGraphicsHud.swf", L"skinHDGraphicsHud.swf");
m_iggyLibraries[eLibrary_GraphicsInGame] = loadSkin(L"skinHDGraphicsInGame.swf", L"skinHDGraphicsInGame.swf");
m_iggyLibraries[eLibrary_GraphicsTooltips] = loadSkin(L"skinHDGraphicsTooltips.swf", L"skinHDGraphicsTooltips.swf");
m_iggyLibraries[eLibrary_GraphicsLabels] = loadSkin(L"skinHDGraphicsLabels.swf", L"skinHDGraphicsLabels.swf");
m_iggyLibraries[eLibrary_Labels] = loadSkin(L"skinHDLabels.swf", L"skinHDLabels.swf");
m_iggyLibraries[eLibrary_InGame] = loadSkin(L"skinHDInGame.swf", L"skinHDInGame.swf");
m_iggyLibraries[eLibrary_HUD] = loadSkin(L"skinHDHud.swf", L"skinHDHud.swf");
m_iggyLibraries[eLibrary_Tooltips] = loadSkin(L"skinHDTooltips.swf", L"skinHDTooltips.swf");
m_iggyLibraries[eLibrary_Default] = loadSkin(L"skinHD.swf", L"skinHD.swf");
#endif
}
IggyLibrary UIController::loadSkin(const wstring &skinPath, const wstring &skinName)
@ -1810,14 +1828,24 @@ GDrawTexture * RADLINK UIController::TextureSubstitutionCreateCallback ( void *
// 4J Stu - All our flash controls that allow replacing textures use a special 64x64 symbol
// Force this size here so that our images don't get scaled wildly
#if (defined __ORBIS__ || defined _DURANGO || defined _WINDOWS64 )
#if (defined __ORBIS__ || defined _DURANGO )
*width = 96;
*height = 96;
#else
*width = 64;
*height = 64;
#endif
#if defined _WINDOWS64
// Only set the size to 96x96 for 1080p on Windows
UIScene *scene = uiController->GetTopScene(0);
if (scene->getSceneResolution() == UIScene::eSceneResolution_1080)
{
*width = 96;
*height = 96;
}
#endif
*destroy_callback_data = (void *)id;
app.DebugPrintf("Found substitution texture %ls (%d) - %dx%d\n", static_cast<wchar_t *>(texture_name), id, image.getWidth(), image.getHeight());

View file

@ -101,9 +101,8 @@ private:
eLibrary_Tooltips,
eLibrary_Default,
#if ( defined(_WINDOWS64) )
// 4J Stu - Load the 720/480 skins so that we have something to fallback on during development
#ifndef _FINAL_BUILD
#if defined(_WINDOWS64)
// Non-HD skin libraries needed by 720p/480p scene SWFs.
eLibraryFallback_Platform,
eLibraryFallback_GraphicsDefault,
eLibraryFallback_GraphicsHUD,
@ -115,7 +114,6 @@ private:
eLibraryFallback_HUD,
eLibraryFallback_Tooltips,
eLibraryFallback_Default,
#endif
#endif
eLibrary_Count,

View file

@ -172,15 +172,22 @@ void UIScene::updateSafeZone()
{
case C4JRender::VIEWPORT_TYPE_SPLIT_TOP:
safeTop = getSafeZoneHalfHeight();
safeLeft = getSafeZoneHalfWidth();
break;
case C4JRender::VIEWPORT_TYPE_SPLIT_BOTTOM:
safeBottom = getSafeZoneHalfHeight();
// safeTop mirrors SPLIT_TOP for visual symmetry. safeBottom omitted.
safeTop = getSafeZoneHalfHeight();
safeLeft = getSafeZoneHalfWidth();
break;
case C4JRender::VIEWPORT_TYPE_SPLIT_LEFT:
safeTop = getSafeZoneHalfHeight();
safeLeft = getSafeZoneHalfWidth();
break;
case C4JRender::VIEWPORT_TYPE_SPLIT_RIGHT:
safeRight = getSafeZoneHalfWidth();
safeTop = getSafeZoneHalfHeight();
break;
case C4JRender::VIEWPORT_TYPE_QUADRANT_TOP_LEFT:
safeTop = getSafeZoneHalfHeight();
@ -188,22 +195,22 @@ void UIScene::updateSafeZone()
break;
case C4JRender::VIEWPORT_TYPE_QUADRANT_TOP_RIGHT:
safeTop = getSafeZoneHalfHeight();
safeRight = getSafeZoneHalfWidth();
break;
case C4JRender::VIEWPORT_TYPE_QUADRANT_BOTTOM_LEFT:
safeBottom = getSafeZoneHalfHeight();
safeTop = getSafeZoneHalfHeight();
safeLeft = getSafeZoneHalfWidth();
break;
case C4JRender::VIEWPORT_TYPE_QUADRANT_BOTTOM_RIGHT:
safeBottom = getSafeZoneHalfHeight();
safeRight = getSafeZoneHalfWidth();
safeTop = getSafeZoneHalfHeight();
break;
case C4JRender::VIEWPORT_TYPE_FULLSCREEN:
default:
safeTop = getSafeZoneHalfHeight();
safeBottom = getSafeZoneHalfHeight();
safeLeft = getSafeZoneHalfWidth();
safeRight = getSafeZoneHalfWidth();
break;
}
setSafeZone(safeTop, safeBottom, safeLeft, safeRight);
@ -286,8 +293,16 @@ void UIScene::loadMovie()
moviePath.append(L"Vita.swf");
m_loadedResolution = eSceneResolution_Vita;
#elif defined _WINDOWS64
moviePath.append(L"1080.swf");
m_loadedResolution = eSceneResolution_1080;
if(ui.getScreenHeight() > 720.0f)
{
moviePath.append(L"1080.swf");
m_loadedResolution = eSceneResolution_1080;
}
else
{
moviePath.append(L"720.swf");
m_loadedResolution = eSceneResolution_720;
}
#else
moviePath.append(L"1080.swf");
m_loadedResolution = eSceneResolution_1080;
@ -570,9 +585,12 @@ bool UIScene::handleMouseClick(F32 x, F32 y)
if (bestCtrl->getControlType() == UIControl::eCheckBox)
{
UIControl_CheckBox *cb = static_cast<UIControl_CheckBox*>(bestCtrl);
bool newState = !cb->IsChecked();
cb->setChecked(newState);
handleCheckboxToggled((F64)bestId, newState);
if (cb->IsEnabled())
{
bool newState = !cb->IsChecked();
cb->setChecked(newState);
handleCheckboxToggled((F64)bestId, newState);
}
}
else
{

View file

@ -278,7 +278,7 @@ void UIScene_FullscreenProgress::handleInput(int iPad, int key, bool repeat, boo
#ifdef __ORBIS__
case ACTION_MENU_TOUCHPAD_PRESS:
#endif
if(pressed)
if(pressed && m_threadCompleted)
{
sendInputToMovie(key, repeat, pressed, released);
}
@ -292,6 +292,7 @@ void UIScene_FullscreenProgress::handleInput(int iPad, int key, bool repeat, boo
}
break;
}
handled = true;
}
}

File diff suppressed because it is too large Load diff

View file

@ -176,5 +176,5 @@ protected:
#endif
private:
void repositionHud(S32 tileWidth, S32 tileHeight, F32 scale);
void repositionHud(S32 tileWidth, S32 tileHeight, F32 scale, bool needsYTile);
};

View file

@ -150,8 +150,6 @@ void UIScene_InGameInfoMenu::updateTooltips()
void UIScene_InGameInfoMenu::handleDestroy()
{
g_NetworkManager.UnRegisterPlayerChangedCallback(m_iPad, &UIScene_InGameInfoMenu::OnPlayerChanged, this);
m_parentLayer->removeComponent(eUIComponent_MenuBackground);
}
void UIScene_InGameInfoMenu::handleGainFocus(bool navBack)

View file

@ -1,3 +0,0 @@
<Contributor ComponentSource="c:\users\paddy\work\tfs\minecraft\dev-cu2-xb1-ps4-x360\commonmedia.vcxproj">
<Configuration Name="Debug" />
</Contributor>

View file

@ -1,113 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Package xmlns="http://schemas.microsoft.com/appx/2010/manifest" xmlns:mx="http://schemas.microsoft.com/appx/2013/xbox/manifest" IgnorableNamespaces="mx">
<Identity Name="Minecraft" Publisher="CN=Publisher" Version="1.0.406.0" ProcessorArchitecture="x64" />
<Properties>
<DisplayName>Minecraft: Xbox One Edition</DisplayName>
<PublisherDisplayName>Microsoft Studios</PublisherDisplayName>
<Logo>StoreLogo.png</Logo>
<Description>Minecraft</Description>
</Properties>
<Prerequisites>
<OSMinVersion>6.2</OSMinVersion>
<OSMaxVersionTested>6.2</OSMaxVersionTested>
<mx:ApplicationEnvironment>title</mx:ApplicationEnvironment>
<mx:OSName>era</mx:OSName>
</Prerequisites>
<Resources>
<Resource Language="en-us" />
<Resource Language="en-gb" />
<Resource Language="de-de" />
<Resource Language="es-es" />
<Resource Language="es-mx" />
<Resource Language="fr-fr" />
<Resource Language="it-it" />
<Resource Language="pt-br" />
<Resource Language="pt-pt" />
<Resource Language="ja-jp" />
<Resource Language="ko-kr" />
<Resource Language="zh-tw" />
<Resource Language="zh-hk" />
</Resources>
<Applications>
<Application Id="App" Executable="Minecraft.Client.exe" EntryPoint="Minecraft.App">
<VisualElements DisplayName="Minecraft: Xbox One Edition" Logo="StoreLogo.png" SmallLogo="SmallLogo.png" Description="Minecraft" ForegroundText="light" BackgroundColor="#464646">
<SplashScreen Image="SplashScreen.png" />
</VisualElements>
<Extensions>
<mx:Extension Category="xbox.live">
<mx:XboxLive TitleId="149E11AE" PrimaryServiceConfigId="05c20100-6e60-45d5-878a-4903149e11ae">
<mx:ConnectedStorage MaxMegabytesPerUser="1024" />
</mx:XboxLive>
</mx:Extension>
<mx:Extension Category="windows.xbox.networking">
<mx:XboxNetworkingManifest>
<mx:SocketDescriptions>
<mx:SocketDescription Name="MultiplayerSocketUdp" SecureIpProtocol="Udp" BoundPort="8700">
<mx:AllowedUsages>
<mx:SecureDeviceSocketUsage Type="Initiate" />
<mx:SecureDeviceSocketUsage Type="Accept" />
<mx:SecureDeviceSocketUsage Type="SendGameData" />
<mx:SecureDeviceSocketUsage Type="ReceiveGameData" />
<mx:SecureDeviceSocketUsage Type="SendChat" />
<mx:SecureDeviceSocketUsage Type="ReceiveChat" />
</mx:AllowedUsages>
</mx:SocketDescription>
</mx:SocketDescriptions>
<mx:SecureDeviceAssociationTemplates>
<mx:SecureDeviceAssociationTemplate Name="MultiplayerUdp" InitiatorSocketDescription="MultiplayerSocketUdp" AcceptorSocketDescription="MultiplayerSocketUdp" MultiplayerSessionRequirement="Required">
<mx:AllowedUsages>
<mx:SecureDeviceAssociationUsage Type="Default" />
</mx:AllowedUsages>
</mx:SecureDeviceAssociationTemplate>
</mx:SecureDeviceAssociationTemplates>
</mx:XboxNetworkingManifest>
</mx:Extension>
</Extensions>
</Application>
</Applications>
<Extensions>
<mx:PackageExtension Category="xbox.store">
<mx:XboxStore>
<mx:RelatedProducts>
<mx:RelatedProduct Id="582e7bcc-11bc-4702-ab1b-b31566f8e327" />
</mx:RelatedProducts>
</mx:XboxStore>
</mx:PackageExtension>
<Extension Category="windows.activatableClass.inProcessServer">
<InProcessServer>
<Path>windows.xbox.networking.realtimesession.dll</Path>
<ActivatableClass ActivatableClassId="Windows.Xbox.Networking.RealtimeSession.Session" ThreadingModel="both" />
</InProcessServer>
</Extension>
<Extension Category="windows.activatableClass.inProcessServer">
<InProcessServer>
<Path>Microsoft.Xbox.GameChat.dll</Path>
<ActivatableClass ActivatableClassId="Microsoft.Xbox.GameChat.ChatManager" ThreadingModel="both" />
</InProcessServer>
</Extension>
<Extension Category="windows.activatableClass.inProcessServer">
<InProcessServer>
<Path>Microsoft.Xbox.Services.dll</Path>
<ActivatableClass ActivatableClassId="Microsoft.Xbox.Services.XboxLiveContext" ThreadingModel="both" />
<ActivatableClass ActivatableClassId="Microsoft.Xbox.Services.Configuration" ThreadingModel="both" />
<ActivatableClass ActivatableClassId="Microsoft.Xbox.Services.Multiplayer.MultiplayerQualityOfServiceMeasurements" ThreadingModel="both" />
<ActivatableClass ActivatableClassId="Microsoft.Xbox.Services.Multiplayer.MultiplayerSessionReference" ThreadingModel="both" />
<ActivatableClass ActivatableClassId="Microsoft.Xbox.Services.Multiplayer.MultiplayerSessionCapabilities" ThreadingModel="both" />
<ActivatableClass ActivatableClassId="Microsoft.Xbox.Services.Multiplayer.MultiplayerSession" ThreadingModel="both" />
<ActivatableClass ActivatableClassId="Microsoft.Xbox.Services.Matchmaking.CreateMatchTicketResponse" ThreadingModel="both" />
<ActivatableClass ActivatableClassId="Microsoft.Xbox.Services.Matchmaking.HopperStatisticsResponse" ThreadingModel="both" />
<ActivatableClass ActivatableClassId="Microsoft.Xbox.Services.Matchmaking.MatchTicketDetailsResponse" ThreadingModel="both" />
<ActivatableClass ActivatableClassId="Microsoft.Xbox.Services.Presence.PresenceData" ThreadingModel="both" />
<ActivatableClass ActivatableClassId="Microsoft.Xbox.Services.TitleStorage.TitleStorageBlobMetadata" ThreadingModel="both" />
<ActivatableClass ActivatableClassId="Microsoft.Xbox.Services.UserStatistics.RequestedStatistics" ThreadingModel="both" />
<ActivatableClass ActivatableClassId="Microsoft.Xbox.Services.Privacy.PermissionIdConstants" ThreadingModel="both" />
<ActivatableClass ActivatableClassId="Microsoft.Xbox.Services.Social.SocialGroupConstants" ThreadingModel="both" />
</InProcessServer>
</Extension>
</Extensions>
<Capabilities>
<Capability Name="internetClient" />
<mx:Capability Name="kinectAudio" />
<mx:Capability Name="kinectGamechat" />
</Capabilities>
</Package>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

View file

@ -954,91 +954,70 @@ float GameRenderer::ComputeGammaFromSlider(float slider0to100)
void GameRenderer::CachePlayerGammas()
{
for (int j = 0; j < XUSER_MAX_COUNT && j < NUM_LIGHT_TEXTURES; ++j)
{
std::shared_ptr<MultiplayerLocalPlayer> player = Minecraft::GetInstance()->localplayers[j];
if (!player)
{
m_cachedGammaPerPlayer[j] = 1.0f;
continue;
}
const float slider = app.GetGameSettings(ProfileManager.GetPrimaryPad(), eGameSetting_Gamma);
const float gamma = ComputeGammaFromSlider(slider);
const float slider = app.GetGameSettings(j, eGameSetting_Gamma); // 0..100
m_cachedGammaPerPlayer[j] = ComputeGammaFromSlider(slider);
}
for (int j = 0; j < XUSER_MAX_COUNT && j < NUM_LIGHT_TEXTURES; ++j)
m_cachedGammaPerPlayer[j] = gamma;
}
bool GameRenderer::ComputeViewportForPlayer(int j, D3D11_VIEWPORT &outViewport) const
{
// Use the actual backbuffer dimensions so viewports adapt to window resize.
extern int g_rScreenWidth;
extern int g_rScreenHeight;
int active = 0;
int indexMap[NUM_LIGHT_TEXTURES] = {-1, -1, -1, -1};
for (int i = 0; i < XUSER_MAX_COUNT && i < NUM_LIGHT_TEXTURES; ++i)
{
if (Minecraft::GetInstance()->localplayers[i])
indexMap[active++] = i;
}
if (active <= 1)
{
outViewport.TopLeftX = 0.0f;
outViewport.TopLeftY = 0.0f;
outViewport.Width = static_cast<FLOAT>(g_rScreenWidth);
outViewport.Height = static_cast<FLOAT>(g_rScreenHeight);
outViewport.MinDepth = 0.0f;
outViewport.MaxDepth = 1.0f;
return true;
}
int k = -1;
for (int ord = 0; ord < active; ++ord)
if (indexMap[ord] == j)
{
k = ord;
break;
}
if (k < 0)
std::shared_ptr<MultiplayerLocalPlayer> player = Minecraft::GetInstance()->localplayers[j];
if (!player)
return false;
const float width = static_cast<float>(g_rScreenWidth);
const float height = static_cast<float>(g_rScreenHeight);
const float w = static_cast<float>(g_rScreenWidth);
const float h = static_cast<float>(g_rScreenHeight);
const float halfW = w * 0.5f;
const float halfH = h * 0.5f;
if (active == 2)
outViewport.MinDepth = 0.0f;
outViewport.MaxDepth = 1.0f;
switch (static_cast<C4JRender::eViewportType>(player->m_iScreenSection))
{
const float halfH = height * 0.5f;
outViewport.TopLeftX = 0.0f;
outViewport.Width = width;
outViewport.MinDepth = 0.0f;
outViewport.MaxDepth = 1.0f;
if (k == 0)
{
outViewport.TopLeftY = 0.0f;
outViewport.Height = halfH;
}
else
{
outViewport.TopLeftY = halfH;
outViewport.Height = halfH;
}
return true;
}
else
{
const float halfW = width * 0.5f;
const float halfH = height * 0.5f;
const int row = (k >= 2) ? 1 : 0;
const int col = (k % 2);
outViewport.TopLeftX = col ? halfW : 0.0f;
outViewport.TopLeftY = row ? halfH : 0.0f;
outViewport.Width = halfW;
outViewport.Height = halfH;
outViewport.MinDepth = 0.0f;
outViewport.MaxDepth = 1.0f;
return true;
case C4JRender::VIEWPORT_TYPE_SPLIT_TOP:
outViewport.TopLeftX = 0; outViewport.TopLeftY = 0;
outViewport.Width = w; outViewport.Height = halfH;
break;
case C4JRender::VIEWPORT_TYPE_SPLIT_BOTTOM:
outViewport.TopLeftX = 0; outViewport.TopLeftY = halfH;
outViewport.Width = w; outViewport.Height = halfH;
break;
case C4JRender::VIEWPORT_TYPE_SPLIT_LEFT:
outViewport.TopLeftX = 0; outViewport.TopLeftY = 0;
outViewport.Width = halfW; outViewport.Height = h;
break;
case C4JRender::VIEWPORT_TYPE_SPLIT_RIGHT:
outViewport.TopLeftX = halfW; outViewport.TopLeftY = 0;
outViewport.Width = halfW; outViewport.Height = h;
break;
case C4JRender::VIEWPORT_TYPE_QUADRANT_TOP_LEFT:
outViewport.TopLeftX = 0; outViewport.TopLeftY = 0;
outViewport.Width = halfW; outViewport.Height = halfH;
break;
case C4JRender::VIEWPORT_TYPE_QUADRANT_TOP_RIGHT:
outViewport.TopLeftX = halfW; outViewport.TopLeftY = 0;
outViewport.Width = halfW; outViewport.Height = halfH;
break;
case C4JRender::VIEWPORT_TYPE_QUADRANT_BOTTOM_LEFT:
outViewport.TopLeftX = 0; outViewport.TopLeftY = halfH;
outViewport.Width = halfW; outViewport.Height = halfH;
break;
case C4JRender::VIEWPORT_TYPE_QUADRANT_BOTTOM_RIGHT:
outViewport.TopLeftX = halfW; outViewport.TopLeftY = halfH;
outViewport.Width = halfW; outViewport.Height = halfH;
break;
default:
outViewport.TopLeftX = 0; outViewport.TopLeftY = 0;
outViewport.Width = w; outViewport.Height = h;
break;
}
return true;
}
uint32_t GameRenderer::BuildPlayerViewports(D3D11_VIEWPORT *outViewports, float *outGammas, UINT maxCount) const

View file

@ -443,7 +443,8 @@ void Gui::render(float a, bool mouseFree, int xMouse, int yMouse)
double maxHealth = minecraft->localplayers[iPad]->getAttribute(SharedMonsterAttributes.MAX_HEALTH);
double totalAbsorption = minecraft->localplayers[iPad]->getAbsorptionAmount();
int numHealthRows = Mth.ceil((maxHealth + totalAbsorption) / 2 / (float) NUM_HEARTS_PER_ROW);
const double healthHalves = (maxHealth + totalAbsorption) / 2.0;
int numHealthRows = Mth.ceil(healthHalves / (float) NUM_HEARTS_PER_ROW);
int healthRowHeight = Math.max(10 - (numHealthRows - 2), 3);
int yLine2 = yLine1 - (numHealthRows - 1) * healthRowHeight - 10;
absorption = totalAbsorption;
@ -469,7 +470,7 @@ void Gui::render(float a, bool mouseFree, int xMouse, int yMouse)
}
//minecraft.profiler.popPush("health");
for (int i = Mth.ceil((maxHealth + totalAbsorption) / 2) - 1; i >= 0; i--)
for (int i = (int)Mth.ceil(healthHalves) - 1; i >= 0; i--)
{
int healthTexBaseX = 16;
if (minecraft.player.hasEffect(MobEffect.poison))
@ -607,8 +608,11 @@ void Gui::render(float a, bool mouseFree, int xMouse, int yMouse)
// render air bubbles
if (minecraft->player->isUnderLiquid(Material::water))
{
int count = (int) ceil((minecraft->player->getAirSupply() - 2) * 10.0f / Player::TOTAL_AIR_SUPPLY);
int extra = (int) ceil((minecraft->player->getAirSupply()) * 10.0f / Player::TOTAL_AIR_SUPPLY) - count;
const int airSupply = minecraft->player->getAirSupply();
const float airScale = 10.0f / Player::TOTAL_AIR_SUPPLY;
const float airSupplyScaled = airSupply * airScale;
int count = (int) ceil((airSupply - 2) * airScale);
int extra = (int) ceil(airSupplyScaled) - count;
for (int i = 0; i < count + extra; i++)
{
// Air bubbles
@ -725,7 +729,8 @@ void Gui::render(float a, bool mouseFree, int xMouse, int yMouse)
Lighting::turnOn();
glRotatef(-45 - 90, 0, 1, 0);
glRotatef(-(float) atan(yd / 40.0f ) * 20, 1, 0, 0);
const float xRotAngle = -(float) atan(yd / 40.0f) * 20;
glRotatef(xRotAngle, 1, 0, 0);
float bodyRot = (minecraft->player->yBodyRotO + (minecraft->player->yBodyRot - minecraft->player->yBodyRotO));
// Fixed rotation angle of degrees, adjusted by bodyRot to negate the rotation that occurs in the renderer
// bodyRot in the rotation below is a simplification of "180 - (180 - bodyRot)" where the first 180 is EntityRenderDispatcher::instance->playerRotY that we set below
@ -736,7 +741,7 @@ void Gui::render(float a, bool mouseFree, int xMouse, int yMouse)
// Set head rotation to body rotation to make head static
minecraft->player->yRot = bodyRot;
minecraft->player->yRotO = minecraft->player->yRot;
minecraft->player->xRot = -(float) atan(yd / 40.0f) * 20;
minecraft->player->xRot = xRotAngle;
minecraft->player->onFire = 0;
minecraft->player->setSharedFlag(Entity::FLAG_ONFIRE, false);
@ -849,201 +854,6 @@ void Gui::render(float a, bool mouseFree, int xMouse, int yMouse)
// font.draw(str, x + 1, y, 0xffffff);
// }
#ifndef _FINAL_BUILD
MemSect(31);
if (minecraft->options->renderDebug && minecraft->player != nullptr && minecraft->level != nullptr)
{
const int debugLeft = 1;
const int debugTop = 1;
const float maxContentWidth = 1200.f;
const float maxContentHeight = 420.f;
float scale = static_cast<float>(screenWidth - debugLeft - 8) / maxContentWidth;
float scaleV = static_cast<float>(screenHeight - debugTop - 80) / maxContentHeight;
if (scaleV < scale) scale = scaleV;
if (scale > 1.f) scale = 1.f;
if (scale < 0.5f) scale = 0.5f;
glPushMatrix();
glTranslatef(static_cast<float>(debugLeft), static_cast<float>(debugTop), 0.f);
glScalef(scale, scale, 1.f);
glTranslatef(static_cast<float>(-debugLeft), static_cast<float>(-debugTop), 0.f);
vector<wstring> lines;
lines.push_back(ClientConstants::VERSION_STRING);
lines.push_back(minecraft->fpsString);
lines.push_back(L"E: " + std::to_wstring(minecraft->level->getAllEntities().size())); // Could maybe use entity::shouldRender to work out how many are rendered but thats like expensive
// TODO Add server information with packet counts - once multiplayer is more stable
int renderDistance = app.GetGameSettings(iPad, eGameSetting_RenderDistance);
// Calculate the chunk sections using 16 * (2n + 1)^2
lines.push_back(L"C: " + std::to_wstring(16 * (2 * renderDistance + 1) * (2 * renderDistance + 1)) + L" D: " + std::to_wstring(renderDistance));
lines.push_back(minecraft->gatherStats4()); // Chunk Cache
// Dimension
wstring dimension = L"unknown";
switch (minecraft->player->dimension)
{
case -1:
dimension = L"minecraft:the_nether";
break;
case 0:
dimension = L"minecraft:overworld";
break;
case 1:
dimension = L"minecraft:the_end";
break;
}
lines.push_back(dimension);
lines.push_back(L""); // Spacer
// Players block pos
int xBlockPos = Mth::floor(minecraft->player->x);
int yBlockPos = Mth::floor(minecraft->player->y);
int zBlockPos = Mth::floor(minecraft->player->z);
// Chunk player is in
int xChunkPos = xBlockPos >> 4;
int yChunkPos = yBlockPos >> 4;
int zChunkPos = zBlockPos >> 4;
// Players offset within the chunk
int xChunkOffset = xBlockPos & 15;
int yChunkOffset = yBlockPos & 15;
int zChunkOffset = zBlockPos & 15;
// Format the position like java with limited decumal places
WCHAR posString[44]; // Allows upto 7 digit positions (+-9_999_999)
swprintf(posString, 44, L"%.3f / %.5f / %.3f", minecraft->player->x, minecraft->player->y, minecraft->player->z);
lines.push_back(L"XYZ: " + std::wstring(posString));
lines.push_back(L"Block: " + std::to_wstring(static_cast<int>(xBlockPos)) + L" " + std::to_wstring(static_cast<int>(yBlockPos)) + L" " + std::to_wstring(static_cast<int>(zBlockPos)));
lines.push_back(L"Chunk: " + std::to_wstring(xChunkOffset) + L" " + std::to_wstring(yChunkOffset) + L" " + std::to_wstring(zChunkOffset) + L" in " + std::to_wstring(xChunkPos) + L" " + std::to_wstring(yChunkPos) + L" " + std::to_wstring(zChunkPos));
// Wrap the yRot to 360 then adjust to (-180 to 180) range to match java
float yRotDisplay = fmod(minecraft->player->yRot, 360.0f);
if (yRotDisplay > 180.0f)
{
yRotDisplay -= 360.0f;
}
if (yRotDisplay < -180.0f)
{
yRotDisplay += 360.0f;
}
// Generate the angle string in the format "yRot / xRot" with one decimal place, similar to java edition
WCHAR angleString[16];
swprintf(angleString, 16, L"%.1f / %.1f", yRotDisplay, minecraft->player->xRot);
// Work out the named direction
int direction = Mth::floor(minecraft->player->yRot * 4.0f / 360.0f + 0.5) & 0x3;
wstring cardinalDirection;
switch (direction)
{
case 0:
cardinalDirection = L"south";
break;
case 1:
cardinalDirection = L"west";
break;
case 2:
cardinalDirection = L"north";
break;
case 3:
cardinalDirection = L"east";
break;
}
lines.push_back(L"Facing: " + cardinalDirection + L" (" + angleString + L")");
// We have to limit y to 256 as we don't get any information past that
if (minecraft->level != NULL && minecraft->level->hasChunkAt(xBlockPos, fmod(yBlockPos, 256), zBlockPos))
{
LevelChunk *chunkAt = minecraft->level->getChunkAt(xBlockPos, zBlockPos);
if (chunkAt != NULL)
{
int skyLight = chunkAt->getBrightness(LightLayer::Sky, xChunkOffset, yChunkOffset, zChunkOffset);
int blockLight = chunkAt->getBrightness(LightLayer::Block, xChunkOffset, yChunkOffset, zChunkOffset);
int maxLight = fmax(skyLight, blockLight);
lines.push_back(L"Light: " + std::to_wstring(maxLight) + L" (" + std::to_wstring(skyLight) + L" sky, " + std::to_wstring(blockLight) + L" block)");
lines.push_back(L"CH S: " + std::to_wstring(chunkAt->getHeightmap(xChunkOffset, zChunkOffset)));
Biome *biome = chunkAt->getBiome(xChunkOffset, zChunkOffset, minecraft->level->getBiomeSource());
lines.push_back(L"Biome: " + biome->m_name + L" (" + std::to_wstring(biome->id) + L")");
lines.push_back(L"Difficulty: " + std::to_wstring(minecraft->level->difficulty) + L" (Day " + std::to_wstring(minecraft->level->getGameTime() / Level::TICKS_PER_DAY) + L")");
}
}
// This is all LCE only stuff, it was never on java
lines.push_back(L""); // Spacer
lines.push_back(L"Seed: " + std::to_wstring(minecraft->level->getLevelData()->getSeed()));
lines.push_back(minecraft->gatherStats1()); // Time to autosave
lines.push_back(minecraft->gatherStats2()); // Empty currently - CPlatformNetworkManagerStub::GatherStats()
lines.push_back(minecraft->gatherStats3()); // RTT
#ifdef _DEBUG // Only show terrain features in debug builds not release
// TERRAIN FEATURES
if (minecraft->level->dimension->id == 0)
{
wstring wfeature[eTerrainFeature_Count];
wfeature[eTerrainFeature_Stronghold] = L"Stronghold: ";
wfeature[eTerrainFeature_Mineshaft] = L"Mineshaft: ";
wfeature[eTerrainFeature_Village] = L"Village: ";
wfeature[eTerrainFeature_Ravine] = L"Ravine: ";
float maxW = static_cast<float>(screenWidth - debugLeft - 8) / scale;
float maxWForContent = maxW - static_cast<float>(font->width(L"..."));
bool truncated[eTerrainFeature_Count] = {};
for (size_t i = 0; i < app.m_vTerrainFeatures.size(); i++)
{
FEATURE_DATA *pFeatureData = app.m_vTerrainFeatures[i];
int type = pFeatureData->eTerrainFeature;
if (type < eTerrainFeature_Stronghold || type > eTerrainFeature_Ravine)
{
continue;
}
if (truncated[type])
{
continue;
}
wstring itemInfo = L"[" + std::to_wstring(pFeatureData->x * 16) + L", " + std::to_wstring(pFeatureData->z * 16) + L"] ";
if (font->width(wfeature[type] + itemInfo) <= maxWForContent)
{
wfeature[type] += itemInfo;
}
else
{
wfeature[type] += L"...";
truncated[type] = true;
}
}
lines.push_back(L""); // Add a spacer line
for (int i = eTerrainFeature_Stronghold; i <= static_cast<int>(eTerrainFeature_Ravine); i++)
{
lines.push_back(wfeature[i]);
}
lines.push_back(L"");
}
#endif
// Loop through the lines and draw them all on screen
int yPos = debugTop;
for (const auto &line : lines)
{
drawString(font, line, debugLeft, yPos, 0xffffff);
yPos += 10;
}
glPopMatrix();
}
MemSect(0);
#endif
lastTickA = a;
// 4J Stu - This is now displayed in a xui scene
#if 0
@ -1197,6 +1007,190 @@ void Gui::render(float a, bool mouseFree, int xMouse, int yMouse)
glPopMatrix();
}
#ifndef _FINAL_BUILD
MemSect(31);
if (true)
{
// Real window dimensions updated on every WM_SIZE — always current
extern int g_rScreenWidth;
extern int g_rScreenHeight;
// Set up a fresh projection using physical pixel coordinates so the debug
// text is never distorted regardless of aspect ratio, splitscreen layout,
// or menu state. 1 coordinate unit = 1 physical pixel.
// Compute the actual viewport dimensions for this player's screen section.
// glOrtho must match the viewport exactly for 1 unit = 1 physical pixel.
int vpW = g_rScreenWidth;
int vpH = g_rScreenHeight;
switch (minecraft->player->m_iScreenSection)
{
case C4JRender::VIEWPORT_TYPE_SPLIT_TOP:
case C4JRender::VIEWPORT_TYPE_SPLIT_BOTTOM:
vpH /= 2;
break;
case C4JRender::VIEWPORT_TYPE_SPLIT_LEFT:
case C4JRender::VIEWPORT_TYPE_SPLIT_RIGHT:
vpW /= 2;
break;
case C4JRender::VIEWPORT_TYPE_QUADRANT_TOP_LEFT:
case C4JRender::VIEWPORT_TYPE_QUADRANT_TOP_RIGHT:
case C4JRender::VIEWPORT_TYPE_QUADRANT_BOTTOM_LEFT:
case C4JRender::VIEWPORT_TYPE_QUADRANT_BOTTOM_RIGHT:
vpW /= 2;
vpH /= 2;
break;
default: // VIEWPORT_TYPE_FULLSCREEN
break;
}
glMatrixMode(GL_PROJECTION);
glPushMatrix();
glLoadIdentity();
glOrtho(0, vpW, vpH, 0, 1000, 3000);
glMatrixMode(GL_MODELVIEW);
glPushMatrix();
glLoadIdentity();
glTranslatef(0, 0, -2000);
// Font was designed for guiScale px/unit; scale up so characters appear
// at the same physical size as the rest of the HUD at 0.5x.
const float fontScale = static_cast<float>(guiScale) * 1.0f;
const int debugLeft = 1;
const int debugTop = 1;
glTranslatef(static_cast<float>(debugLeft), static_cast<float>(debugTop), 0.f);
glScalef(fontScale, fontScale, 1.f);
glTranslatef(static_cast<float>(-debugLeft), static_cast<float>(-debugTop), 0.f);
vector<wstring> lines;
// Only show version/branch for player 0 to avoid cluttering each splitscreen viewport
if (iPad == 0)
{
lines.push_back(ClientConstants::VERSION_STRING);
lines.push_back(ClientConstants::BRANCH_STRING);
}
if (minecraft->options->renderDebug && minecraft->player != nullptr && minecraft->level != nullptr)
{
lines.push_back(minecraft->fpsString);
lines.push_back(L"E: " + std::to_wstring(minecraft->level->getAllEntities().size()));
int renderDistance = app.GetGameSettings(iPad, eGameSetting_RenderDistance);
lines.push_back(L"C: " + std::to_wstring(16 * (2 * renderDistance + 1) * (2 * renderDistance + 1)) + L" D: " + std::to_wstring(renderDistance));
lines.push_back(minecraft->gatherStats4());
wstring dimension = L"unknown";
switch (minecraft->player->dimension)
{
case -1: dimension = L"minecraft:the_nether"; break;
case 0: dimension = L"minecraft:overworld"; break;
case 1: dimension = L"minecraft:the_end"; break;
}
lines.push_back(dimension);
lines.push_back(L"");
int xBlockPos = Mth::floor(minecraft->player->x);
int yBlockPos = Mth::floor(minecraft->player->y);
int zBlockPos = Mth::floor(minecraft->player->z);
int xChunkPos = xBlockPos >> 4;
int yChunkPos = yBlockPos >> 4;
int zChunkPos = zBlockPos >> 4;
int xChunkOffset = xBlockPos & 15;
int yChunkOffset = yBlockPos & 15;
int zChunkOffset = zBlockPos & 15;
WCHAR posString[44];
swprintf(posString, 44, L"%.3f / %.5f / %.3f", minecraft->player->x, minecraft->player->y, minecraft->player->z);
lines.push_back(L"XYZ: " + std::wstring(posString));
lines.push_back(L"Block: " + std::to_wstring(xBlockPos) + L" " + std::to_wstring(yBlockPos) + L" " + std::to_wstring(zBlockPos));
lines.push_back(L"Chunk: " + std::to_wstring(xChunkOffset) + L" " + std::to_wstring(yChunkOffset) + L" " + std::to_wstring(zChunkOffset) + L" in " + std::to_wstring(xChunkPos) + L" " + std::to_wstring(yChunkPos) + L" " + std::to_wstring(zChunkPos));
float yRotDisplay = fmod(minecraft->player->yRot, 360.0f);
if (yRotDisplay > 180.0f) yRotDisplay -= 360.0f;
if (yRotDisplay < -180.0f) yRotDisplay += 360.0f;
WCHAR angleString[16];
swprintf(angleString, 16, L"%.1f / %.1f", yRotDisplay, minecraft->player->xRot);
int direction = Mth::floor(minecraft->player->yRot * 4.0f / 360.0f + 0.5) & 0x3;
const wchar_t* cardinals[] = { L"south", L"west", L"north", L"east" };
lines.push_back(L"Facing: " + std::wstring(cardinals[direction]) + L" (" + angleString + L")");
if (minecraft->level != NULL && minecraft->level->hasChunkAt(xBlockPos, fmod(yBlockPos, 256), zBlockPos))
{
LevelChunk *chunkAt = minecraft->level->getChunkAt(xBlockPos, zBlockPos);
if (chunkAt != NULL)
{
int skyLight = chunkAt->getBrightness(LightLayer::Sky, xChunkOffset, yChunkOffset, zChunkOffset);
int blockLight = chunkAt->getBrightness(LightLayer::Block, xChunkOffset, yChunkOffset, zChunkOffset);
int maxLight = fmax(skyLight, blockLight);
lines.push_back(L"Light: " + std::to_wstring(maxLight) + L" (" + std::to_wstring(skyLight) + L" sky, " + std::to_wstring(blockLight) + L" block)");
lines.push_back(L"CH S: " + std::to_wstring(chunkAt->getHeightmap(xChunkOffset, zChunkOffset)));
Biome *biome = chunkAt->getBiome(xChunkOffset, zChunkOffset, minecraft->level->getBiomeSource());
lines.push_back(L"Biome: " + biome->m_name + L" (" + std::to_wstring(biome->id) + L")");
lines.push_back(L"Difficulty: " + std::to_wstring(minecraft->level->difficulty) + L" (Day " + std::to_wstring(minecraft->level->getGameTime() / Level::TICKS_PER_DAY) + L")");
}
}
lines.push_back(L"");
lines.push_back(L"Seed: " + std::to_wstring(minecraft->level->getLevelData()->getSeed()));
lines.push_back(minecraft->gatherStats1());
lines.push_back(minecraft->gatherStats2());
lines.push_back(minecraft->gatherStats3());
}
#ifdef _DEBUG
if (minecraft->options->renderDebug && minecraft->player != nullptr && minecraft->level != nullptr && minecraft->level->dimension->id == 0)
{
wstring wfeature[eTerrainFeature_Count];
wfeature[eTerrainFeature_Stronghold] = L"Stronghold: ";
wfeature[eTerrainFeature_Mineshaft] = L"Mineshaft: ";
wfeature[eTerrainFeature_Village] = L"Village: ";
wfeature[eTerrainFeature_Ravine] = L"Ravine: ";
// maxW in font units: physical width divided by font scale
float maxW = (static_cast<float>(g_rScreenWidth) - debugLeft - 8) / fontScale;
float maxWForContent = maxW - static_cast<float>(font->width(L"..."));
bool truncated[eTerrainFeature_Count] = {};
for (size_t i = 0; i < app.m_vTerrainFeatures.size(); i++)
{
FEATURE_DATA *pFeatureData = app.m_vTerrainFeatures[i];
int type = pFeatureData->eTerrainFeature;
if (type < eTerrainFeature_Stronghold || type > eTerrainFeature_Ravine) continue;
if (truncated[type]) continue;
wstring itemInfo = L"[" + std::to_wstring(pFeatureData->x * 16) + L", " + std::to_wstring(pFeatureData->z * 16) + L"] ";
if (font->width(wfeature[type] + itemInfo) <= maxWForContent)
wfeature[type] += itemInfo;
else
{
wfeature[type] += L"...";
truncated[type] = true;
}
}
lines.push_back(L"");
for (int i = eTerrainFeature_Stronghold; i <= static_cast<int>(eTerrainFeature_Ravine); i++)
lines.push_back(wfeature[i]);
lines.push_back(L"");
}
#endif
int yPos = debugTop;
for (const auto &line : lines)
{
drawString(font, line, debugLeft, yPos, 0xffffff);
yPos += 10;
}
glMatrixMode(GL_MODELVIEW);
glPopMatrix();
glMatrixMode(GL_PROJECTION);
glPopMatrix();
glMatrixMode(GL_MODELVIEW);
}
MemSect(0);
#endif
glColor4f(1, 1, 1, 1);
glDisable(GL_BLEND);
glEnable(GL_ALPHA_TEST);

View file

@ -228,7 +228,7 @@ void ItemInHandRenderer::renderItem(shared_ptr<LivingEntity> mob, shared_ptr<Ite
// by texture lighting. This is for colourising things held in 3rd person view.
if ( (setColor) && (item != nullptr) )
{
int col = Item::items[item->id]->getColor(item,0);
int col = Item::items[item->id]->getColor(item, layer);
float red = ((col >> 16) & 0xff) / 255.0f;
float g = ((col >> 8) & 0xff) / 255.0f;
float b = ((col) & 0xff) / 255.0f;

View file

@ -1629,7 +1629,7 @@ void Minecraft::run_middle()
s_prevXButtons[i] = xCurButtons;
}
bool startJustPressed = s_startPressLatch[i] > 0;
bool tryJoin = !pause && !ui.IsIgnorePlayerJoinMenuDisplayed(ProfileManager.GetPrimaryPad()) && g_NetworkManager.SessionHasSpace() && xCurButtons != 0;
bool tryJoin = !pause && !ui.IsIgnorePlayerJoinMenuDisplayed(ProfileManager.GetPrimaryPad()) && g_NetworkManager.SessionHasSpace() && xCurButtons != 0 && g_KBMInput.IsWindowFocused();
#else
bool tryJoin = !pause && !ui.IsIgnorePlayerJoinMenuDisplayed(ProfileManager.GetPrimaryPad()) && g_NetworkManager.SessionHasSpace() && RenderManager.IsHiDef() && InputManager.ButtonPressed(i);
#endif
@ -3706,7 +3706,9 @@ void Minecraft::tick(bool bFirst, bool bUpdateTextures)
app.EnableDebugOverlay(options->renderDebug,iPad);
#else
// 4J Stu - The xbox uses a completely different way of navigating to this scene
ui.NavigateToScene(0, eUIScene_DebugOverlay, nullptr, eUILayer_Debug);
// Always open in the fullscreen group so the overlay spans the full window
// regardless of split-screen viewport configuration.
ui.NavigateToScene(0, eUIScene_DebugOverlay, nullptr, eUILayer_Debug, eUIGroup_Fullscreen);
#endif
#endif
}

View file

@ -569,6 +569,7 @@ MinecraftServer::MinecraftServer()
playerIdleTimeout = 0;
m_postUpdateThread = nullptr;
forceGameType = false;
m_spawnProtectionRadius = 0;
commandDispatcher = new ServerCommandDispatcher();
InitializeCriticalSection(&m_consoleInputCS);
@ -615,6 +616,10 @@ bool MinecraftServer::initServer(int64_t seed, NetworkGameInitData *initData, DW
logger.info("Loading properties");
#endif
settings = new Settings(new File(L"server.properties"));
// Dedicated-only: spawn-protection radius in blocks; 0 disables protection.
m_spawnProtectionRadius = GetDedicatedServerInt(settings, L"spawn-protection", 0);
if (m_spawnProtectionRadius < 0) m_spawnProtectionRadius = 0;
if (m_spawnProtectionRadius > 256) m_spawnProtectionRadius = 256;
app.SetGameHostOption(eGameHostOption_Difficulty, GetDedicatedServerInt(settings, L"difficulty", app.GetGameHostOption(eGameHostOption_Difficulty)));
app.SetGameHostOption(eGameHostOption_GameType, GetDedicatedServerInt(settings, L"gamemode", app.GetGameHostOption(eGameHostOption_GameType)));
@ -631,6 +636,7 @@ bool MinecraftServer::initServer(int64_t seed, NetworkGameInitData *initData, DW
app.DebugPrintf("ServerSettings: pvp is %s\n",(app.GetGameHostOption(eGameHostOption_PvP)>0)?"on":"off");
app.DebugPrintf("ServerSettings: fire spreads is %s\n",(app.GetGameHostOption(eGameHostOption_FireSpreads)>0)?"on":"off");
app.DebugPrintf("ServerSettings: tnt explodes is %s\n",(app.GetGameHostOption(eGameHostOption_TNT)>0)?"on":"off");
app.DebugPrintf("ServerSettings: spawn protection radius is %d\n", m_spawnProtectionRadius);
app.DebugPrintf("\n");
// TODO 4J Stu - Init a load of settings based on data passed as params
@ -937,7 +943,11 @@ bool MinecraftServer::loadLevel(LevelStorageSource *storageSource, const wstring
storage = shared_ptr<McRegionLevelStorage>(new McRegionLevelStorage(newFormatSave, File(L"."), name, true));
#else
storage = std::make_shared<McRegionLevelStorage>(new ConsoleSaveFileOriginal(L""), File(L"."), name, true);
ConsoleSaveFileOriginal* pSave = new ConsoleSaveFileOriginal(L"");
pSave->ConvertToLocalPlatform();
storage = std::make_shared<McRegionLevelStorage>(pSave, File(L"."), name, true);
#endif
}
@ -1657,7 +1667,9 @@ Level *MinecraftServer::getCommandSenderWorld()
int MinecraftServer::getSpawnProtectionRadius()
{
return 16;
// Client-host mode must never apply dedicated-server spawn protection settings.
if (!ShouldUseDedicatedServerProperties()) return 0;
return m_spawnProtectionRadius;
}
bool MinecraftServer::isUnderSpawnProtection(Level *level, int x, int y, int z, shared_ptr<Player> player)

View file

@ -44,6 +44,7 @@ typedef struct _NetworkGameInitData
LevelGenerationOptions *levelGen;
DWORD texturePackId;
bool findSeed;
bool dedicatedNoLocalHostPlayer;
unsigned int xzSize;
unsigned char hellScale;
ESavePlatform savePlatform;
@ -57,6 +58,7 @@ typedef struct _NetworkGameInitData
levelGen = nullptr;
texturePackId = 0;
findSeed = false;
dedicatedNoLocalHostPlayer = false;
xzSize = LEVEL_LEGACY_WIDTH;
hellScale = HELL_LEVEL_LEGACY_SCALE;
savePlatform = SAVE_FILE_PLATFORM_LOCAL;
@ -119,6 +121,7 @@ public:
int maxBuildHeight;
int playerIdleTimeout;
bool forceGameType;
int m_spawnProtectionRadius;
private:
// 4J Added

View file

@ -12,6 +12,7 @@ using namespace std;
class MultiPlayerLevel : public Level
{
friend class ClientConnection;
private:
static const int TICKS_BEFORE_RESET = 20 * 4;

View file

@ -14,6 +14,11 @@
#include "..\Minecraft.World\net.minecraft.world.item.h"
#include "..\Minecraft.World\SharedConstants.h"
#include "Settings.h"
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
#include "..\Minecraft.Server\ServerLogManager.h"
#include "..\Minecraft.Server\Access\Access.h"
#include "..\Minecraft.World\Socket.h"
#endif
// #ifdef __PS3__
// #include "PS3\Network\NetworkPlayerSony.h"
// #endif
@ -24,6 +29,24 @@ Random *PendingConnection::random = new Random();
bool g_bRejectDuplicateNames = true;
#endif
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
namespace
{
static unsigned char GetPendingConnectionSmallId(Connection *connection)
{
if (connection != nullptr)
{
Socket *socket = connection->getSocket();
if (socket != nullptr)
{
return socket->getSmallId();
}
}
return 0;
}
}
#endif
PendingConnection::PendingConnection(MinecraftServer *server, Socket *socket, const wstring& id)
{
// 4J - added initialisers
@ -180,44 +203,60 @@ void PendingConnection::handleLogin(shared_ptr<LoginPacket> packet)
duplicateXuid = true;
}
bool bannedXuid = false;
if (loginXuid != INVALID_XUID)
{
bannedXuid = server->getPlayers()->isXuidBanned(loginXuid);
}
if (!bannedXuid && packet->m_onlineXuid != INVALID_XUID && packet->m_onlineXuid != loginXuid)
{
bannedXuid = server->getPlayers()->isXuidBanned(packet->m_onlineXuid);
}
bool whitelistSatisfied = true;
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
if (ServerRuntime::Access::IsWhitelistEnabled())
{
whitelistSatisfied = false;
if (loginXuid != INVALID_XUID)
{
whitelistSatisfied = ServerRuntime::Access::IsPlayerWhitelisted(loginXuid);
}
if (!whitelistSatisfied && packet->m_onlineXuid != INVALID_XUID && packet->m_onlineXuid != loginXuid)
{
whitelistSatisfied = ServerRuntime::Access::IsPlayerWhitelisted(packet->m_onlineXuid);
}
}
#endif
if( sentDisconnect )
{
// Do nothing
}
else if( server->getPlayers()->isXuidBanned( packet->m_onlineXuid ) )
else if (bannedXuid)
{
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
ServerRuntime::ServerLogManager::OnRejectedPlayerLogin(GetPendingConnectionSmallId(connection), name, ServerRuntime::ServerLogManager::eLoginRejectReason_BannedXuid);
#endif
disconnect(DisconnectPacket::eDisconnect_Banned);
}
else if (!whitelistSatisfied)
{
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
ServerRuntime::ServerLogManager::OnRejectedPlayerLogin(GetPendingConnectionSmallId(connection), name, ServerRuntime::ServerLogManager::eLoginRejectReason_NotWhitelisted);
#endif
disconnect(DisconnectPacket::eDisconnect_Banned);
}
else if (duplicateXuid)
{
// The old player is still in PlayerList (disconnect hasn't been
// processed yet). Force-close the stale connection so the
// reconnecting client isn't rejected.
app.DebugPrintf("RECONNECT: Duplicate xuid for name: %ls, forcing old connection closed\n", name.c_str());
shared_ptr<ServerPlayer> stalePlayer = server->getPlayers()->getPlayer(loginXuid);
if (stalePlayer == nullptr && packet->m_onlineXuid != INVALID_XUID)
stalePlayer = server->getPlayers()->getPlayer(packet->m_onlineXuid);
if (stalePlayer != nullptr && stalePlayer->connection != nullptr)
{
BYTE oldSmallId = 0;
if (stalePlayer->connection->connection != nullptr && stalePlayer->connection->connection->getSocket() != nullptr)
oldSmallId = stalePlayer->connection->connection->getSocket()->getSmallId();
app.DebugPrintf("RECONNECT: Force-disconnecting old player smallId=%d\n", oldSmallId);
stalePlayer->connection->disconnect(DisconnectPacket::eDisconnect_Closed);
// Queue the old SmallId for recycling so it's not permanently leaked.
// PlayerList::tick() will call PushFreeSmallId/ClearSocketForSmallId.
if (oldSmallId != 0)
server->getPlayers()->queueSmallIdForRecycle(oldSmallId);
app.DebugPrintf("RECONNECT: Old player force-disconnect complete\n");
}
// Accept the login now that the old entry is removed.
app.DebugPrintf("RECONNECT: Calling handleAcceptedLogin for new connection\n");
handleAcceptedLogin(packet);
app.DebugPrintf("RECONNECT: handleAcceptedLogin complete\n");
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
ServerRuntime::ServerLogManager::OnRejectedPlayerLogin(GetPendingConnectionSmallId(connection), name, ServerRuntime::ServerLogManager::eLoginRejectReason_DuplicateXuid);
#endif
// Reject the incoming connection — a player with this UID is already
// on the server. Allowing duplicates causes invisible players and
// other undefined behaviour.
app.DebugPrintf("LOGIN: Rejecting duplicate xuid for name: %ls\n", name.c_str());
disconnect(DisconnectPacket::eDisconnect_Banned);
}
#ifdef _WINDOWS64
else if (g_bRejectDuplicateNames)
@ -234,6 +273,9 @@ void PendingConnection::handleLogin(shared_ptr<LoginPacket> packet)
}
if (nameTaken)
{
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
ServerRuntime::ServerLogManager::OnRejectedPlayerLogin(GetPendingConnectionSmallId(connection), name, ServerRuntime::ServerLogManager::eLoginRejectReason_DuplicateName);
#endif
app.DebugPrintf("Rejecting duplicate name: %ls\n", name.c_str());
disconnect(DisconnectPacket::eDisconnect_Banned);
}
@ -291,6 +333,9 @@ void PendingConnection::handleAcceptedLogin(shared_ptr<LoginPacket> packet)
shared_ptr<ServerPlayer> playerEntity = server->getPlayers()->getPlayerForLogin(this, name, playerXuid,packet->m_onlineXuid);
if (playerEntity != nullptr)
{
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
ServerRuntime::ServerLogManager::OnAcceptedPlayerLogin(GetPendingConnectionSmallId(connection), name);
#endif
server->getPlayers()->placeNewPlayer(connection, playerEntity, packet);
connection = nullptr; // We've moved responsibility for this over to the new PlayerConnection, nullptr so we don't delete our reference to it here in our dtor
}
@ -348,4 +393,4 @@ bool PendingConnection::isServerPacketListener()
bool PendingConnection::isDisconnected()
{
return done;
}
}

View file

@ -34,9 +34,26 @@
// 4J Added
#include "..\Minecraft.World\net.minecraft.world.item.crafting.h"
#include "Options.h"
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
#include "..\Minecraft.Server\ServerLogManager.h"
#endif
namespace
{
// Anti-cheat thresholds. Keep server-side checks authoritative even in host mode.
// Base max squared displacement allowed per move packet before speed flags trigger.
const double kMoveBaseAllowanceSq = 100.0;
// Extra squared displacement allowance derived from current server-side velocity.
const double kMoveVelocityAllowanceScale = 100.0;
// Max squared distance for interact/attack when the target is visible (normal reach).
const double kInteractReachSq = 6.0 * 6.0;
// Stricter max squared distance used when LOS is blocked to reduce wall-hit abuse.
const double kInteractBlockedReachSq = 3.0 * 3.0;
}
Random PlayerConnection::random;
PlayerConnection::PlayerConnection(MinecraftServer *server, Connection *connection, shared_ptr<ServerPlayer> player)
{
// 4J - added initialisers
@ -66,6 +83,13 @@ PlayerConnection::PlayerConnection(MinecraftServer *server, Connection *connecti
m_offlineXUID = INVALID_XUID;
m_onlineXUID = INVALID_XUID;
m_bHasClientTickedOnce = false;
m_logSmallId = 0;
// Cache the first valid transport smallId because disconnect teardown can clear it before the server logger runs.
if (this->connection != NULL && this->connection->getSocket() != NULL)
{
m_logSmallId = this->connection->getSocket()->getSmallId();
}
setShowOnMaps(app.GetGameHostOption(eGameHostOption_Gamertags)!=0?true:false);
}
@ -76,6 +100,17 @@ PlayerConnection::~PlayerConnection()
DeleteCriticalSection(&done_cs);
}
unsigned char PlayerConnection::getLogSmallId()
{
// Fall back to the live socket only while the cached value is still empty.
if (m_logSmallId == 0 && connection != NULL && connection->getSocket() != NULL)
{
m_logSmallId = connection->getSocket()->getSmallId();
}
return m_logSmallId;
}
void PlayerConnection::tick()
{
if( done ) return;
@ -118,6 +153,13 @@ void PlayerConnection::disconnect(DisconnectPacket::eDisconnectReason reason)
return;
}
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
ServerRuntime::ServerLogManager::OnPlayerDisconnected(
getLogSmallId(),
(player != NULL) ? player->name : std::wstring(),
reason,
true);
#endif
app.DebugPrintf("PlayerConnection disconect reason: %d\n", reason );
player->disconnect();
@ -271,16 +313,19 @@ void PlayerConnection::handleMovePlayer(shared_ptr<MovePlayerPacket> packet)
double dist = xDist * xDist + yDist * yDist + zDist * zDist;
// 4J-PB - removing this one for now
/*if (dist > 100.0f)
// Anti-cheat: reject movement packets that exceed server-authoritative bounds.
double velocitySq = player->xd * player->xd + player->yd * player->yd + player->zd * player->zd;
double maxAllowedSq = kMoveBaseAllowanceSq + (velocitySq * kMoveVelocityAllowanceScale);
if (player->isAllowedToFly() || player->gameMode->isCreative())
{
// logger.warning(player->name + " moved too quickly!");
disconnect(DisconnectPacket::eDisconnect_MovedTooQuickly);
// System.out.println("Moved too quickly at " + xt + ", " + yt + ", " + zt);
// teleport(player->x, player->y, player->z, player->yRot, player->xRot);
return;
// Creative / flight-allowed players can move farther legitimately per tick.
maxAllowedSq *= 1.5;
}
if (dist > maxAllowedSq)
{
disconnect(DisconnectPacket::eDisconnect_MovedTooQuickly);
return;
}
*/
float r = 1 / 16.0f;
bool oldOk = level->getCubes(player, player->bb->copy()->shrink(r, r, r))->empty();
@ -308,8 +353,8 @@ void PlayerConnection::handleMovePlayer(shared_ptr<MovePlayerPacket> packet)
xDist = xt - player->x;
yDist = yt - player->y;
// 4J-PB - line below will always be true!
if (yDist > -0.5 || yDist < 0.5)
// Clamp tiny Y drift noise to reduce false positives.
if (yDist > -0.5 && yDist < 0.5)
{
yDist = 0;
}
@ -430,7 +475,8 @@ void PlayerConnection::handlePlayerAction(shared_ptr<PlayerActionPacket> packet)
if (packet->action == PlayerActionPacket::START_DESTROY_BLOCK)
{
if (true) player->gameMode->startDestroyBlock(x, y, z, packet->face); // 4J - condition was !server->isUnderSpawnProtection(level, x, y, z, player) (from Java 1.6.4) but putting back to old behaviour
// Anti-cheat: validate spawn protection on the server for mining start.
if (!server->isUnderSpawnProtection(level, x, y, z, player)) player->gameMode->startDestroyBlock(x, y, z, packet->face);
else player->connection->send(std::make_shared<TileUpdatePacket>(x, y, z, level));
}
@ -458,8 +504,6 @@ void PlayerConnection::handleUseItem(shared_ptr<UseItemPacket> packet)
int face = packet->getFace();
player->resetLastActionTime();
// 4J Stu - We don't have ops, so just use the levels setting
bool canEditSpawn = level->canEditSpawn; // = level->dimension->id != 0 || server->players->isOp(player->name);
if (packet->getFace() == 255)
{
if (item == nullptr) return;
@ -469,7 +513,8 @@ void PlayerConnection::handleUseItem(shared_ptr<UseItemPacket> packet)
{
if (synched && player->distanceToSqr(x + 0.5, y + 0.5, z + 0.5) < 8 * 8)
{
if (true) // 4J - condition was !server->isUnderSpawnProtection(level, x, y, z, player) (from java 1.6.4) but putting back to old behaviour
// Anti-cheat: block placement/use must pass server-side spawn protection.
if (!server->isUnderSpawnProtection(level, x, y, z, player))
{
player->gameMode->useItemOn(player, level, item, x, y, z, face, packet->getClickX(), packet->getClickY(), packet->getClickZ());
}
@ -538,7 +583,18 @@ void PlayerConnection::handleUseItem(shared_ptr<UseItemPacket> packet)
void PlayerConnection::onDisconnect(DisconnectPacket::eDisconnectReason reason, void *reasonObjects)
{
EnterCriticalSection(&done_cs);
if( done ) return;
if( done )
{
LeaveCriticalSection(&done_cs);
return;
}
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
ServerRuntime::ServerLogManager::OnPlayerDisconnected(
getLogSmallId(),
(player != NULL) ? player->name : std::wstring(),
reason,
false);
#endif
// logger.info(player.name + " lost connection: " + reason);
// 4J-PB - removed, since it needs to be localised in the language the client is in
//server->players->broadcastAll( shared_ptr<ChatPacket>( new ChatPacket(L"<22>e" + player->name + L" left the game.") ) );
@ -742,17 +798,16 @@ void PlayerConnection::handleInteract(shared_ptr<InteractPacket> packet)
// 4J Stu - If the client says that we hit something, then agree with it. The canSee can fail here as it checks
// a ray from head->head, but we may actually be looking at a different part of the entity that can be seen
// even though the ray is blocked.
if (target != nullptr) // && player->canSee(target) && player->distanceToSqr(target) < 6 * 6)
if (target != nullptr)
{
//boole canSee = player->canSee(target);
//double maxDist = 6 * 6;
//if (!canSee)
//{
// maxDist = 3 * 3;
//}
// Anti-cheat: enforce reach and LOS on the server to reject forged hits.
bool canSeeTarget = player->canSee(target);
double maxDistSq = canSeeTarget ? kInteractReachSq : kInteractBlockedReachSq;
if (player->distanceToSqr(target) > maxDistSq)
{
return;
}
//if (player->distanceToSqr(target) < maxDist)
//{
if (packet->action == InteractPacket::INTERACT)
{
player->interact(target);
@ -767,7 +822,6 @@ void PlayerConnection::handleInteract(shared_ptr<InteractPacket> packet)
}
player->attack(target);
}
//}
}
}
@ -1588,13 +1642,13 @@ void PlayerConnection::handleCraftItem(shared_ptr<CraftItemPacket> packet)
if(iRecipe == -1)
return;
int recipeCount = (int)Recipes::getInstance()->getRecipies()->size();
if(iRecipe < 0 || iRecipe >= recipeCount)
return;
Recipy::INGREDIENTS_REQUIRED *pRecipeIngredientsRequired=Recipes::getInstance()->getRecipeIngredientsArray();
shared_ptr<ItemInstance> pTempItemInst=pRecipeIngredientsRequired[iRecipe].pRecipy->assemble(nullptr);
size_t recipeCount = Recipes::getInstance()->getRecipies()->size();
if (iRecipe < 0 || iRecipe >= (int)recipeCount)
return;
if(app.DebugSettingsOn() && (player->GetDebugOptions()&(1L<<eDebugSetting_CraftAnything)))
{
pTempItemInst->onCraftedBy(player->level, dynamic_pointer_cast<Player>( player->shared_from_this() ), pTempItemInst->count );

View file

@ -37,6 +37,7 @@ private:
int dropSpamTickCount;
bool m_bHasClientTickedOnce;
unsigned char m_logSmallId;
public:
PlayerConnection(MinecraftServer *server, Connection *connection, shared_ptr<ServerPlayer> player);
@ -45,6 +46,10 @@ public:
void disconnect(DisconnectPacket::eDisconnectReason reason);
private:
/**
* Returns the stable network smallId used by dedicated-server logging and refreshes it from the live socket when possible
*/
unsigned char getLogSmallId();
double xLastOk, yLastOk, zLastOk;
bool synched;

View file

@ -37,6 +37,11 @@
#include "Common\Network\Sony\NetworkPlayerSony.h"
#endif
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
#include "..\Minecraft.Server\Access\Access.h"
extern bool g_Win64DedicatedServer;
#endif
// 4J - this class is fairly substantially altered as there didn't seem any point in porting code for banning, whitelisting, ops etc.
PlayerList::PlayerList(MinecraftServer *server)
@ -1674,6 +1679,13 @@ bool PlayerList::isXuidBanned(PlayerUID xuid)
}
}
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
if (!banned && g_Win64DedicatedServer)
{
banned = ServerRuntime::Access::IsPlayerBanned(xuid);
}
#endif
return banned;
}

View file

@ -1077,7 +1077,12 @@ void ServerLevel::entityRemoved(shared_ptr<Entity> e)
shared_ptr<Entity> ServerLevel::getEntity(int id)
{
return entitiesById[id];
auto it = entitiesById.find(id);
if (it != entitiesById.end())
{
return it->second;
}
return nullptr;
}
bool ServerLevel::addGlobalEntity(shared_ptr<Entity> e)

View file

@ -176,31 +176,29 @@ void ServerPlayerGameMode::stopDestroyBlock(int x, int y, int z)
{
if (x == xDestroyBlock && y == yDestroyBlock && z == zDestroyBlock)
{
// int ticksSpentDestroying = gameTicks - destroyProgressStart;
int t = level->getTile(x, y, z);
if (t != 0)
{
Tile *tile = Tile::tiles[t];
// MGH - removed checking for the destroy progress here, it has already been checked on the client before it sent the packet.
// fixes issues with this failing to destroy because of packets bunching up
// float destroyProgress = tile->getDestroyProgress(player, player->level, x, y, z) * (ticksSpentDestroying + 1);
// if (destroyProgress >= .7f || bIgnoreDestroyProgress)
// Anti-cheat: re-check destroy progress on the server for STOP_DESTROY.
int ticksSpentDestroying = gameTicks - destroyProgressStart;
float destroyProgress = tile->getDestroyProgress(player, player->level, x, y, z) * (ticksSpentDestroying + 1);
if (destroyProgress >= 1.0f)
{
isDestroyingBlock = false;
level->destroyTileProgress(player->entityId, x, y, z, -1);
destroyBlock(x, y, z);
}
// else if (!hasDelayedDestroy)
// {
// isDestroyingBlock = false;
// hasDelayedDestroy = true;
// delayedDestroyX = x;
// delayedDestroyY = y;
// delayedDestroyZ = z;
// delayedTickStart = destroyProgressStart;
// }
else if (!hasDelayedDestroy)
{
// Keep server-authoritative mining while allowing legit latency to finish via delayed tick progression.
isDestroyingBlock = false;
hasDelayedDestroy = true;
delayedDestroyX = x;
delayedDestroyY = y;
delayedDestroyZ = z;
delayedTickStart = destroyProgressStart;
}
}
}
}
@ -393,4 +391,4 @@ void ServerPlayerGameMode::setGameRules(GameRulesInstance *rules)
{
if(m_gameRules != nullptr) delete m_gameRules;
m_gameRules = rules;
}
}

View file

@ -998,11 +998,7 @@ static void RADLINK gdraw_SetViewSizeAndWorldScale(S32 w, S32 h, F32 scalex, F32
{
static S32 s_lastW = 0, s_lastH = 0;
static F32 s_lastSx = 0, s_lastSy = 0;
if (w != s_lastW || h != s_lastH || scalex != s_lastSx || scaley != s_lastSy) {
app.DebugPrintf("[GDRAW] SetViewSize: fw=%d fh=%d scale=%.6f,%.6f frametex=%dx%d vx=%d vy=%d\n",
w, h, scalex, scaley, gdraw->frametex_width, gdraw->frametex_height, gdraw->vx, gdraw->vy);
s_lastW = w; s_lastH = h; s_lastSx = scalex; s_lastSy = scaley;
}
memset(gdraw->frame, 0, sizeof(gdraw->frame));
gdraw->cur = gdraw->frame;
gdraw->fw = w;

View file

@ -8,11 +8,20 @@
#include "WinsockNetLayer.h"
#include "..\..\Common\Network\PlatformNetworkManagerStub.h"
#include "..\..\..\Minecraft.World\Socket.h"
#if defined(MINECRAFT_SERVER_BUILD)
#include "..\..\..\Minecraft.Server\Access\Access.h"
#include "..\..\..\Minecraft.Server\ServerLogManager.h"
#endif
#include "..\..\..\Minecraft.World\DisconnectPacket.h"
#include "..\..\Minecraft.h"
#include "..\4JLibs\inc\4J_Profile.h"
#include <string>
static bool RecvExact(SOCKET sock, BYTE* buf, int len);
#if defined(MINECRAFT_SERVER_BUILD)
static bool TryGetNumericRemoteIp(const sockaddr_in &remoteAddress, std::string *outIp);
#endif
SOCKET WinsockNetLayer::s_listenSocket = INVALID_SOCKET;
SOCKET WinsockNetLayer::s_hostConnectionSocket = INVALID_SOCKET;
@ -65,6 +74,7 @@ char g_Win64MultiplayerIP[256] = "127.0.0.1";
bool g_Win64DedicatedServer = false;
int g_Win64DedicatedServerPort = WIN64_NET_DEFAULT_PORT;
char g_Win64DedicatedServerBindIP[256] = "";
bool g_Win64DedicatedServerLanAdvertise = true;
bool WinsockNetLayer::Initialize()
{
@ -90,7 +100,11 @@ bool WinsockNetLayer::Initialize()
s_initialized = true;
StartDiscovery();
// Dedicated Server does not use LAN session discovery and therefore does not initiate discovery.
if (!g_Win64DedicatedServer)
{
StartDiscovery();
}
return true;
}
@ -416,6 +430,11 @@ bool WinsockNetLayer::JoinGame(const char* ip, int port)
}
s_localSmallId = assignedSmallId;
// Save the host IP and port so JoinSplitScreen can connect to the same host
// regardless of how the connection was initiated (UI vs command line).
strncpy_s(g_Win64MultiplayerIP, sizeof(g_Win64MultiplayerIP), ip, _TRUNCATE);
g_Win64MultiplayerPort = port;
app.DebugPrintf("Win64 LAN: Connected to %s:%d, assigned smallId=%d\n", ip, port, s_localSmallId);
s_active = true;
@ -428,7 +447,7 @@ bool WinsockNetLayer::JoinGame(const char* ip, int port)
bool WinsockNetLayer::SendOnSocket(SOCKET sock, const void* data, int dataSize)
{
if (sock == INVALID_SOCKET || dataSize <= 0) return false;
if (sock == INVALID_SOCKET || dataSize <= 0 || dataSize > WIN64_NET_MAX_PACKET_SIZE) return false;
// TODO: s_sendLock is a single global lock for ALL sockets. If one client's
// send() blocks (TCP window full, slow WiFi), every other write thread stalls
@ -531,6 +550,27 @@ static bool RecvExact(SOCKET sock, BYTE* buf, int len)
return true;
}
#if defined(MINECRAFT_SERVER_BUILD)
static bool TryGetNumericRemoteIp(const sockaddr_in &remoteAddress, std::string *outIp)
{
if (outIp == nullptr)
{
return false;
}
outIp->clear();
char ipBuffer[64] = {};
const char *ip = inet_ntop(AF_INET, (void *)&remoteAddress.sin_addr, ipBuffer, sizeof(ipBuffer));
if (ip == nullptr || ip[0] == 0)
{
return false;
}
*outIp = ip;
return true;
}
#endif
void WinsockNetLayer::HandleDataReceived(BYTE fromSmallId, BYTE toSmallId, unsigned char* data, unsigned int dataSize)
{
INetworkPlayer* pPlayerFrom = g_NetworkManager.GetPlayerBySmallId(fromSmallId);
@ -565,7 +605,10 @@ DWORD WINAPI WinsockNetLayer::AcceptThreadProc(LPVOID param)
{
while (s_active)
{
SOCKET clientSocket = accept(s_listenSocket, NULL, NULL);
sockaddr_in remoteAddress;
ZeroMemory(&remoteAddress, sizeof(remoteAddress));
int remoteAddressLength = sizeof(remoteAddress);
SOCKET clientSocket = accept(s_listenSocket, (sockaddr*)&remoteAddress, &remoteAddressLength);
if (clientSocket == INVALID_SOCKET)
{
if (s_active)
@ -576,10 +619,36 @@ DWORD WINAPI WinsockNetLayer::AcceptThreadProc(LPVOID param)
int noDelay = 1;
setsockopt(clientSocket, IPPROTO_TCP, TCP_NODELAY, (const char*)&noDelay, sizeof(noDelay));
#if defined(MINECRAFT_SERVER_BUILD)
std::string remoteIp;
const bool hasRemoteIp = TryGetNumericRemoteIp(remoteAddress, &remoteIp);
const char *remoteIpForLog = hasRemoteIp ? remoteIp.c_str() : "unknown";
if (g_Win64DedicatedServer)
{
ServerRuntime::ServerLogManager::OnIncomingTcpConnection(remoteIpForLog);
if (hasRemoteIp && ServerRuntime::Access::IsIpBanned(remoteIp))
{
ServerRuntime::ServerLogManager::OnRejectedTcpConnection(remoteIpForLog, ServerRuntime::ServerLogManager::eTcpRejectReason_BannedIp);
SendRejectWithReason(clientSocket, DisconnectPacket::eDisconnect_Banned);
closesocket(clientSocket);
continue;
}
}
#endif
extern QNET_STATE _iQNetStubState;
if (_iQNetStubState != QNET_STATE_GAME_PLAY)
{
app.DebugPrintf("Win64 LAN: Rejecting connection, game not ready\n");
#if defined(MINECRAFT_SERVER_BUILD)
if (g_Win64DedicatedServer)
{
ServerRuntime::ServerLogManager::OnRejectedTcpConnection(remoteIpForLog, ServerRuntime::ServerLogManager::eTcpRejectReason_GameNotReady);
}
else
#endif
{
app.DebugPrintf("Win64 LAN: Rejecting connection, game not ready\n");
}
closesocket(clientSocket);
continue;
}
@ -587,7 +656,16 @@ DWORD WINAPI WinsockNetLayer::AcceptThreadProc(LPVOID param)
extern CPlatformNetworkManagerStub* g_pPlatformNetworkManager;
if (g_pPlatformNetworkManager != nullptr && !g_pPlatformNetworkManager->CanAcceptMoreConnections())
{
app.DebugPrintf("Win64 LAN: Rejecting connection, server at max players\n");
#if defined(MINECRAFT_SERVER_BUILD)
if (g_Win64DedicatedServer)
{
ServerRuntime::ServerLogManager::OnRejectedTcpConnection(remoteIpForLog, ServerRuntime::ServerLogManager::eTcpRejectReason_ServerFull);
}
else
#endif
{
app.DebugPrintf("Win64 LAN: Rejecting connection, server at max players\n");
}
SendRejectWithReason(clientSocket, DisconnectPacket::eDisconnect_ServerFull);
closesocket(clientSocket);
continue;
@ -607,7 +685,16 @@ DWORD WINAPI WinsockNetLayer::AcceptThreadProc(LPVOID param)
else
{
LeaveCriticalSection(&s_freeSmallIdLock);
app.DebugPrintf("Win64 LAN: Server full, rejecting connection\n");
#if defined(MINECRAFT_SERVER_BUILD)
if (g_Win64DedicatedServer)
{
ServerRuntime::ServerLogManager::OnRejectedTcpConnection(remoteIpForLog, ServerRuntime::ServerLogManager::eTcpRejectReason_ServerFull);
}
else
#endif
{
app.DebugPrintf("Win64 LAN: Server full, rejecting connection\n");
}
SendRejectWithReason(clientSocket, DisconnectPacket::eDisconnect_ServerFull);
closesocket(clientSocket);
continue;
@ -635,7 +722,16 @@ DWORD WINAPI WinsockNetLayer::AcceptThreadProc(LPVOID param)
int connIdx = static_cast<int>(s_connections.size()) - 1;
LeaveCriticalSection(&s_connectionsLock);
app.DebugPrintf("Win64 LAN: Client connected, assigned smallId=%d\n", assignedSmallId);
#if defined(MINECRAFT_SERVER_BUILD)
if (g_Win64DedicatedServer)
{
ServerRuntime::ServerLogManager::OnAcceptedTcpConnection(assignedSmallId, remoteIpForLog);
}
else
#endif
{
app.DebugPrintf("Win64 LAN: Client connected, assigned smallId=%d\n", assignedSmallId);
}
EnterCriticalSection(&s_smallIdToSocketLock);
s_smallIdToSocket[assignedSmallId] = clientSocket;
@ -757,6 +853,11 @@ bool WinsockNetLayer::PopDisconnectedSmallId(BYTE* outSmallId)
void WinsockNetLayer::PushFreeSmallId(BYTE smallId)
{
// SmallIds 0..(XUSER_MAX_COUNT-1) are permanently reserved for the host's
// local pads and must never be recycled to remote clients.
if (smallId < (BYTE)XUSER_MAX_COUNT)
return;
EnterCriticalSection(&s_freeSmallIdLock);
// Guard against double-recycle: the reconnect path (queueSmallIdForRecycle) and
// the DoWork disconnect path can both push the same smallId. If we allow duplicates,

View file

@ -171,5 +171,6 @@ extern char g_Win64MultiplayerIP[256];
extern bool g_Win64DedicatedServer;
extern int g_Win64DedicatedServerPort;
extern char g_Win64DedicatedServerBindIP[256];
extern bool g_Win64DedicatedServerLanAdvertise;
#endif

View file

@ -21,13 +21,17 @@ const char* PostProcesser::g_gammaPSCode =
"cbuffer GammaCB : register(b0)\n"
"{\n"
" float gamma;\n"
" float3 pad;\n"
" float _pad;\n"
" float2 uvOffset;\n"
" float2 uvScale;\n"
" float2 _pad2;\n"
"};\n"
"Texture2D sceneTex : register(t0);\n"
"SamplerState sceneSampler : register(s0);\n"
"float4 main(float4 pos : SV_Position, float2 uv : TEXCOORD0) : SV_Target\n"
"{\n"
" float4 color = sceneTex.Sample(sceneSampler, uv);\n"
" float2 texUV = uvOffset + uv * uvScale;\n"
" float4 color = sceneTex.Sample(sceneSampler, texUV);\n"
"\n"
" color.rgb = max(color.rgb, 0.0);\n"
"\n"
@ -158,7 +162,7 @@ void PostProcesser::Init()
cbDesc.BindFlags = D3D11_BIND_CONSTANT_BUFFER;
cbDesc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;
GammaCBData initData = {1.0f, {0, 0, 0}};
GammaCBData initData = {1.0f, 0, 0.0f, 0.0f, 1.0f, 1.0f, {0, 0}};
D3D11_SUBRESOURCE_DATA srData;
srData.pSysMem = &initData;
srData.SysMemPitch = 0;
@ -237,6 +241,10 @@ void PostProcesser::Apply() const
{
GammaCBData *cb = static_cast<GammaCBData *>(mapped.pData);
cb->gamma = m_gamma;
cb->uvOffsetX = 0.0f;
cb->uvOffsetY = 0.0f;
cb->uvScaleX = 1.0f;
cb->uvScaleY = 1.0f;
ctx->Unmap(m_pGammaCB, 0);
}
@ -333,27 +341,6 @@ void PostProcesser::ApplyFromCopied() const
ID3D11DeviceContext* ctx = g_pImmediateContext;
D3D11_MAPPED_SUBRESOURCE mapped;
const D3D11_MAP mapType = m_wineMode ? D3D11_MAP_WRITE_NO_OVERWRITE : D3D11_MAP_WRITE_DISCARD;
const HRESULT hr = ctx->Map(m_pGammaCB, 0, mapType, 0, &mapped);
if (SUCCEEDED(hr))
{
const auto cb = static_cast<GammaCBData*>(mapped.pData);
cb->gamma = m_gamma;
ctx->Unmap(m_pGammaCB, 0);
}
ID3D11RenderTargetView* oldRTV = nullptr;
ID3D11DepthStencilView* oldDSV = nullptr;
ctx->OMGetRenderTargets(1, &oldRTV, &oldDSV);
UINT numViewports = 1;
D3D11_VIEWPORT oldViewport = {};
ctx->RSGetViewports(&numViewports, &oldViewport);
ID3D11RenderTargetView* bbRTV = g_pRenderTargetView;
ctx->OMSetRenderTargets(1, &bbRTV, nullptr);
D3D11_VIEWPORT vp;
if (m_useCustomViewport)
{
@ -369,6 +356,33 @@ void PostProcesser::ApplyFromCopied() const
vp.TopLeftY = 0;
}
D3D11_MAPPED_SUBRESOURCE mapped;
const D3D11_MAP mapType = m_wineMode ? D3D11_MAP_WRITE_NO_OVERWRITE : D3D11_MAP_WRITE_DISCARD;
const HRESULT hr = ctx->Map(m_pGammaCB, 0, mapType, 0, &mapped);
if (SUCCEEDED(hr))
{
const auto cb = static_cast<GammaCBData*>(mapped.pData);
cb->gamma = m_gamma;
const float texW = static_cast<float>(m_gammaTexWidth);
const float texH = static_cast<float>(m_gammaTexHeight);
cb->uvOffsetX = vp.TopLeftX / texW;
cb->uvOffsetY = vp.TopLeftY / texH;
cb->uvScaleX = vp.Width / texW;
cb->uvScaleY = vp.Height / texH;
ctx->Unmap(m_pGammaCB, 0);
}
ID3D11RenderTargetView* oldRTV = nullptr;
ID3D11DepthStencilView* oldDSV = nullptr;
ctx->OMGetRenderTargets(1, &oldRTV, &oldDSV);
UINT numViewports = 1;
D3D11_VIEWPORT oldViewport = {};
ctx->RSGetViewports(&numViewports, &oldViewport);
ID3D11RenderTargetView* bbRTV = g_pRenderTargetView;
ctx->OMSetRenderTargets(1, &bbRTV, nullptr);
ctx->RSSetViewports(1, &vp);
ctx->IASetInputLayout(nullptr);

View file

@ -1,5 +1,18 @@
$sha = (git rev-parse --short=7 HEAD)
$ref = (git symbolic-ref --short HEAD)
if ($env:GITHUB_REPOSITORY) {
$ref = "$env:GITHUB_REPOSITORY/$(git symbolic-ref --short HEAD)"
} else {
$remoteUrl = (git remote get-url origin)
# handle github urls only, can't predict other origins behavior
if ($remoteUrl -match '(?:github\.com[:/])([^/:]+/[^/]+?)(?:\.git)?$') {
$ref = "$($matches[1])/$(git symbolic-ref --short HEAD)"
}else{
# fallback to just symbolic ref in case remote isnt what we expect
$ref = "UNKNOWN/$(git symbolic-ref --short HEAD)"
}
}
$build = 560 # Note: Build/network has to stay static for now, as without it builds wont be able to play together. We can change it later when we have a better versioning scheme in place.
$suffix = ""
@ -18,7 +31,8 @@ if (git status --porcelain) {
#pragma once
#define VER_PRODUCTBUILD $build
#define VER_PRODUCTVERSION_STR_W L"$sha$suffix ($ref)"
#define VER_PRODUCTVERSION_STR_W L"$sha$suffix"
#define VER_FILEVERSION_STR_W VER_PRODUCTVERSION_STR_W
#define VER_BRANCHVERSION_STR_W L"$ref"
#define VER_NETWORK VER_PRODUCTBUILD
"@ | Set-Content "Common/BuildVer.h"

View file

@ -0,0 +1,460 @@
#include "stdafx.h"
#include "Access.h"
#include "..\Common\StringUtils.h"
#include "..\ServerLogger.h"
#include <memory>
#include <mutex>
namespace ServerRuntime
{
namespace Access
{
namespace
{
/**
* **Access State**
*
* These features are used extensively from various parts of the code, so safe read/write handling is implemented
* Stores the published BAN manager snapshot plus a writer gate for clone-and-publish updates
* BanManagerスナップショットと更新直列化用ロックを保持する
*/
struct AccessState
{
std::mutex stateLock;
std::mutex writeLock;
std::shared_ptr<BanManager> banManager;
std::shared_ptr<WhitelistManager> whitelistManager;
bool whitelistEnabled = false;
};
AccessState g_accessState;
/**
* Copies the currently published manager pointer so readers can work without holding the publish mutex
* BanManager共有ポインタを複製取得する
*/
static std::shared_ptr<BanManager> GetBanManagerSnapshot()
{
std::lock_guard<std::mutex> stateLock(g_accessState.stateLock);
return g_accessState.banManager;
}
/**
* Replaces the shared manager pointer with a fully prepared snapshot in one short critical section
* BanManagerスナップショットを短いロックで公開する
*/
static void PublishBanManagerSnapshot(const std::shared_ptr<BanManager> &banManager)
{
std::lock_guard<std::mutex> stateLock(g_accessState.stateLock);
g_accessState.banManager = banManager;
}
static std::shared_ptr<WhitelistManager> GetWhitelistManagerSnapshot()
{
std::lock_guard<std::mutex> stateLock(g_accessState.stateLock);
return g_accessState.whitelistManager;
}
static void PublishWhitelistManagerSnapshot(const std::shared_ptr<WhitelistManager> &whitelistManager)
{
std::lock_guard<std::mutex> stateLock(g_accessState.stateLock);
g_accessState.whitelistManager = whitelistManager;
}
}
std::string FormatXuid(PlayerUID xuid)
{
if (xuid == INVALID_XUID)
{
return "";
}
char buffer[32] = {};
sprintf_s(buffer, sizeof(buffer), "0x%016llx", (unsigned long long)xuid);
return buffer;
}
bool TryParseXuid(const std::string &text, PlayerUID *outXuid)
{
if (outXuid == nullptr)
{
return false;
}
unsigned long long parsed = 0;
if (!StringUtils::TryParseUnsignedLongLong(text, &parsed) || parsed == 0ULL)
{
return false;
}
*outXuid = (PlayerUID)parsed;
return true;
}
bool Initialize(const std::string &baseDirectory, bool whitelistEnabled)
{
std::lock_guard<std::mutex> writeLock(g_accessState.writeLock);
// Build the replacement manager privately so readers keep using the last published snapshot during disk I/O.
std::shared_ptr<BanManager> banManager = std::make_shared<BanManager>(baseDirectory);
std::shared_ptr<WhitelistManager> whitelistManager = std::make_shared<WhitelistManager>(baseDirectory);
if (!banManager->EnsureBanFilesExist())
{
LogError("access", "failed to ensure dedicated server ban files exist");
return false;
}
if (!whitelistManager->EnsureWhitelistFileExists())
{
LogError("access", "failed to ensure dedicated server whitelist file exists");
return false;
}
if (!banManager->Reload())
{
LogError("access", "failed to load dedicated server ban files");
return false;
}
if (!whitelistManager->Reload())
{
LogError("access", "failed to load dedicated server whitelist file");
return false;
}
std::vector<BannedPlayerEntry> playerEntries;
std::vector<BannedIpEntry> ipEntries;
std::vector<WhitelistedPlayerEntry> whitelistEntries;
banManager->SnapshotBannedPlayers(&playerEntries);
banManager->SnapshotBannedIps(&ipEntries);
whitelistManager->SnapshotWhitelistedPlayers(&whitelistEntries);
PublishBanManagerSnapshot(banManager);
PublishWhitelistManagerSnapshot(whitelistManager);
{
std::lock_guard<std::mutex> stateLock(g_accessState.stateLock);
g_accessState.whitelistEnabled = whitelistEnabled;
}
LogInfof(
"access",
"loaded %u player bans, %u ip bans, and %u whitelist entries (whitelist=%s)",
(unsigned)playerEntries.size(),
(unsigned)ipEntries.size(),
(unsigned)whitelistEntries.size(),
whitelistEnabled ? "enabled" : "disabled");
return true;
}
void Shutdown()
{
std::lock_guard<std::mutex> writeLock(g_accessState.writeLock);
PublishBanManagerSnapshot(std::shared_ptr<BanManager>{});
PublishWhitelistManagerSnapshot(std::shared_ptr<WhitelistManager>{});
std::lock_guard<std::mutex> stateLock(g_accessState.stateLock);
g_accessState.whitelistEnabled = false;
}
bool Reload()
{
std::lock_guard<std::mutex> writeLock(g_accessState.writeLock);
std::shared_ptr<BanManager> current = GetBanManagerSnapshot();
std::shared_ptr<WhitelistManager> currentWhitelist = GetWhitelistManagerSnapshot();
if (current == nullptr || currentWhitelist == nullptr)
{
return false;
}
std::shared_ptr<BanManager> banManager = std::make_shared<BanManager>(*current);
std::shared_ptr<WhitelistManager> whitelistManager = std::make_shared<WhitelistManager>(*currentWhitelist);
if (!banManager->EnsureBanFilesExist())
{
return false;
}
if (!whitelistManager->EnsureWhitelistFileExists())
{
return false;
}
if (!banManager->Reload())
{
return false;
}
if (!whitelistManager->Reload())
{
return false;
}
PublishBanManagerSnapshot(banManager);
PublishWhitelistManagerSnapshot(whitelistManager);
return true;
}
bool ReloadWhitelist()
{
std::lock_guard<std::mutex> writeLock(g_accessState.writeLock);
const auto current = GetWhitelistManagerSnapshot();
if (current == nullptr)
{
return false;
}
auto whitelistManager = std::make_shared<WhitelistManager>(*current);
if (!whitelistManager->EnsureWhitelistFileExists())
{
return false;
}
if (!whitelistManager->Reload())
{
return false;
}
PublishWhitelistManagerSnapshot(whitelistManager);
return true;
}
bool IsInitialized()
{
return GetBanManagerSnapshot() != nullptr && GetWhitelistManagerSnapshot() != nullptr;
}
bool IsWhitelistEnabled()
{
std::lock_guard<std::mutex> stateLock(g_accessState.stateLock);
return g_accessState.whitelistEnabled;
}
void SetWhitelistEnabled(bool enabled)
{
std::lock_guard<std::mutex> writeLock(g_accessState.writeLock);
std::lock_guard<std::mutex> stateLock(g_accessState.stateLock);
g_accessState.whitelistEnabled = enabled;
}
bool IsPlayerBanned(PlayerUID xuid)
{
const std::string formatted = FormatXuid(xuid);
if (formatted.empty())
{
return false;
}
std::shared_ptr<BanManager> banManager = GetBanManagerSnapshot();
return (banManager != nullptr) ? banManager->IsPlayerBannedByXuid(formatted) : false;
}
bool IsIpBanned(const std::string &ip)
{
std::shared_ptr<BanManager> banManager = GetBanManagerSnapshot();
return (banManager != nullptr) ? banManager->IsIpBanned(ip) : false;
}
bool IsPlayerWhitelisted(PlayerUID xuid)
{
const std::string formatted = FormatXuid(xuid);
if (formatted.empty())
{
return false;
}
std::shared_ptr<WhitelistManager> whitelistManager = GetWhitelistManagerSnapshot();
return (whitelistManager != nullptr) ? whitelistManager->IsPlayerWhitelistedByXuid(formatted) : false;
}
bool AddPlayerBan(PlayerUID xuid, const std::string &name, const BanMetadata &metadata)
{
const std::string formatted = FormatXuid(xuid);
if (formatted.empty())
{
return false;
}
std::lock_guard<std::mutex> writeLock(g_accessState.writeLock);
std::shared_ptr<BanManager> current = GetBanManagerSnapshot();
if (current == nullptr)
{
return false;
}
std::shared_ptr<BanManager> banManager = std::make_shared<BanManager>(*current);
BannedPlayerEntry entry;
entry.xuid = formatted;
entry.name = name;
entry.metadata = metadata;
if (!banManager->AddPlayerBan(entry))
{
return false;
}
PublishBanManagerSnapshot(banManager);
return true;
}
bool AddIpBan(const std::string &ip, const BanMetadata &metadata)
{
std::lock_guard<std::mutex> writeLock(g_accessState.writeLock);
std::shared_ptr<BanManager> current = GetBanManagerSnapshot();
if (current == nullptr)
{
return false;
}
std::shared_ptr<BanManager> banManager = std::make_shared<BanManager>(*current);
BannedIpEntry entry;
entry.ip = ip;
entry.metadata = metadata;
if (!banManager->AddIpBan(entry))
{
return false;
}
PublishBanManagerSnapshot(banManager);
return true;
}
bool RemovePlayerBan(PlayerUID xuid)
{
const std::string formatted = FormatXuid(xuid);
if (formatted.empty())
{
return false;
}
std::lock_guard<std::mutex> writeLock(g_accessState.writeLock);
std::shared_ptr<BanManager> current = GetBanManagerSnapshot();
if (current == nullptr)
{
return false;
}
std::shared_ptr<BanManager> banManager = std::make_shared<BanManager>(*current);
if (!banManager->RemovePlayerBanByXuid(formatted))
{
return false;
}
PublishBanManagerSnapshot(banManager);
return true;
}
bool RemoveIpBan(const std::string &ip)
{
std::lock_guard<std::mutex> writeLock(g_accessState.writeLock);
std::shared_ptr<BanManager> current = GetBanManagerSnapshot();
if (current == nullptr)
{
return false;
}
std::shared_ptr<BanManager> banManager = std::make_shared<BanManager>(*current);
if (!banManager->RemoveIpBan(ip))
{
return false;
}
PublishBanManagerSnapshot(banManager);
return true;
}
bool AddWhitelistedPlayer(PlayerUID xuid, const std::string &name, const WhitelistMetadata &metadata)
{
const auto formatted = FormatXuid(xuid);
if (formatted.empty())
{
return false;
}
std::lock_guard<std::mutex> writeLock(g_accessState.writeLock);
const auto current = GetWhitelistManagerSnapshot();
if (current == nullptr)
{
return false;
}
auto whitelistManager = std::make_shared<WhitelistManager>(*current);
const WhitelistedPlayerEntry entry = { formatted, name, metadata };
if (!whitelistManager->AddPlayer(entry))
{
return false;
}
PublishWhitelistManagerSnapshot(whitelistManager);
return true;
}
bool RemoveWhitelistedPlayer(PlayerUID xuid)
{
const auto formatted = FormatXuid(xuid);
if (formatted.empty())
{
return false;
}
std::lock_guard<std::mutex> writeLock(g_accessState.writeLock);
const auto current = GetWhitelistManagerSnapshot();
if (current == nullptr)
{
return false;
}
auto whitelistManager = std::make_shared<WhitelistManager>(*current);
if (!whitelistManager->RemovePlayerByXuid(formatted))
{
return false;
}
PublishWhitelistManagerSnapshot(whitelistManager);
return true;
}
bool SnapshotBannedPlayers(std::vector<BannedPlayerEntry> *outEntries)
{
if (outEntries == nullptr)
{
return false;
}
std::shared_ptr<BanManager> banManager = GetBanManagerSnapshot();
if (banManager == nullptr)
{
outEntries->clear();
return false;
}
return banManager->SnapshotBannedPlayers(outEntries);
}
bool SnapshotBannedIps(std::vector<BannedIpEntry> *outEntries)
{
if (outEntries == nullptr)
{
return false;
}
std::shared_ptr<BanManager> banManager = GetBanManagerSnapshot();
if (banManager == nullptr)
{
outEntries->clear();
return false;
}
return banManager->SnapshotBannedIps(outEntries);
}
bool SnapshotWhitelistedPlayers(std::vector<WhitelistedPlayerEntry> *outEntries)
{
if (outEntries == nullptr)
{
return false;
}
const auto whitelistManager = GetWhitelistManagerSnapshot();
if (whitelistManager == nullptr)
{
outEntries->clear();
return false;
}
return whitelistManager->SnapshotWhitelistedPlayers(outEntries);
}
}
}

View file

@ -0,0 +1,47 @@
#pragma once
#include "BanManager.h"
#include "WhitelistManager.h"
namespace ServerRuntime
{
/**
* A frontend that will be general-purpose, assuming the implementation of whitelists and ops in the future.
*/
namespace Access
{
bool Initialize(const std::string &baseDirectory = ".", bool whitelistEnabled = false);
void Shutdown();
bool Reload();
bool ReloadWhitelist();
bool IsInitialized();
bool IsWhitelistEnabled();
void SetWhitelistEnabled(bool enabled);
bool IsPlayerBanned(PlayerUID xuid);
bool IsIpBanned(const std::string &ip);
bool IsPlayerWhitelisted(PlayerUID xuid);
bool AddPlayerBan(PlayerUID xuid, const std::string &name, const BanMetadata &metadata);
bool AddIpBan(const std::string &ip, const BanMetadata &metadata);
bool RemovePlayerBan(PlayerUID xuid);
bool RemoveIpBan(const std::string &ip);
bool AddWhitelistedPlayer(PlayerUID xuid, const std::string &name, const WhitelistMetadata &metadata);
bool RemoveWhitelistedPlayer(PlayerUID xuid);
/**
* Copies the current cached player bans for inspection or command output
* BAN一覧を複製取得
*/
bool SnapshotBannedPlayers(std::vector<BannedPlayerEntry> *outEntries);
/**
* Copies the current cached IP bans for inspection or command output
* IP BAN一覧を複製取得
*/
bool SnapshotBannedIps(std::vector<BannedIpEntry> *outEntries);
bool SnapshotWhitelistedPlayers(std::vector<WhitelistedPlayerEntry> *outEntries);
std::string FormatXuid(PlayerUID xuid);
bool TryParseXuid(const std::string &text, PlayerUID *outXuid);
}
}

View file

@ -0,0 +1,631 @@
#include "stdafx.h"
#include "BanManager.h"
#include "..\Common\AccessStorageUtils.h"
#include "..\Common\FileUtils.h"
#include "..\Common\NetworkUtils.h"
#include "..\Common\StringUtils.h"
#include "..\ServerLogger.h"
#include "..\vendor\nlohmann\json.hpp"
#include <algorithm>
#include <stdio.h>
namespace ServerRuntime
{
namespace Access
{
using OrderedJson = nlohmann::ordered_json;
namespace
{
static const char *kBannedPlayersFileName = "banned-players.json";
static const char *kBannedIpsFileName = "banned-ips.json";
static bool TryParseUtcTimestamp(const std::string &text, unsigned long long *outFileTime)
{
if (outFileTime == nullptr)
{
return false;
}
std::string trimmed = StringUtils::TrimAscii(text);
if (trimmed.empty())
{
return false;
}
unsigned year = 0;
unsigned month = 0;
unsigned day = 0;
unsigned hour = 0;
unsigned minute = 0;
unsigned second = 0;
if (sscanf_s(trimmed.c_str(), "%4u-%2u-%2uT%2u:%2u:%2uZ", &year, &month, &day, &hour, &minute, &second) != 6)
{
return false;
}
SYSTEMTIME utc = {};
utc.wYear = (WORD)year;
utc.wMonth = (WORD)month;
utc.wDay = (WORD)day;
utc.wHour = (WORD)hour;
utc.wMinute = (WORD)minute;
utc.wSecond = (WORD)second;
FILETIME fileTime = {};
if (!SystemTimeToFileTime(&utc, &fileTime))
{
return false;
}
ULARGE_INTEGER value = {};
value.LowPart = fileTime.dwLowDateTime;
value.HighPart = fileTime.dwHighDateTime;
*outFileTime = value.QuadPart;
return true;
}
static bool IsMetadataExpired(const BanMetadata &metadata, unsigned long long nowFileTime)
{
if (metadata.expires.empty())
{
return false;
}
unsigned long long expiresFileTime = 0;
if (!TryParseUtcTimestamp(metadata.expires, &expiresFileTime))
{
// Keep malformed metadata active instead of silently unbanning a player or address.
return false;
}
return expiresFileTime <= nowFileTime;
}
}
BanManager::BanManager(const std::string &baseDirectory)
: m_baseDirectory(baseDirectory.empty() ? "." : baseDirectory)
{
}
bool BanManager::EnsureBanFilesExist() const
{
const std::string playersPath = GetBannedPlayersFilePath();
const std::string ipsPath = GetBannedIpsFilePath();
const bool playersOk = AccessStorageUtils::EnsureJsonListFileExists(playersPath);
const bool ipsOk = AccessStorageUtils::EnsureJsonListFileExists(ipsPath);
if (!playersOk)
{
LogErrorf("access", "failed to create %s", playersPath.c_str());
}
if (!ipsOk)
{
LogErrorf("access", "failed to create %s", ipsPath.c_str());
}
return playersOk && ipsOk;
}
bool BanManager::Reload()
{
std::vector<BannedPlayerEntry> players;
std::vector<BannedIpEntry> ips;
if (!LoadPlayers(&players))
{
return false;
}
if (!LoadIps(&ips))
{
return false;
}
m_bannedPlayers.swap(players);
m_bannedIps.swap(ips);
return true;
}
bool BanManager::Save() const
{
std::vector<BannedPlayerEntry> players;
std::vector<BannedIpEntry> ips;
return SnapshotBannedPlayers(&players) &&
SnapshotBannedIps(&ips) &&
SavePlayers(players) &&
SaveIps(ips);
}
bool BanManager::LoadPlayers(std::vector<BannedPlayerEntry> *outEntries) const
{
if (outEntries == nullptr)
{
return false;
}
outEntries->clear();
std::string text;
const std::string path = GetBannedPlayersFilePath();
if (!FileUtils::ReadTextFile(path, &text))
{
LogErrorf("access", "failed to read %s", path.c_str());
return false;
}
if (text.empty())
{
text = "[]";
}
OrderedJson root;
try
{
// Strip an optional UTF-8 BOM because some editors prepend it when rewriting JSON files.
root = OrderedJson::parse(StringUtils::StripUtf8Bom(text));
}
catch (const nlohmann::json::exception &e)
{
LogErrorf("access", "failed to parse %s: %s", path.c_str(), e.what());
return false;
}
if (!root.is_array())
{
LogErrorf("access", "failed to parse %s: root json value is not an array", path.c_str());
return false;
}
const unsigned long long nowFileTime = FileUtils::GetCurrentUtcFileTime();
for (const auto &object : root)
{
if (!object.is_object())
{
LogWarnf("access", "skipping banned player entry that is not an object in %s", path.c_str());
continue;
}
std::string rawXuid;
if (!AccessStorageUtils::TryGetStringField(object, "xuid", &rawXuid))
{
LogWarnf("access", "skipping banned player entry without xuid in %s", path.c_str());
continue;
}
BannedPlayerEntry entry;
entry.xuid = AccessStorageUtils::NormalizeXuid(rawXuid);
if (entry.xuid.empty())
{
LogWarnf("access", "skipping banned player entry with empty xuid in %s", path.c_str());
continue;
}
AccessStorageUtils::TryGetStringField(object, "name", &entry.name);
AccessStorageUtils::TryGetStringField(object, "created", &entry.metadata.created);
AccessStorageUtils::TryGetStringField(object, "source", &entry.metadata.source);
AccessStorageUtils::TryGetStringField(object, "expires", &entry.metadata.expires);
AccessStorageUtils::TryGetStringField(object, "reason", &entry.metadata.reason);
NormalizeMetadata(&entry.metadata);
// Ignore entries that already expired before reload so the in-memory cache starts from the active set.
if (IsMetadataExpired(entry.metadata, nowFileTime))
{
continue;
}
outEntries->push_back(entry);
}
return true;
}
bool BanManager::LoadIps(std::vector<BannedIpEntry> *outEntries) const
{
if (outEntries == nullptr)
{
return false;
}
outEntries->clear();
std::string text;
const std::string path = GetBannedIpsFilePath();
if (!FileUtils::ReadTextFile(path, &text))
{
LogErrorf("access", "failed to read %s", path.c_str());
return false;
}
if (text.empty())
{
text = "[]";
}
OrderedJson root;
try
{
// Strip an optional UTF-8 BOM because some editors prepend it when rewriting JSON files.
root = OrderedJson::parse(StringUtils::StripUtf8Bom(text));
}
catch (const nlohmann::json::exception &e)
{
LogErrorf("access", "failed to parse %s: %s", path.c_str(), e.what());
return false;
}
if (!root.is_array())
{
LogErrorf("access", "failed to parse %s: root json value is not an array", path.c_str());
return false;
}
const unsigned long long nowFileTime = FileUtils::GetCurrentUtcFileTime();
for (const auto &object : root)
{
if (!object.is_object())
{
LogWarnf("access", "skipping banned ip entry that is not an object in %s", path.c_str());
continue;
}
std::string rawIp;
if (!AccessStorageUtils::TryGetStringField(object, "ip", &rawIp))
{
LogWarnf("access", "skipping banned ip entry without ip in %s", path.c_str());
continue;
}
BannedIpEntry entry;
entry.ip = NetworkUtils::NormalizeIpToken(rawIp);
if (entry.ip.empty())
{
LogWarnf("access", "skipping banned ip entry with empty ip in %s", path.c_str());
continue;
}
AccessStorageUtils::TryGetStringField(object, "created", &entry.metadata.created);
AccessStorageUtils::TryGetStringField(object, "source", &entry.metadata.source);
AccessStorageUtils::TryGetStringField(object, "expires", &entry.metadata.expires);
AccessStorageUtils::TryGetStringField(object, "reason", &entry.metadata.reason);
NormalizeMetadata(&entry.metadata);
// Ignore entries that already expired before reload so the in-memory cache starts from the active set.
if (IsMetadataExpired(entry.metadata, nowFileTime))
{
continue;
}
outEntries->push_back(entry);
}
return true;
}
bool BanManager::SavePlayers(const std::vector<BannedPlayerEntry> &entries) const
{
OrderedJson root = OrderedJson::array();
for (const auto &entry : entries)
{
OrderedJson object = OrderedJson::object();
object["xuid"] = AccessStorageUtils::NormalizeXuid(entry.xuid);
object["name"] = entry.name;
object["created"] = entry.metadata.created;
object["source"] = entry.metadata.source;
object["expires"] = entry.metadata.expires;
object["reason"] = entry.metadata.reason;
root.push_back(object);
}
const std::string path = GetBannedPlayersFilePath();
const std::string json = root.empty() ? std::string("[]\n") : (root.dump(2) + "\n");
if (!FileUtils::WriteTextFileAtomic(path, json))
{
LogErrorf("access", "failed to write %s", path.c_str());
return false;
}
return true;
}
bool BanManager::SaveIps(const std::vector<BannedIpEntry> &entries) const
{
OrderedJson root = OrderedJson::array();
for (const auto &entry : entries)
{
OrderedJson object = OrderedJson::object();
object["ip"] = NetworkUtils::NormalizeIpToken(entry.ip);
object["created"] = entry.metadata.created;
object["source"] = entry.metadata.source;
object["expires"] = entry.metadata.expires;
object["reason"] = entry.metadata.reason;
root.push_back(object);
}
const std::string path = GetBannedIpsFilePath();
const std::string json = root.empty() ? std::string("[]\n") : (root.dump(2) + "\n");
if (!FileUtils::WriteTextFileAtomic(path, json))
{
LogErrorf("access", "failed to write %s", path.c_str());
return false;
}
return true;
}
const std::vector<BannedPlayerEntry> &BanManager::GetBannedPlayers() const
{
return m_bannedPlayers;
}
const std::vector<BannedIpEntry> &BanManager::GetBannedIps() const
{
return m_bannedIps;
}
bool BanManager::SnapshotBannedPlayers(std::vector<BannedPlayerEntry> *outEntries) const
{
if (outEntries == nullptr)
{
return false;
}
outEntries->clear();
outEntries->reserve(m_bannedPlayers.size());
const unsigned long long nowFileTime = FileUtils::GetCurrentUtcFileTime();
for (const auto &entry : m_bannedPlayers)
{
if (!IsMetadataExpired(entry.metadata, nowFileTime))
{
outEntries->push_back(entry);
}
}
return true;
}
bool BanManager::SnapshotBannedIps(std::vector<BannedIpEntry> *outEntries) const
{
if (outEntries == nullptr)
{
return false;
}
outEntries->clear();
outEntries->reserve(m_bannedIps.size());
const unsigned long long nowFileTime = FileUtils::GetCurrentUtcFileTime();
for (const auto &entry : m_bannedIps)
{
if (!IsMetadataExpired(entry.metadata, nowFileTime))
{
outEntries->push_back(entry);
}
}
return true;
}
bool BanManager::IsPlayerBannedByXuid(const std::string &xuid) const
{
const std::string normalized = AccessStorageUtils::NormalizeXuid(xuid);
if (normalized.empty())
{
return false;
}
const unsigned long long nowFileTime = FileUtils::GetCurrentUtcFileTime();
return std::any_of(
m_bannedPlayers.begin(),
m_bannedPlayers.end(),
[&normalized, nowFileTime](const BannedPlayerEntry &entry)
{
return entry.xuid == normalized && !IsMetadataExpired(entry.metadata, nowFileTime);
});
}
bool BanManager::IsIpBanned(const std::string &ip) const
{
const std::string normalized = NetworkUtils::NormalizeIpToken(ip);
if (normalized.empty())
{
return false;
}
const unsigned long long nowFileTime = FileUtils::GetCurrentUtcFileTime();
return std::any_of(
m_bannedIps.begin(),
m_bannedIps.end(),
[&normalized, nowFileTime](const BannedIpEntry &entry)
{
return entry.ip == normalized && !IsMetadataExpired(entry.metadata, nowFileTime);
});
}
bool BanManager::AddPlayerBan(const BannedPlayerEntry &entry)
{
std::vector<BannedPlayerEntry> updatedEntries;
if (!SnapshotBannedPlayers(&updatedEntries))
{
return false;
}
BannedPlayerEntry normalized = entry;
normalized.xuid = AccessStorageUtils::NormalizeXuid(normalized.xuid);
NormalizeMetadata(&normalized.metadata);
if (normalized.xuid.empty())
{
return false;
}
const auto existing = std::find_if(
updatedEntries.begin(),
updatedEntries.end(),
[&normalized](const BannedPlayerEntry &candidate)
{
return candidate.xuid == normalized.xuid;
});
if (existing != updatedEntries.end())
{
// Update the existing entry in place so the stored list remains unique by canonical XUID.
*existing = normalized;
if (!SavePlayers(updatedEntries))
{
return false;
}
m_bannedPlayers.swap(updatedEntries);
return true;
}
updatedEntries.push_back(normalized);
if (!SavePlayers(updatedEntries))
{
return false;
}
m_bannedPlayers.swap(updatedEntries);
return true;
}
bool BanManager::AddIpBan(const BannedIpEntry &entry)
{
std::vector<BannedIpEntry> updatedEntries;
if (!SnapshotBannedIps(&updatedEntries))
{
return false;
}
BannedIpEntry normalized = entry;
normalized.ip = NetworkUtils::NormalizeIpToken(normalized.ip);
NormalizeMetadata(&normalized.metadata);
if (normalized.ip.empty())
{
return false;
}
const auto existing = std::find_if(
updatedEntries.begin(),
updatedEntries.end(),
[&normalized](const BannedIpEntry &candidate)
{
return candidate.ip == normalized.ip;
});
if (existing != updatedEntries.end())
{
// Update the existing entry in place so the stored list remains unique by normalized IP.
*existing = normalized;
if (!SaveIps(updatedEntries))
{
return false;
}
m_bannedIps.swap(updatedEntries);
return true;
}
updatedEntries.push_back(normalized);
if (!SaveIps(updatedEntries))
{
return false;
}
m_bannedIps.swap(updatedEntries);
return true;
}
bool BanManager::RemovePlayerBanByXuid(const std::string &xuid)
{
const std::string normalized = AccessStorageUtils::NormalizeXuid(xuid);
if (normalized.empty())
{
return false;
}
std::vector<BannedPlayerEntry> updatedEntries;
if (!SnapshotBannedPlayers(&updatedEntries))
{
return false;
}
size_t oldSize = updatedEntries.size();
updatedEntries.erase(
std::remove_if(
updatedEntries.begin(),
updatedEntries.end(),
[&normalized](const BannedPlayerEntry &entry) { return entry.xuid == normalized; }),
updatedEntries.end());
if (updatedEntries.size() == oldSize)
{
return false;
}
if (!SavePlayers(updatedEntries))
{
return false;
}
m_bannedPlayers.swap(updatedEntries);
return true;
}
bool BanManager::RemoveIpBan(const std::string &ip)
{
const std::string normalized = NetworkUtils::NormalizeIpToken(ip);
if (normalized.empty())
{
return false;
}
std::vector<BannedIpEntry> updatedEntries;
if (!SnapshotBannedIps(&updatedEntries))
{
return false;
}
size_t oldSize = updatedEntries.size();
updatedEntries.erase(
std::remove_if(
updatedEntries.begin(),
updatedEntries.end(),
[&normalized](const BannedIpEntry &entry) { return entry.ip == normalized; }),
updatedEntries.end());
if (updatedEntries.size() == oldSize)
{
return false;
}
if (!SaveIps(updatedEntries))
{
return false;
}
m_bannedIps.swap(updatedEntries);
return true;
}
std::string BanManager::GetBannedPlayersFilePath() const
{
return BuildPath(kBannedPlayersFileName);
}
std::string BanManager::GetBannedIpsFilePath() const
{
return BuildPath(kBannedIpsFileName);
}
BanMetadata BanManager::BuildDefaultMetadata(const char *source)
{
BanMetadata metadata;
metadata.created = StringUtils::GetCurrentUtcTimestampIso8601();
metadata.source = (source != nullptr) ? source : "Server";
metadata.expires = "";
metadata.reason = "";
return metadata;
}
void BanManager::NormalizeMetadata(BanMetadata *metadata)
{
if (metadata == nullptr)
{
return;
}
metadata->created = StringUtils::TrimAscii(metadata->created);
metadata->source = StringUtils::TrimAscii(metadata->source);
metadata->expires = StringUtils::TrimAscii(metadata->expires);
metadata->reason = StringUtils::TrimAscii(metadata->reason);
}
std::string BanManager::BuildPath(const char *fileName) const
{
return AccessStorageUtils::BuildPathFromBaseDirectory(m_baseDirectory, fileName);
}
}
}

View file

@ -0,0 +1,106 @@
#pragma once
#include <string>
#include <vector>
namespace ServerRuntime
{
namespace Access
{
/**
* Information shared with player bans and IP bans
* BANとIP BANで共有する情報
*/
struct BanMetadata
{
std::string created;
std::string source;
std::string expires;
std::string reason;
};
struct BannedPlayerEntry
{
std::string xuid;
std::string name;
BanMetadata metadata;
};
struct BannedIpEntry
{
std::string ip;
BanMetadata metadata;
};
/**
* Dedicated server BAN file manager.
*
* Files:
* - banned-players.json
* - banned-ips.json
*
* This class only handles storage/caching.
* Connection-time hooks are wired separately.
*/
class BanManager
{
public:
/**
* **Create Ban Manager**
*
* Binds the manager to the directory that stores the dedicated-server access files
* Dedicated Server
*/
explicit BanManager(const std::string &baseDirectory = ".");
/**
* Creates empty JSON array files when the dedicated server starts without persisted access data
* BANファイルが無い初回起動時に空JSONを用意する
*/
bool EnsureBanFilesExist() const;
bool Reload();
bool Save() const;
bool LoadPlayers(std::vector<BannedPlayerEntry> *outEntries) const;
bool LoadIps(std::vector<BannedIpEntry> *outEntries) const;
bool SavePlayers(const std::vector<BannedPlayerEntry> &entries) const;
bool SaveIps(const std::vector<BannedIpEntry> &entries) const;
const std::vector<BannedPlayerEntry> &GetBannedPlayers() const;
const std::vector<BannedIpEntry> &GetBannedIps() const;
/**
* Copies only currently active player BAN entries so expired metadata does not leak into command output
* BAN一覧を複製取得する
*/
bool SnapshotBannedPlayers(std::vector<BannedPlayerEntry> *outEntries) const;
/**
* Copies only currently active IP BAN entries so expired metadata does not leak into command output
* IP BAN一覧を複製取得する
*/
bool SnapshotBannedIps(std::vector<BannedIpEntry> *outEntries) const;
bool IsPlayerBannedByXuid(const std::string &xuid) const;
bool IsIpBanned(const std::string &ip) const;
bool AddPlayerBan(const BannedPlayerEntry &entry);
bool AddIpBan(const BannedIpEntry &entry);
bool RemovePlayerBanByXuid(const std::string &xuid);
bool RemoveIpBan(const std::string &ip);
std::string GetBannedPlayersFilePath() const;
std::string GetBannedIpsFilePath() const;
static BanMetadata BuildDefaultMetadata(const char *source = "Server");
private:
static void NormalizeMetadata(BanMetadata *metadata);
std::string BuildPath(const char *fileName) const;
private:
std::string m_baseDirectory;
std::vector<BannedPlayerEntry> m_bannedPlayers;
std::vector<BannedIpEntry> m_bannedIps;
};
}
}

View file

@ -0,0 +1,297 @@
#include "stdafx.h"
#include "WhitelistManager.h"
#include "..\Common\AccessStorageUtils.h"
#include "..\Common\FileUtils.h"
#include "..\Common\StringUtils.h"
#include "..\ServerLogger.h"
#include "..\vendor\nlohmann\json.hpp"
#include <algorithm>
namespace ServerRuntime
{
namespace Access
{
using OrderedJson = nlohmann::ordered_json;
namespace
{
static const char *kWhitelistFileName = "whitelist.json";
}
WhitelistManager::WhitelistManager(const std::string &baseDirectory)
: m_baseDirectory(baseDirectory.empty() ? "." : baseDirectory)
{
}
bool WhitelistManager::EnsureWhitelistFileExists() const
{
const std::string path = GetWhitelistFilePath();
if (!AccessStorageUtils::EnsureJsonListFileExists(path))
{
LogErrorf("access", "failed to create %s", path.c_str());
return false;
}
return true;
}
bool WhitelistManager::Reload()
{
std::vector<WhitelistedPlayerEntry> players;
if (!LoadPlayers(&players))
{
return false;
}
m_whitelistedPlayers.swap(players);
return true;
}
bool WhitelistManager::Save() const
{
std::vector<WhitelistedPlayerEntry> players;
return SnapshotWhitelistedPlayers(&players) && SavePlayers(players);
}
bool WhitelistManager::LoadPlayers(std::vector<WhitelistedPlayerEntry> *outEntries) const
{
if (outEntries == nullptr)
{
return false;
}
outEntries->clear();
std::string text;
const std::string path = GetWhitelistFilePath();
if (!FileUtils::ReadTextFile(path, &text))
{
LogErrorf("access", "failed to read %s", path.c_str());
return false;
}
if (text.empty())
{
text = "[]";
}
OrderedJson root;
try
{
root = OrderedJson::parse(StringUtils::StripUtf8Bom(text));
}
catch (const nlohmann::json::exception &e)
{
LogErrorf("access", "failed to parse %s: %s", path.c_str(), e.what());
return false;
}
if (!root.is_array())
{
LogErrorf("access", "failed to parse %s: root json value is not an array", path.c_str());
return false;
}
for (const auto &object : root)
{
if (!object.is_object())
{
LogWarnf("access", "skipping whitelist entry that is not an object in %s", path.c_str());
continue;
}
std::string rawXuid;
if (!AccessStorageUtils::TryGetStringField(object, "xuid", &rawXuid))
{
LogWarnf("access", "skipping whitelist entry without xuid in %s", path.c_str());
continue;
}
WhitelistedPlayerEntry entry;
entry.xuid = AccessStorageUtils::NormalizeXuid(rawXuid);
if (entry.xuid.empty())
{
LogWarnf("access", "skipping whitelist entry with empty xuid in %s", path.c_str());
continue;
}
AccessStorageUtils::TryGetStringField(object, "name", &entry.name);
AccessStorageUtils::TryGetStringField(object, "created", &entry.metadata.created);
AccessStorageUtils::TryGetStringField(object, "source", &entry.metadata.source);
NormalizeMetadata(&entry.metadata);
outEntries->push_back(entry);
}
return true;
}
bool WhitelistManager::SavePlayers(const std::vector<WhitelistedPlayerEntry> &entries) const
{
OrderedJson root = OrderedJson::array();
for (const auto &entry : entries)
{
OrderedJson object = OrderedJson::object();
object["xuid"] = AccessStorageUtils::NormalizeXuid(entry.xuid);
object["name"] = entry.name;
object["created"] = entry.metadata.created;
object["source"] = entry.metadata.source;
root.push_back(object);
}
const std::string path = GetWhitelistFilePath();
const std::string json = root.empty() ? std::string("[]\n") : (root.dump(2) + "\n");
if (!FileUtils::WriteTextFileAtomic(path, json))
{
LogErrorf("access", "failed to write %s", path.c_str());
return false;
}
return true;
}
const std::vector<WhitelistedPlayerEntry> &WhitelistManager::GetWhitelistedPlayers() const
{
return m_whitelistedPlayers;
}
bool WhitelistManager::SnapshotWhitelistedPlayers(std::vector<WhitelistedPlayerEntry> *outEntries) const
{
if (outEntries == nullptr)
{
return false;
}
*outEntries = m_whitelistedPlayers;
return true;
}
bool WhitelistManager::IsPlayerWhitelistedByXuid(const std::string &xuid) const
{
const auto normalized = AccessStorageUtils::NormalizeXuid(xuid);
if (normalized.empty())
{
return false;
}
return std::any_of(
m_whitelistedPlayers.begin(),
m_whitelistedPlayers.end(),
[&normalized](const WhitelistedPlayerEntry &entry)
{
return entry.xuid == normalized;
});
}
bool WhitelistManager::AddPlayer(const WhitelistedPlayerEntry &entry)
{
std::vector<WhitelistedPlayerEntry> updatedEntries;
if (!SnapshotWhitelistedPlayers(&updatedEntries))
{
return false;
}
auto normalized = entry;
normalized.xuid = AccessStorageUtils::NormalizeXuid(normalized.xuid);
NormalizeMetadata(&normalized.metadata);
if (normalized.xuid.empty())
{
return false;
}
const auto existing = std::find_if(
updatedEntries.begin(),
updatedEntries.end(),
[&normalized](const WhitelistedPlayerEntry &candidate)
{
return candidate.xuid == normalized.xuid;
});
if (existing != updatedEntries.end())
{
*existing = normalized;
if (!SavePlayers(updatedEntries))
{
return false;
}
m_whitelistedPlayers.swap(updatedEntries);
return true;
}
updatedEntries.push_back(normalized);
if (!SavePlayers(updatedEntries))
{
return false;
}
m_whitelistedPlayers.swap(updatedEntries);
return true;
}
bool WhitelistManager::RemovePlayerByXuid(const std::string &xuid)
{
const auto normalized = AccessStorageUtils::NormalizeXuid(xuid);
if (normalized.empty())
{
return false;
}
std::vector<WhitelistedPlayerEntry> updatedEntries;
if (!SnapshotWhitelistedPlayers(&updatedEntries))
{
return false;
}
const auto oldSize = updatedEntries.size();
updatedEntries.erase(
std::remove_if(
updatedEntries.begin(),
updatedEntries.end(),
[&normalized](const WhitelistedPlayerEntry &entry) { return entry.xuid == normalized; }),
updatedEntries.end());
if (updatedEntries.size() == oldSize)
{
return false;
}
if (!SavePlayers(updatedEntries))
{
return false;
}
m_whitelistedPlayers.swap(updatedEntries);
return true;
}
std::string WhitelistManager::GetWhitelistFilePath() const
{
return BuildPath(kWhitelistFileName);
}
WhitelistMetadata WhitelistManager::BuildDefaultMetadata(const char *source)
{
WhitelistMetadata metadata;
metadata.created = StringUtils::GetCurrentUtcTimestampIso8601();
metadata.source = (source != nullptr) ? source : "Server";
return metadata;
}
void WhitelistManager::NormalizeMetadata(WhitelistMetadata *metadata)
{
if (metadata == nullptr)
{
return;
}
metadata->created = StringUtils::TrimAscii(metadata->created);
metadata->source = StringUtils::TrimAscii(metadata->source);
}
std::string WhitelistManager::BuildPath(const char *fileName) const
{
return AccessStorageUtils::BuildPathFromBaseDirectory(m_baseDirectory, fileName);
}
}
}

View file

@ -0,0 +1,64 @@
#pragma once
#include <string>
#include <vector>
namespace ServerRuntime
{
namespace Access
{
struct WhitelistMetadata
{
std::string created;
std::string source;
};
struct WhitelistedPlayerEntry
{
std::string xuid;
std::string name;
WhitelistMetadata metadata;
};
/**
* whitelist manager
*
* Files:
* - whitelist.json
*
* Stores and normalizes XUID-based allow entries.
*/
class WhitelistManager
{
public:
explicit WhitelistManager(const std::string &baseDirectory = ".");
bool EnsureWhitelistFileExists() const;
bool Reload();
bool Save() const;
bool LoadPlayers(std::vector<WhitelistedPlayerEntry> *outEntries) const;
bool SavePlayers(const std::vector<WhitelistedPlayerEntry> &entries) const;
const std::vector<WhitelistedPlayerEntry> &GetWhitelistedPlayers() const;
bool SnapshotWhitelistedPlayers(std::vector<WhitelistedPlayerEntry> *outEntries) const;
bool IsPlayerWhitelistedByXuid(const std::string &xuid) const;
bool AddPlayer(const WhitelistedPlayerEntry &entry);
bool RemovePlayerByXuid(const std::string &xuid);
std::string GetWhitelistFilePath() const;
static WhitelistMetadata BuildDefaultMetadata(const char *source = "Server");
private:
static void NormalizeMetadata(WhitelistMetadata *metadata);
std::string BuildPath(const char *fileName) const;
private:
std::string m_baseDirectory;
std::vector<WhitelistedPlayerEntry> m_whitelistedPlayers;
};
}
}

View file

@ -0,0 +1,105 @@
#pragma once
#include "FileUtils.h"
#include "StringUtils.h"
#include "..\vendor\nlohmann\json.hpp"
#include <stdio.h>
namespace ServerRuntime
{
namespace AccessStorageUtils
{
inline bool IsRegularFile(const std::string &path)
{
const std::wstring widePath = StringUtils::Utf8ToWide(path);
if (widePath.empty())
{
return false;
}
const DWORD attributes = GetFileAttributesW(widePath.c_str());
return (attributes != INVALID_FILE_ATTRIBUTES) && ((attributes & FILE_ATTRIBUTE_DIRECTORY) == 0);
}
inline bool EnsureJsonListFileExists(const std::string &path)
{
return IsRegularFile(path) || FileUtils::WriteTextFileAtomic(path, "[]\n");
}
inline bool TryGetStringField(const nlohmann::ordered_json &object, const char *key, std::string *outValue)
{
if (key == nullptr || outValue == nullptr || !object.is_object())
{
return false;
}
const auto it = object.find(key);
if (it == object.end() || !it->is_string())
{
return false;
}
*outValue = it->get<std::string>();
return true;
}
inline std::string NormalizeXuid(const std::string &xuid)
{
const std::string trimmed = StringUtils::TrimAscii(xuid);
if (trimmed.empty())
{
return "";
}
unsigned long long numericXuid = 0;
if (StringUtils::TryParseUnsignedLongLong(trimmed, &numericXuid))
{
if (numericXuid == 0ULL)
{
return "";
}
char buffer[32] = {};
sprintf_s(buffer, sizeof(buffer), "0x%016llx", numericXuid);
return buffer;
}
return StringUtils::ToLowerAscii(trimmed);
}
inline std::string BuildPathFromBaseDirectory(const std::string &baseDirectory, const char *fileName)
{
if (fileName == nullptr || fileName[0] == 0)
{
return "";
}
const std::wstring wideFileName = StringUtils::Utf8ToWide(fileName);
if (wideFileName.empty())
{
return "";
}
if (baseDirectory.empty() || baseDirectory == ".")
{
return StringUtils::WideToUtf8(wideFileName);
}
const std::wstring wideBaseDirectory = StringUtils::Utf8ToWide(baseDirectory);
if (wideBaseDirectory.empty())
{
return StringUtils::WideToUtf8(wideFileName);
}
const wchar_t last = wideBaseDirectory[wideBaseDirectory.size() - 1];
if (last == L'\\' || last == L'/')
{
return StringUtils::WideToUtf8(wideBaseDirectory + wideFileName);
}
return StringUtils::WideToUtf8(wideBaseDirectory + L"\\" + wideFileName);
}
}
}

View file

@ -0,0 +1,146 @@
#include "stdafx.h"
#include "FileUtils.h"
#include "StringUtils.h"
#include <io.h>
#include <stdio.h>
namespace ServerRuntime
{
namespace FileUtils
{
namespace
{
static std::wstring ToWidePath(const std::string &filePath)
{
return StringUtils::Utf8ToWide(filePath);
}
}
unsigned long long GetCurrentUtcFileTime()
{
FILETIME now = {};
GetSystemTimeAsFileTime(&now);
ULARGE_INTEGER value = {};
value.LowPart = now.dwLowDateTime;
value.HighPart = now.dwHighDateTime;
return value.QuadPart;
}
bool ReadTextFile(const std::string &filePath, std::string *outText)
{
if (outText == nullptr)
{
return false;
}
outText->clear();
const std::wstring widePath = ToWidePath(filePath);
if (widePath.empty())
{
return false;
}
FILE *inFile = nullptr;
if (_wfopen_s(&inFile, widePath.c_str(), L"rb") != 0 || inFile == nullptr)
{
return false;
}
if (fseek(inFile, 0, SEEK_END) != 0)
{
fclose(inFile);
return false;
}
long fileSize = ftell(inFile);
if (fileSize < 0)
{
fclose(inFile);
return false;
}
if (fseek(inFile, 0, SEEK_SET) != 0)
{
fclose(inFile);
return false;
}
if (fileSize == 0)
{
fclose(inFile);
return true;
}
outText->resize((size_t)fileSize);
size_t bytesRead = fread(&(*outText)[0], 1, (size_t)fileSize, inFile);
fclose(inFile);
if (bytesRead != (size_t)fileSize)
{
outText->clear();
return false;
}
return true;
}
bool WriteTextFileAtomic(const std::string &filePath, const std::string &text)
{
const std::wstring widePath = ToWidePath(filePath);
if (widePath.empty())
{
return false;
}
const std::wstring tmpPath = widePath + L".tmp";
FILE *outFile = nullptr;
if (_wfopen_s(&outFile, tmpPath.c_str(), L"wb") != 0 || outFile == nullptr)
{
return false;
}
if (!text.empty())
{
size_t bytesWritten = fwrite(text.data(), 1, text.size(), outFile);
if (bytesWritten != text.size())
{
fclose(outFile);
DeleteFileW(tmpPath.c_str());
return false;
}
}
if (fflush(outFile) != 0 || _commit(_fileno(outFile)) != 0)
{
fclose(outFile);
DeleteFileW(tmpPath.c_str());
return false;
}
fclose(outFile);
DWORD attrs = GetFileAttributesW(widePath.c_str());
if (attrs != INVALID_FILE_ATTRIBUTES && ((attrs & FILE_ATTRIBUTE_DIRECTORY) == 0))
{
// Replace the destination without deleting the last known-good file first.
if (ReplaceFileW(widePath.c_str(), tmpPath.c_str(), nullptr, REPLACEFILE_IGNORE_MERGE_ERRORS, nullptr, nullptr))
{
return true;
}
}
if (MoveFileExW(tmpPath.c_str(), widePath.c_str(), MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH))
{
return true;
}
// Keep the temp file on failure so the original file remains recoverable and the caller can inspect the write result.
return false;
}
}
}

View file

@ -0,0 +1,25 @@
#pragma once
#include <string>
namespace ServerRuntime
{
namespace FileUtils
{
/**
* Reads the full UTF-8 path target into memory without interpreting JSON or line endings
* UTF-8
*/
bool ReadTextFile(const std::string &filePath, std::string *outText);
/**
* Writes text through a same-directory temporary file and publishes it with a single replacement step
*
*/
bool WriteTextFileAtomic(const std::string &filePath, const std::string &text);
/**
* Returns the current UTC timestamp encoded in Windows FILETIME units for expiry comparisons
* UTC時刻をWindows FILETIME単位で返す
*/
unsigned long long GetCurrentUtcFileTime();
}
}

View file

@ -0,0 +1,29 @@
#pragma once
#include "StringUtils.h"
#include <WS2tcpip.h>
namespace ServerRuntime
{
namespace NetworkUtils
{
inline std::string NormalizeIpToken(const std::string &ip)
{
return StringUtils::ToLowerAscii(StringUtils::TrimAscii(ip));
}
inline bool IsIpLiteral(const std::string &text)
{
const std::string trimmed = StringUtils::TrimAscii(text);
if (trimmed.empty())
{
return false;
}
IN_ADDR ipv4 = {};
IN6_ADDR ipv6 = {};
return InetPtonA(AF_INET, trimmed.c_str(), &ipv4) == 1 || InetPtonA(AF_INET6, trimmed.c_str(), &ipv6) == 1;
}
}
}

View file

@ -0,0 +1,212 @@
#include "stdafx.h"
#include "StringUtils.h"
#include <cctype>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
namespace ServerRuntime
{
namespace StringUtils
{
std::string WideToUtf8(const std::wstring &value)
{
if (value.empty())
{
return std::string();
}
int charCount = WideCharToMultiByte(CP_UTF8, 0, value.c_str(), (int)value.length(), NULL, 0, NULL, NULL);
if (charCount <= 0)
{
return std::string();
}
std::string utf8;
utf8.resize(charCount);
WideCharToMultiByte(CP_UTF8, 0, value.c_str(), (int)value.length(), &utf8[0], charCount, NULL, NULL);
return utf8;
}
std::wstring Utf8ToWide(const char *value)
{
if (value == NULL || value[0] == 0)
{
return std::wstring();
}
int wideCount = MultiByteToWideChar(CP_UTF8, 0, value, -1, NULL, 0);
if (wideCount <= 0)
{
// Fall back to the current ANSI code page so legacy non-UTF-8 inputs remain readable.
wideCount = MultiByteToWideChar(CP_ACP, 0, value, -1, NULL, 0);
if (wideCount <= 0)
{
return std::wstring();
}
std::wstring wide;
wide.resize(wideCount - 1);
MultiByteToWideChar(CP_ACP, 0, value, -1, &wide[0], wideCount);
return wide;
}
std::wstring wide;
wide.resize(wideCount - 1);
MultiByteToWideChar(CP_UTF8, 0, value, -1, &wide[0], wideCount);
return wide;
}
std::wstring Utf8ToWide(const std::string &value)
{
return Utf8ToWide(value.c_str());
}
std::string StripUtf8Bom(const std::string &value)
{
if (value.size() >= 3 &&
(unsigned char)value[0] == 0xEF &&
(unsigned char)value[1] == 0xBB &&
(unsigned char)value[2] == 0xBF)
{
return value.substr(3);
}
return value;
}
std::string TrimAscii(const std::string &value)
{
size_t start = 0;
while (start < value.length() && std::isspace((unsigned char)value[start]))
{
++start;
}
size_t end = value.length();
while (end > start && std::isspace((unsigned char)value[end - 1]))
{
--end;
}
return value.substr(start, end - start);
}
std::string ToLowerAscii(const std::string &value)
{
std::string lowered = value;
for (size_t i = 0; i < lowered.length(); ++i)
{
lowered[i] = (char)std::tolower((unsigned char)lowered[i]);
}
return lowered;
}
std::string JoinTokens(const std::vector<std::string> &tokens, size_t startIndex, const char *separator)
{
if (startIndex >= tokens.size())
{
return std::string();
}
const auto joinSeparator = std::string((separator != nullptr) ? separator : " ");
size_t totalLength = 0;
for (size_t i = startIndex; i < tokens.size(); ++i)
{
totalLength += tokens[i].size();
}
totalLength += (tokens.size() - startIndex - 1) * joinSeparator.size();
std::string joined;
joined.reserve(totalLength);
for (size_t i = startIndex; i < tokens.size(); ++i)
{
if (!joined.empty())
{
joined += joinSeparator;
}
joined += tokens[i];
}
return joined;
}
bool StartsWithIgnoreCase(const std::string &value, const std::string &prefix)
{
if (prefix.size() > value.size())
{
return false;
}
for (size_t i = 0; i < prefix.size(); ++i)
{
unsigned char a = (unsigned char)value[i];
unsigned char b = (unsigned char)prefix[i];
if (std::tolower(a) != std::tolower(b))
{
return false;
}
}
return true;
}
bool TryParseUnsignedLongLong(const std::string &value, unsigned long long *outValue)
{
if (outValue == nullptr)
{
return false;
}
const std::string trimmed = TrimAscii(value);
if (trimmed.empty())
{
return false;
}
errno = 0;
char *end = nullptr;
const unsigned long long parsed = _strtoui64(trimmed.c_str(), &end, 0);
if (end == trimmed.c_str() || errno != 0)
{
return false;
}
while (*end == ' ' || *end == '\t' || *end == '\r' || *end == '\n')
{
++end;
}
if (*end != 0)
{
return false;
}
*outValue = parsed;
return true;
}
std::string GetCurrentUtcTimestampIso8601()
{
SYSTEMTIME utc = {};
GetSystemTime(&utc);
char created[64] = {};
sprintf_s(
created,
sizeof(created),
"%04u-%02u-%02uT%02u:%02u:%02uZ",
(unsigned)utc.wYear,
(unsigned)utc.wMonth,
(unsigned)utc.wDay,
(unsigned)utc.wHour,
(unsigned)utc.wMinute,
(unsigned)utc.wSecond);
return created;
}
}
}

View file

@ -0,0 +1,23 @@
#pragma once
#include <string>
#include <vector>
namespace ServerRuntime
{
namespace StringUtils
{
std::string WideToUtf8(const std::wstring &value);
std::wstring Utf8ToWide(const char *value);
std::wstring Utf8ToWide(const std::string &value);
std::string StripUtf8Bom(const std::string &value);
std::string TrimAscii(const std::string &value);
std::string ToLowerAscii(const std::string &value);
std::string JoinTokens(const std::vector<std::string> &tokens, size_t startIndex = 0, const char *separator = " ");
bool StartsWithIgnoreCase(const std::string &value, const std::string &prefix);
bool TryParseUnsignedLongLong(const std::string &value, unsigned long long *outValue);
std::string GetCurrentUtcTimestampIso8601();
}
}

View file

@ -0,0 +1,44 @@
#include "stdafx.h"
#include "ServerCli.h"
#include "ServerCliEngine.h"
#include "ServerCliInput.h"
namespace ServerRuntime
{
ServerCli::ServerCli()
: m_engine(new ServerCliEngine())
, m_input(new ServerCliInput())
{
}
ServerCli::~ServerCli()
{
Stop();
}
void ServerCli::Start()
{
if (m_input && m_engine)
{
m_input->Start(m_engine.get());
}
}
void ServerCli::Stop()
{
if (m_input)
{
m_input->Stop();
}
}
void ServerCli::Poll()
{
if (m_engine)
{
m_engine->Poll();
}
}
}

View file

@ -0,0 +1,50 @@
#pragma once
#include <memory>
namespace ServerRuntime
{
class ServerCliEngine;
class ServerCliInput;
/**
* **Server CLI facade**
*
* Owns the command engine and input component, and exposes a small lifecycle API.
* CLI
*/
class ServerCli
{
public:
ServerCli();
~ServerCli();
/**
* **Start console input processing**
*
* Connects input to the engine and starts background reading.
*
*/
void Start();
/**
* **Stop console input processing**
*
* Stops background input safely and detaches from the engine.
*
*/
void Stop();
/**
* **Process queued command lines**
*
* Drains commands collected by input and executes them in the main loop.
*
*/
void Poll();
private:
std::unique_ptr<ServerCliEngine> m_engine;
std::unique_ptr<ServerCliInput> m_input;
};
}

View file

@ -0,0 +1,395 @@
#include "stdafx.h"
#include "ServerCliEngine.h"
#include "ServerCliParser.h"
#include "ServerCliRegistry.h"
#include "commands\IServerCliCommand.h"
#include "commands\ban\CliCommandBan.h"
#include "commands\ban-ip\CliCommandBanIp.h"
#include "commands\ban-list\CliCommandBanList.h"
#include "commands\defaultgamemode\CliCommandDefaultGamemode.h"
#include "commands\enchant\CliCommandEnchant.h"
#include "commands\experience\CliCommandExperience.h"
#include "commands\gamemode\CliCommandGamemode.h"
#include "commands\give\CliCommandGive.h"
#include "commands\help\CliCommandHelp.h"
#include "commands\kill\CliCommandKill.h"
#include "commands\list\CliCommandList.h"
#include "commands\pardon\CliCommandPardon.h"
#include "commands\pardon-ip\CliCommandPardonIp.h"
#include "commands\stop\CliCommandStop.h"
#include "commands\time\CliCommandTime.h"
#include "commands\tp\CliCommandTp.h"
#include "commands\weather\CliCommandWeather.h"
#include "commands\whitelist\CliCommandWhitelist.h"
#include "..\Common\StringUtils.h"
#include "..\ServerShutdown.h"
#include "..\ServerLogger.h"
#include "..\..\Minecraft.Client\MinecraftServer.h"
#include "..\..\Minecraft.Client\PlayerList.h"
#include "..\..\Minecraft.Client\ServerPlayer.h"
#include "..\..\Minecraft.World\CommandDispatcher.h"
#include "..\..\Minecraft.World\CommandSender.h"
#include "..\..\Minecraft.World\LevelSettings.h"
#include "..\..\Minecraft.World\StringHelpers.h"
#include <stdlib.h>
#include <unordered_set>
namespace ServerRuntime
{
/**
* Create an authorized Sender to make the CLI appear as a user.
* The return value can also be used to display logs.
*/
namespace
{
class ServerCliConsoleCommandSender : public CommandSender
{
public:
explicit ServerCliConsoleCommandSender(const ServerCliEngine *engine)
: m_engine(engine)
{
}
void sendMessage(const wstring &message, ChatPacket::EChatPacketMessage type, int customData, const wstring &additionalMessage) override
{
(void)type;
(void)customData;
(void)additionalMessage;
if (m_engine == nullptr)
{
return;
}
m_engine->LogInfo(StringUtils::WideToUtf8(message));
}
bool hasPermission(EGameCommand command) override
{
(void)command;
return true;
}
private:
const ServerCliEngine *m_engine;
};
}
ServerCliEngine::ServerCliEngine()
: m_registry(new ServerCliRegistry())
, m_consoleSender(std::make_shared<ServerCliConsoleCommandSender>(this))
{
RegisterDefaultCommands();
}
ServerCliEngine::~ServerCliEngine()
{
}
void ServerCliEngine::RegisterDefaultCommands()
{
m_registry->Register(std::unique_ptr<IServerCliCommand>(new CliCommandHelp()));
m_registry->Register(std::unique_ptr<IServerCliCommand>(new CliCommandStop()));
m_registry->Register(std::unique_ptr<IServerCliCommand>(new CliCommandList()));
m_registry->Register(std::unique_ptr<IServerCliCommand>(new CliCommandBan()));
m_registry->Register(std::unique_ptr<IServerCliCommand>(new CliCommandBanIp()));
m_registry->Register(std::unique_ptr<IServerCliCommand>(new CliCommandPardon()));
m_registry->Register(std::unique_ptr<IServerCliCommand>(new CliCommandPardonIp()));
m_registry->Register(std::unique_ptr<IServerCliCommand>(new CliCommandBanList()));
m_registry->Register(std::unique_ptr<IServerCliCommand>(new CliCommandWhitelist()));
m_registry->Register(std::unique_ptr<IServerCliCommand>(new CliCommandTp()));
m_registry->Register(std::unique_ptr<IServerCliCommand>(new CliCommandTime()));
m_registry->Register(std::unique_ptr<IServerCliCommand>(new CliCommandWeather()));
m_registry->Register(std::unique_ptr<IServerCliCommand>(new CliCommandGive()));
m_registry->Register(std::unique_ptr<IServerCliCommand>(new CliCommandEnchant()));
m_registry->Register(std::unique_ptr<IServerCliCommand>(new CliCommandKill()));
m_registry->Register(std::unique_ptr<IServerCliCommand>(new CliCommandGamemode()));
m_registry->Register(std::unique_ptr<IServerCliCommand>(new CliCommandDefaultGamemode()));
m_registry->Register(std::unique_ptr<IServerCliCommand>(new CliCommandExperience()));
}
void ServerCliEngine::EnqueueCommandLine(const std::string &line)
{
std::lock_guard<std::mutex> lock(m_queueMutex);
m_pendingLines.push(line);
}
void ServerCliEngine::Poll()
{
for (;;)
{
std::string line;
{
// Keep the lock scope minimal: dequeue only, execute outside.
std::lock_guard<std::mutex> lock(m_queueMutex);
if (m_pendingLines.empty())
{
break;
}
line = m_pendingLines.front();
m_pendingLines.pop();
}
ExecuteCommandLine(line);
}
}
bool ServerCliEngine::ExecuteCommandLine(const std::string &line)
{
// Normalize user input before parsing (trim + optional leading slash).
std::wstring wide = trimString(StringUtils::Utf8ToWide(line));
if (wide.empty())
{
return true;
}
std::string normalizedLine = StringUtils::WideToUtf8(wide);
if (!normalizedLine.empty() && normalizedLine[0] == '/')
{
normalizedLine = normalizedLine.substr(1);
}
ServerCliParsedLine parsed = ServerCliParser::Parse(normalizedLine);
if (parsed.tokens.empty())
{
return true;
}
IServerCliCommand *command = m_registry->FindMutable(parsed.tokens[0]);
if (command == NULL)
{
LogWarn("Unknown command: " + parsed.tokens[0]);
return false;
}
return command->Execute(parsed, this);
}
void ServerCliEngine::BuildCompletions(const std::string &line, std::vector<std::string> *out) const
{
if (out == NULL)
{
return;
}
out->clear();
ServerCliCompletionContext context = ServerCliParser::BuildCompletionContext(line);
bool slashPrefixedCommand = false;
std::string commandToken;
if (!context.parsed.tokens.empty())
{
// Completion accepts both "tp" and "/tp" style command heads.
commandToken = context.parsed.tokens[0];
if (!commandToken.empty() && commandToken[0] == '/')
{
commandToken = commandToken.substr(1);
slashPrefixedCommand = true;
}
}
if (context.currentTokenIndex == 0)
{
std::string prefix = context.prefix;
if (!prefix.empty() && prefix[0] == '/')
{
prefix = prefix.substr(1);
slashPrefixedCommand = true;
}
std::string linePrefix = context.linePrefix;
if (slashPrefixedCommand && linePrefix.empty())
{
// Preserve leading slash when user started with "/".
linePrefix = "/";
}
m_registry->SuggestCommandNames(prefix, linePrefix, out);
}
else
{
const IServerCliCommand *command = m_registry->Find(commandToken);
if (command != NULL)
{
command->Complete(context, this, out);
}
}
std::unordered_set<std::string> seen;
std::vector<std::string> unique;
for (size_t i = 0; i < out->size(); ++i)
{
// Remove duplicates while keeping first-seen ordering.
if (seen.insert((*out)[i]).second)
{
unique.push_back((*out)[i]);
}
}
out->swap(unique);
}
void ServerCliEngine::LogInfo(const std::string &message) const
{
LogInfof("console", "%s", message.c_str());
}
void ServerCliEngine::LogWarn(const std::string &message) const
{
LogWarnf("console", "%s", message.c_str());
}
void ServerCliEngine::LogError(const std::string &message) const
{
LogErrorf("console", "%s", message.c_str());
}
void ServerCliEngine::RequestShutdown() const
{
RequestDedicatedServerShutdown();
}
std::vector<std::string> ServerCliEngine::GetOnlinePlayerNamesUtf8() const
{
std::vector<std::string> result;
MinecraftServer *server = MinecraftServer::getInstance();
if (server == NULL)
{
return result;
}
PlayerList *players = server->getPlayers();
if (players == NULL)
{
return result;
}
for (size_t i = 0; i < players->players.size(); ++i)
{
std::shared_ptr<ServerPlayer> player = players->players[i];
if (player != NULL)
{
result.push_back(StringUtils::WideToUtf8(player->getName()));
}
}
return result;
}
std::shared_ptr<ServerPlayer> ServerCliEngine::FindPlayerByNameUtf8(const std::string &name) const
{
MinecraftServer *server = MinecraftServer::getInstance();
if (server == NULL)
{
return nullptr;
}
PlayerList *players = server->getPlayers();
if (players == NULL)
{
return nullptr;
}
std::wstring target = StringUtils::Utf8ToWide(name);
for (size_t i = 0; i < players->players.size(); ++i)
{
std::shared_ptr<ServerPlayer> player = players->players[i];
if (player != NULL && equalsIgnoreCase(player->getName(), target))
{
return player;
}
}
return nullptr;
}
void ServerCliEngine::SuggestPlayers(const std::string &prefix, const std::string &linePrefix, std::vector<std::string> *out) const
{
std::vector<std::string> players = GetOnlinePlayerNamesUtf8();
std::string loweredPrefix = StringUtils::ToLowerAscii(prefix);
for (size_t i = 0; i < players.size(); ++i)
{
std::string loweredName = StringUtils::ToLowerAscii(players[i]);
if (loweredName.compare(0, loweredPrefix.size(), loweredPrefix) == 0)
{
out->push_back(linePrefix + players[i]);
}
}
}
void ServerCliEngine::SuggestGamemodes(const std::string &prefix, const std::string &linePrefix, std::vector<std::string> *out) const
{
static const char *kModes[] = { "survival", "creative", "s", "c", "0", "1" };
std::string loweredPrefix = StringUtils::ToLowerAscii(prefix);
for (size_t i = 0; i < sizeof(kModes) / sizeof(kModes[0]); ++i)
{
std::string candidate = kModes[i];
std::string loweredCandidate = StringUtils::ToLowerAscii(candidate);
if (loweredCandidate.compare(0, loweredPrefix.size(), loweredPrefix) == 0)
{
out->push_back(linePrefix + candidate);
}
}
}
GameType *ServerCliEngine::ParseGamemode(const std::string &token) const
{
std::string lowered = StringUtils::ToLowerAscii(token);
if (lowered == "survival" || lowered == "s" || lowered == "0")
{
return GameType::SURVIVAL;
}
if (lowered == "creative" || lowered == "c" || lowered == "1")
{
return GameType::CREATIVE;
}
char *end = NULL;
long id = strtol(lowered.c_str(), &end, 10);
if (end != NULL && *end == 0)
{
// Numeric fallback supports extended ids handled by level settings.
return LevelSettings::validateGameType((int)id);
}
return NULL;
}
bool ServerCliEngine::DispatchWorldCommand(EGameCommand command, byteArray commandData, const std::shared_ptr<CommandSender> &sender) const
{
MinecraftServer *server = MinecraftServer::getInstance();
if (server == NULL)
{
LogWarn("MinecraftServer instance is not available.");
return false;
}
CommandDispatcher *dispatcher = server->getCommandDispatcher();
if (dispatcher == NULL)
{
LogWarn("Command dispatcher is not available.");
return false;
}
std::shared_ptr<CommandSender> commandSender = sender;
if (commandSender == nullptr)
{
// fall back to console sender if caller did not provide one
commandSender = m_consoleSender;
}
if (commandSender == nullptr)
{
LogWarn("No command sender is available.");
return false;
}
dispatcher->performCommand(commandSender, command, commandData);
return true;
}
const ServerCliRegistry &ServerCliEngine::Registry() const
{
return *m_registry;
}
}

View file

@ -0,0 +1,127 @@
#pragma once
#include <memory>
#include <mutex>
#include <queue>
#include <string>
#include <vector>
#include "..\..\Minecraft.World\ArrayWithLength.h"
#include "..\..\Minecraft.World\CommandsEnum.h"
class GameType;
class ServerPlayer;
class CommandSender;
namespace ServerRuntime
{
class ServerCliRegistry;
/**
* **CLI execution engine**
*
* Handles parsing, command dispatch, completion suggestions, and server-side helpers.
*
*/
class ServerCliEngine
{
public:
ServerCliEngine();
~ServerCliEngine();
/**
* **Queue one raw command line**
*
* Called by input thread; execution is deferred to `Poll()`.
*
*/
void EnqueueCommandLine(const std::string &line);
/**
* **Execute queued commands**
*
* Drains pending lines and dispatches them in order.
*
*/
void Poll();
/**
* **Execute one command line immediately**
*
* Parses and dispatches a normalized line to a registered command.
* 1
*/
bool ExecuteCommandLine(const std::string &line);
/**
* **Build completion candidates for current line**
*
* Produces command or argument suggestions based on parser context.
*
*/
void BuildCompletions(const std::string &line, std::vector<std::string> *out) const;
void LogInfo(const std::string &message) const;
void LogWarn(const std::string &message) const;
void LogError(const std::string &message) const;
void RequestShutdown() const;
/**
* **List connected players as UTF-8 names**
*
*
*/
std::vector<std::string> GetOnlinePlayerNamesUtf8() const;
/**
* **Find a player by UTF-8 name**
*/
std::shared_ptr<ServerPlayer> FindPlayerByNameUtf8(const std::string &name) const;
/**
* **Suggest player-name arguments**
*
* Appends matching player candidates using the given completion prefix.
*
*/
void SuggestPlayers(const std::string &prefix, const std::string &linePrefix, std::vector<std::string> *out) const;
/**
* **Suggest gamemode arguments**
*
* Appends standard gamemode aliases (survival/creative/0/1).
*
*/
void SuggestGamemodes(const std::string &prefix, const std::string &linePrefix, std::vector<std::string> *out) const;
/**
* **Parse gamemode token**
*
* Supports names, short aliases, and numeric ids.
*
*/
GameType *ParseGamemode(const std::string &token) const;
/**
* **Dispatch one Minecraft.World game command**
*
* Uses `Minecraft.World::CommandDispatcher` for actual execution.
* When `sender` is null, an internal console command sender is used.
*
* Minecraft.Worldのコマンドを実行するためのディスパッチャーのラッパー
* senderがnullの場合はコンソールコマンド送信者を使用
*/
bool DispatchWorldCommand(EGameCommand command, byteArray commandData, const std::shared_ptr<CommandSender> &sender = nullptr) const;
const ServerCliRegistry &Registry() const;
private:
void RegisterDefaultCommands();
private:
mutable std::mutex m_queueMutex;
std::queue<std::string> m_pendingLines;
std::unique_ptr<ServerCliRegistry> m_registry;
std::shared_ptr<CommandSender> m_consoleSender;
};
}

View file

@ -0,0 +1,285 @@
#include "stdafx.h"
#include "ServerCliInput.h"
#include "ServerCliEngine.h"
#include "..\ServerLogger.h"
#include "..\vendor\linenoise\linenoise.h"
#include <ctype.h>
#include <stdlib.h>
namespace
{
bool UseStreamInputMode()
{
const char *mode = getenv("SERVER_CLI_INPUT_MODE");
if (mode != NULL)
{
return _stricmp(mode, "stream") == 0
|| _stricmp(mode, "stdin") == 0;
}
return false;
}
int WaitForStdinReadable(HANDLE stdinHandle, DWORD waitMs)
{
if (stdinHandle == NULL || stdinHandle == INVALID_HANDLE_VALUE)
{
return -1;
}
DWORD fileType = GetFileType(stdinHandle);
if (fileType == FILE_TYPE_PIPE)
{
DWORD available = 0;
if (!PeekNamedPipe(stdinHandle, NULL, 0, NULL, &available, NULL))
{
return -1;
}
return available > 0 ? 1 : 0;
}
if (fileType == FILE_TYPE_CHAR)
{
// console/pty char handles are often not waitable across Wine+Docker.
return 1;
}
DWORD waitResult = WaitForSingleObject(stdinHandle, waitMs);
if (waitResult == WAIT_OBJECT_0)
{
return 1;
}
if (waitResult == WAIT_TIMEOUT)
{
return 0;
}
return -1;
}
}
namespace ServerRuntime
{
// C-style completion callback bridge requires a static instance pointer.
ServerCliInput *ServerCliInput::s_instance = NULL;
ServerCliInput::ServerCliInput()
: m_running(false)
, m_engine(NULL)
{
}
ServerCliInput::~ServerCliInput()
{
Stop();
}
void ServerCliInput::Start(ServerCliEngine *engine)
{
if (engine == NULL || m_running.exchange(true))
{
return;
}
m_engine = engine;
s_instance = this;
linenoiseResetStop();
linenoiseHistorySetMaxLen(128);
linenoiseSetCompletionCallback(&ServerCliInput::CompletionThunk);
m_inputThread = std::thread(&ServerCliInput::RunInputLoop, this);
LogInfo("console", "CLI input thread started.");
}
void ServerCliInput::Stop()
{
if (!m_running.exchange(false))
{
return;
}
// Ask linenoise to break out first, then join thread safely.
linenoiseRequestStop();
if (m_inputThread.joinable())
{
CancelSynchronousIo((HANDLE)m_inputThread.native_handle());
m_inputThread.join();
}
linenoiseSetCompletionCallback(NULL);
if (s_instance == this)
{
s_instance = NULL;
}
m_engine = NULL;
LogInfo("console", "CLI input thread stopped.");
}
bool ServerCliInput::IsRunning() const
{
return m_running.load();
}
void ServerCliInput::RunInputLoop()
{
if (UseStreamInputMode())
{
LogInfo("console", "CLI input mode: stream(file stdin)");
RunStreamInputLoop();
return;
}
RunLinenoiseLoop();
}
/**
* use linenoise for interactive console input, with line editing and history support
*/
void ServerCliInput::RunLinenoiseLoop()
{
while (m_running)
{
char *line = linenoise("server> ");
if (line == NULL)
{
// NULL is expected on stop request (or Ctrl+C inside linenoise).
if (!m_running)
{
break;
}
Sleep(10);
continue;
}
EnqueueLine(line);
linenoiseFree(line);
}
}
/**
* use file-based stdin reading instead of linenoise when requested or when stdin is not a console/pty (e.g. piped input or non-interactive docker)
*/
void ServerCliInput::RunStreamInputLoop()
{
HANDLE stdinHandle = GetStdHandle(STD_INPUT_HANDLE);
if (stdinHandle == NULL || stdinHandle == INVALID_HANDLE_VALUE)
{
LogWarn("console", "stream input mode requested but STDIN handle is unavailable; falling back to linenoise.");
RunLinenoiseLoop();
return;
}
std::string line;
bool skipNextLf = false;
printf("server> ");
fflush(stdout);
while (m_running)
{
int readable = WaitForStdinReadable(stdinHandle, 50);
if (readable <= 0)
{
Sleep(10);
continue;
}
char ch = 0;
DWORD bytesRead = 0;
if (!ReadFile(stdinHandle, &ch, 1, &bytesRead, NULL) || bytesRead == 0)
{
Sleep(10);
continue;
}
if (skipNextLf && ch == '\n')
{
skipNextLf = false;
continue;
}
if (ch == '\r' || ch == '\n')
{
if (ch == '\r')
{
skipNextLf = true;
}
else
{
skipNextLf = false;
}
if (!line.empty())
{
EnqueueLine(line.c_str());
line.clear();
}
printf("server> ");
fflush(stdout);
continue;
}
skipNextLf = false;
if ((unsigned char)ch == 3)
{
continue;
}
if ((unsigned char)ch == 8 || (unsigned char)ch == 127)
{
if (!line.empty())
{
line.resize(line.size() - 1);
}
continue;
}
if (isprint((unsigned char)ch) && line.size() < 4096)
{
line.push_back(ch);
}
}
}
void ServerCliInput::EnqueueLine(const char *line)
{
if (line == NULL || line[0] == 0 || m_engine == NULL)
{
return;
}
// Keep local history and forward command for main-thread execution.
linenoiseHistoryAdd(line);
m_engine->EnqueueCommandLine(line);
}
void ServerCliInput::CompletionThunk(const char *line, linenoiseCompletions *completions)
{
// Static thunk forwards callback into instance state.
if (s_instance != NULL)
{
s_instance->BuildCompletions(line, completions);
}
}
void ServerCliInput::BuildCompletions(const char *line, linenoiseCompletions *completions)
{
if (line == NULL || completions == NULL || m_engine == NULL)
{
return;
}
std::vector<std::string> suggestions;
m_engine->BuildCompletions(line, &suggestions);
for (size_t i = 0; i < suggestions.size(); ++i)
{
linenoiseAddCompletion(completions, suggestions[i].c_str());
}
}
}

View file

@ -0,0 +1,63 @@
#pragma once
#include <atomic>
#include <thread>
struct linenoiseCompletions;
namespace ServerRuntime
{
class ServerCliEngine;
/**
* **CLI input worker**
*
* Owns the interactive input thread and bridges linenoise callbacks to the engine.
*
*/
class ServerCliInput
{
public:
ServerCliInput();
~ServerCliInput();
/**
* **Start input loop**
*
* Binds to an engine and starts reading user input from the console.
*
*/
void Start(ServerCliEngine *engine);
/**
* **Stop input loop**
*
* Requests stop and joins the input thread.
*
*/
void Stop();
/**
* **Check running state**
*
* Returns true while the input thread is active.
*
*/
bool IsRunning() const;
private:
void RunInputLoop();
void RunLinenoiseLoop();
void RunStreamInputLoop();
void EnqueueLine(const char *line);
static void CompletionThunk(const char *line, linenoiseCompletions *completions);
void BuildCompletions(const char *line, linenoiseCompletions *completions);
private:
std::atomic<bool> m_running;
std::thread m_inputThread;
ServerCliEngine *m_engine;
static ServerCliInput *s_instance;
};
}

View file

@ -0,0 +1,116 @@
#include "stdafx.h"
#include "ServerCliParser.h"
namespace ServerRuntime
{
static void TokenizeLine(const std::string &line, std::vector<std::string> *tokens, bool *trailingSpace)
{
std::string current;
bool inQuotes = false;
bool escaped = false;
tokens->clear();
*trailingSpace = false;
for (size_t i = 0; i < line.size(); ++i)
{
char ch = line[i];
if (escaped)
{
// Keep escaped character literally (e.g. \" or \ ).
current.push_back(ch);
escaped = false;
continue;
}
if (ch == '\\')
{
escaped = true;
continue;
}
if (ch == '"')
{
// Double quotes group spaces into one token.
inQuotes = !inQuotes;
continue;
}
if (!inQuotes && (ch == ' ' || ch == '\t'))
{
if (!current.empty())
{
tokens->push_back(current);
current.clear();
}
continue;
}
current.push_back(ch);
}
if (!current.empty())
{
tokens->push_back(current);
}
if (!line.empty())
{
char tail = line[line.size() - 1];
// Trailing space means completion targets the next token slot.
*trailingSpace = (!inQuotes && (tail == ' ' || tail == '\t'));
}
}
ServerCliParsedLine ServerCliParser::Parse(const std::string &line)
{
ServerCliParsedLine parsed;
parsed.raw = line;
TokenizeLine(line, &parsed.tokens, &parsed.trailingSpace);
return parsed;
}
ServerCliCompletionContext ServerCliParser::BuildCompletionContext(const std::string &line)
{
ServerCliCompletionContext context;
context.parsed = Parse(line);
if (context.parsed.tokens.empty())
{
context.currentTokenIndex = 0;
context.prefix.clear();
context.linePrefix.clear();
return context;
}
if (context.parsed.trailingSpace)
{
// Cursor is after a separator, so complete a new token.
context.currentTokenIndex = context.parsed.tokens.size();
context.prefix.clear();
}
else
{
// Cursor is inside current token, so complete by its prefix.
context.currentTokenIndex = context.parsed.tokens.size() - 1;
context.prefix = context.parsed.tokens.back();
}
for (size_t i = 0; i < context.currentTokenIndex; ++i)
{
// linePrefix is the immutable left side reused by completion output.
if (!context.linePrefix.empty())
{
context.linePrefix.push_back(' ');
}
context.linePrefix += context.parsed.tokens[i];
}
if (!context.linePrefix.empty())
{
context.linePrefix.push_back(' ');
}
return context;
}
}

View file

@ -0,0 +1,63 @@
#pragma once
#include <string>
#include <vector>
namespace ServerRuntime
{
/**
* **Parsed command line**
*/
struct ServerCliParsedLine
{
std::string raw;
std::vector<std::string> tokens;
bool trailingSpace;
ServerCliParsedLine()
: trailingSpace(false)
{
}
};
/**
* **Completion context for one input line**
*
* Indicates current token index, token prefix, and the fixed line prefix.
*/
struct ServerCliCompletionContext
{
ServerCliParsedLine parsed;
size_t currentTokenIndex;
std::string prefix;
std::string linePrefix;
ServerCliCompletionContext()
: currentTokenIndex(0)
{
}
};
/**
* **CLI parser helpers**
*
* Converts raw input text into tokenized data used by execution and completion.
*/
class ServerCliParser
{
public:
/**
* **Tokenize one command line**
*
* Supports quoted segments and escaped characters.
*/
static ServerCliParsedLine Parse(const std::string &line);
/**
* **Build completion metadata**
*
* Determines active token position and reusable prefix parts.
*/
static ServerCliCompletionContext BuildCompletionContext(const std::string &line);
};
}

View file

@ -0,0 +1,99 @@
#include "stdafx.h"
#include "ServerCliRegistry.h"
#include "commands\IServerCliCommand.h"
#include "..\Common\StringUtils.h"
namespace ServerRuntime
{
bool ServerCliRegistry::Register(std::unique_ptr<IServerCliCommand> command)
{
if (!command)
{
return false;
}
IServerCliCommand *raw = command.get();
std::string baseName = StringUtils::ToLowerAscii(raw->Name());
// Reject empty/duplicate primary command names.
if (baseName.empty() || m_lookup.find(baseName) != m_lookup.end())
{
return false;
}
std::vector<std::string> aliases = raw->Aliases();
std::vector<std::string> normalizedAliases;
normalizedAliases.reserve(aliases.size());
for (size_t i = 0; i < aliases.size(); ++i)
{
std::string alias = StringUtils::ToLowerAscii(aliases[i]);
// Alias must also be unique across all names and aliases.
if (alias.empty() || m_lookup.find(alias) != m_lookup.end())
{
return false;
}
normalizedAliases.push_back(alias);
}
m_lookup[baseName] = raw;
for (size_t i = 0; i < normalizedAliases.size(); ++i)
{
m_lookup[normalizedAliases[i]] = raw;
}
// Command objects are owned here; lookup stores non-owning pointers.
m_commands.push_back(std::move(command));
return true;
}
const IServerCliCommand *ServerCliRegistry::Find(const std::string &name) const
{
std::string key = StringUtils::ToLowerAscii(name);
auto it = m_lookup.find(key);
if (it == m_lookup.end())
{
return NULL;
}
return it->second;
}
IServerCliCommand *ServerCliRegistry::FindMutable(const std::string &name)
{
std::string key = StringUtils::ToLowerAscii(name);
auto it = m_lookup.find(key);
if (it == m_lookup.end())
{
return NULL;
}
return it->second;
}
void ServerCliRegistry::SuggestCommandNames(const std::string &prefix, const std::string &linePrefix, std::vector<std::string> *out) const
{
for (size_t i = 0; i < m_commands.size(); ++i)
{
const IServerCliCommand *command = m_commands[i].get();
std::string name = command->Name();
if (StringUtils::StartsWithIgnoreCase(name, prefix))
{
out->push_back(linePrefix + name);
}
// Include aliases so users can discover shorthand commands.
std::vector<std::string> aliases = command->Aliases();
for (size_t aliasIndex = 0; aliasIndex < aliases.size(); ++aliasIndex)
{
if (StringUtils::StartsWithIgnoreCase(aliases[aliasIndex], prefix))
{
out->push_back(linePrefix + aliases[aliasIndex]);
}
}
}
}
const std::vector<std::unique_ptr<IServerCliCommand>> &ServerCliRegistry::Commands() const
{
return m_commands;
}
}

View file

@ -0,0 +1,58 @@
#pragma once
#include <memory>
#include <string>
#include <unordered_map>
#include <vector>
namespace ServerRuntime
{
class IServerCliCommand;
/**
* **CLI command registry**
*/
class ServerCliRegistry
{
public:
/**
* **Register a command object**
*
* Validates name/aliases and adds lookup entries.
*
*/
bool Register(std::unique_ptr<IServerCliCommand> command);
/**
* **Find command by name or alias (const)**
*
* Returns null when no match exists.
*/
const IServerCliCommand *Find(const std::string &name) const;
/**
* **Find mutable command by name or alias**
*
* Used by runtime dispatch path.
*/
IServerCliCommand *FindMutable(const std::string &name);
/**
* **Suggest top-level command names**
*
* Adds matching command names and aliases to the output list.
*/
void SuggestCommandNames(const std::string &prefix, const std::string &linePrefix, std::vector<std::string> *out) const;
/**
* **Get registered command list**
*
* Intended for help output and inspection.
*/
const std::vector<std::unique_ptr<IServerCliCommand>> &Commands() const;
private:
std::vector<std::unique_ptr<IServerCliCommand>> m_commands;
std::unordered_map<std::string, IServerCliCommand *> m_lookup;
};
}

View file

@ -0,0 +1,39 @@
#pragma once
#include <cerrno>
#include <cstdlib>
#include <limits>
#include <string>
namespace ServerRuntime
{
namespace CommandParsing
{
inline bool TryParseInt(const std::string &text, int *outValue)
{
if (outValue == nullptr || text.empty())
{
return false;
}
char *end = nullptr;
errno = 0;
const long parsedValue = std::strtol(text.c_str(), &end, 10);
if (end == text.c_str() || *end != '\0')
{
return false;
}
if (errno == ERANGE)
{
return false;
}
if (parsedValue < (std::numeric_limits<int>::min)() || parsedValue > (std::numeric_limits<int>::max)())
{
return false;
}
*outValue = static_cast<int>(parsedValue);
return true;
}
}
}

View file

@ -0,0 +1,50 @@
#pragma once
#include <string>
#include <vector>
namespace ServerRuntime
{
class ServerCliEngine;
struct ServerCliParsedLine;
struct ServerCliCompletionContext;
/**
* **Command interface for server CLI**
*
* Implement this contract to add new commands without changing engine internals.
*/
class IServerCliCommand
{
public:
virtual ~IServerCliCommand() = default;
/** Primary command name */
virtual const char *Name() const = 0;
/** Optional aliases */
virtual std::vector<std::string> Aliases() const { return {}; }
/** Usage text for help */
virtual const char *Usage() const = 0;
/** Short command description*/
virtual const char *Description() const = 0;
/**
* **Execute command logic**
*
* Called after tokenization and command lookup.
*/
virtual bool Execute(const ServerCliParsedLine &line, ServerCliEngine *engine) = 0;
/**
* **Provide argument completion candidates**
*
* Override when command-specific completion is needed.
*/
virtual void Complete(const ServerCliCompletionContext &context, const ServerCliEngine *engine, std::vector<std::string> *out) const
{
(void)context;
(void)engine;
(void)out;
}
};
}

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