Merge remote-tracking branch 'itsRevela/main'

# Conflicts:
#	.github/workflows/nightly.yml
#	.gitignore
#	Minecraft.Client/ChatScreen.cpp
#	Minecraft.Client/ClientConnection.cpp
#	Minecraft.Client/Common/Audio/SoundEngine.cpp
#	Minecraft.Client/Common/Audio/SoundEngine.h
#	Minecraft.Client/Common/Media/MediaWindows64.arc
#	Minecraft.Client/Common/UI/IUIScene_HUD.cpp
#	Minecraft.Client/Common/UI/UIControl_Base.cpp
#	Minecraft.Client/Common/UI/UIScene_DeathMenu.cpp
#	Minecraft.Client/Common/UI/UIScene_JoinMenu.cpp
#	Minecraft.Client/Common/XUI/XUI_Chat.cpp
#	Minecraft.Client/Common/XUI/XUI_Death.cpp
#	Minecraft.Client/Font.cpp
#	Minecraft.Client/Gui.cpp
#	Minecraft.Client/PendingConnection.cpp
#	Minecraft.Client/PlayerConnection.cpp
#	Minecraft.Client/PlayerConnection.h
#	Minecraft.Client/PlayerList.cpp
#	Minecraft.Client/Windows64/Network/WinsockNetLayer.cpp
#	Minecraft.Client/Windows64/Network/WinsockNetLayer.h
#	Minecraft.Client/Windows64Media/strings.h
#	Minecraft.Client/cmake/sources/Common.cmake
#	Minecraft.Server/Console/ServerCliEngine.cpp
#	Minecraft.Server/Console/commands/whitelist/CliCommandWhitelist.cpp
#	Minecraft.Server/Windows64/ServerMain.cpp
#	Minecraft.World/WitherBoss.h
#	Minecraft.World/cmake/sources/Common.cmake
#	README.md
This commit is contained in:
George V. 2026-04-09 15:21:43 +03:00
commit 506ebd2176
220 changed files with 13703 additions and 1209 deletions

3
.gitattributes vendored
View file

@ -0,0 +1,3 @@
.github/workflows/docker-nightly.yml merge=ours
.github/workflows/nightly.yml merge=ours
docker-compose.dedicated-server.ghcr.yml merge=ours

BIN
.github/LCRE-banner.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 496 KiB

BIN
.github/hardcore-hearts.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

BIN
.github/hardcore-preview.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 778 KiB

View file

@ -18,144 +18,144 @@ jobs:
runs-on: windows-latest
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Checkout
uses: actions/checkout@v6
- name: Setup MSVC
uses: ilammy/msvc-dev-cmd@v1
- name: Setup MSVC
uses: ilammy/msvc-dev-cmd@v1
- name: Setup CMake
uses: lukka/get-cmake@latest
- name: Setup CMake
uses: lukka/get-cmake@latest
- name: Run CMake
uses: lukka/run-cmake@v10
env:
VCPKG_ROOT: ""
with:
configurePreset: windows64
buildPreset: windows64-release
buildPresetAdditionalArgs: "['--target', 'Minecraft.Client']"
- name: Run CMake
uses: lukka/run-cmake@v10
env:
VCPKG_ROOT: ""
with:
configurePreset: windows64
buildPreset: windows64-release
buildPresetAdditionalArgs: "['--target', 'Minecraft.Client']"
- name: Zip Build
shell: pwsh
run: |
$source = "./build/windows64/Minecraft.Client/Release"
$zip = "LCREWindows64.zip"
$topLevel = "LCREWindows64"
# Collect files, excluding unwanted extensions
$files = Get-ChildItem -Path $source -Recurse -File |
Where-Object { $_.Extension -notin '.pch', '.zip', '.ipdb', '.iobj' }
Add-Type -AssemblyName System.IO.Compression
Add-Type -AssemblyName System.IO.Compression.FileSystem
$basePath = (Resolve-Path $source).Path
$fs = [System.IO.File]::Open($zip, [System.IO.FileMode]::Create)
- name: Zip Build
shell: pwsh
run: |
$source = "./build/windows64/Minecraft.Client/Release"
$zip = "LCREWindows64.zip"
$topLevel = "LCREWindows64"
# Collect files, excluding unwanted extensions
$files = Get-ChildItem -Path $source -Recurse -File |
Where-Object { $_.Extension -notin '.pch', '.zip', '.ipdb', '.iobj' }
Add-Type -AssemblyName System.IO.Compression
Add-Type -AssemblyName System.IO.Compression.FileSystem
$basePath = (Resolve-Path $source).Path
$fs = [System.IO.File]::Open($zip, [System.IO.FileMode]::Create)
try {
$archive = New-Object System.IO.Compression.ZipArchive($fs, [System.IO.Compression.ZipArchiveMode]::Create)
try {
$archive = New-Object System.IO.Compression.ZipArchive($fs, [System.IO.Compression.ZipArchiveMode]::Create)
try {
# Add directories
Get-ChildItem -Path $basePath -Recurse -Directory | ForEach-Object {
$rel = $_.FullName.Substring($basePath.Length).TrimStart('\', '/')
$archive.CreateEntry("$topLevel/$($rel -replace '\\','/')/") | Out-Null
}
# Add files
foreach ($file in $files) {
$rel = $file.FullName.Substring($basePath.Length).TrimStart('\', '/')
$entryName = "$topLevel/$($rel -replace '\\','/')"
[System.IO.Compression.ZipFileExtensions]::CreateEntryFromFile(
$archive, $file.FullName, $entryName,
[System.IO.Compression.CompressionLevel]::Optimal
) | Out-Null
}
} finally { $archive.Dispose() }
} finally { $fs.Dispose() }
Write-Host "Created $zip"
# Add directories
Get-ChildItem -Path $basePath -Recurse -Directory | ForEach-Object {
$rel = $_.FullName.Substring($basePath.Length).TrimStart('\', '/')
$archive.CreateEntry("$topLevel/$($rel -replace '\\','/')/") | Out-Null
}
# Add files
foreach ($file in $files) {
$rel = $file.FullName.Substring($basePath.Length).TrimStart('\', '/')
$entryName = "$topLevel/$($rel -replace '\\','/')"
[System.IO.Compression.ZipFileExtensions]::CreateEntryFromFile(
$archive, $file.FullName, $entryName,
[System.IO.Compression.CompressionLevel]::Optimal
) | Out-Null
}
} finally { $archive.Dispose() }
} finally { $fs.Dispose() }
- name: Stage artifacts
shell: pwsh
run: |
New-Item -ItemType Directory -Force -Path staging
Copy-Item LCREWindows64.zip staging/
Copy-Item ./build/windows64/Minecraft.Client/Release/Minecraft.Client.exe staging/
Write-Host "Created $zip"
- name: Upload artifacts
uses: actions/upload-artifact@v6
with:
name: client-build
path: staging/*
- name: Stage artifacts
shell: pwsh
run: |
New-Item -ItemType Directory -Force -Path staging
Copy-Item LCREWindows64.zip staging/
Copy-Item ./build/windows64/Minecraft.Client/Release/Minecraft.Client.exe staging/
- name: Upload artifacts
uses: actions/upload-artifact@v6
with:
name: client-build
path: staging/*
build-server:
name: Build Server
runs-on: windows-latest
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Checkout
uses: actions/checkout@v6
- name: Setup MSVC
uses: ilammy/msvc-dev-cmd@v1
- name: Setup MSVC
uses: ilammy/msvc-dev-cmd@v1
- name: Setup CMake
uses: lukka/get-cmake@latest
- name: Setup CMake
uses: lukka/get-cmake@latest
- name: Run CMake
uses: lukka/run-cmake@v10
env:
VCPKG_ROOT: ""
with:
configurePreset: windows64
buildPreset: windows64-release
buildPresetAdditionalArgs: "['--target', 'Minecraft.Server']"
- name: Run CMake
uses: lukka/run-cmake@v10
env:
VCPKG_ROOT: ""
with:
configurePreset: windows64
buildPreset: windows64-release
buildPresetAdditionalArgs: "['--target', 'Minecraft.Server']"
- name: Zip Build
shell: pwsh
run: |
$source = "./build/windows64/Minecraft.Server/Release"
$zip = "LCREServerWindows64.zip"
$topLevel = "LCREServerWindows64"
$files = Get-ChildItem -Path $source -Recurse -File |
Where-Object { $_.Extension -notin '.pch', '.zip', '.ipdb', '.iobj' }
Add-Type -AssemblyName System.IO.Compression
Add-Type -AssemblyName System.IO.Compression.FileSystem
$basePath = (Resolve-Path $source).Path
$fs = [System.IO.File]::Open($zip, [System.IO.FileMode]::Create)
- name: Zip Build
shell: pwsh
run: |
$source = "./build/windows64/Minecraft.Server/Release"
$zip = "LCREServerWindows64.zip"
$topLevel = "LCREServerWindows64"
$files = Get-ChildItem -Path $source -Recurse -File |
Where-Object { $_.Extension -notin '.pch', '.zip', '.ipdb', '.iobj' }
Add-Type -AssemblyName System.IO.Compression
Add-Type -AssemblyName System.IO.Compression.FileSystem
$basePath = (Resolve-Path $source).Path
$fs = [System.IO.File]::Open($zip, [System.IO.FileMode]::Create)
try {
$archive = New-Object System.IO.Compression.ZipArchive($fs, [System.IO.Compression.ZipArchiveMode]::Create)
try {
$archive = New-Object System.IO.Compression.ZipArchive($fs, [System.IO.Compression.ZipArchiveMode]::Create)
try {
Get-ChildItem -Path $basePath -Recurse -Directory | ForEach-Object {
$rel = $_.FullName.Substring($basePath.Length).TrimStart('\', '/')
$archive.CreateEntry("$topLevel/$($rel -replace '\\','/')/") | Out-Null
}
foreach ($file in $files) {
$rel = $file.FullName.Substring($basePath.Length).TrimStart('\', '/')
$entryName = "$topLevel/$($rel -replace '\\','/')"
[System.IO.Compression.ZipFileExtensions]::CreateEntryFromFile(
$archive, $file.FullName, $entryName,
[System.IO.Compression.CompressionLevel]::Optimal
) | Out-Null
}
} finally { $archive.Dispose() }
} finally { $fs.Dispose() }
Write-Host "Created $zip"
Get-ChildItem -Path $basePath -Recurse -Directory | ForEach-Object {
$rel = $_.FullName.Substring($basePath.Length).TrimStart('\', '/')
$archive.CreateEntry("$topLevel/$($rel -replace '\\','/')/") | Out-Null
}
foreach ($file in $files) {
$rel = $file.FullName.Substring($basePath.Length).TrimStart('\', '/')
$entryName = "$topLevel/$($rel -replace '\\','/')"
[System.IO.Compression.ZipFileExtensions]::CreateEntryFromFile(
$archive, $file.FullName, $entryName,
[System.IO.Compression.CompressionLevel]::Optimal
) | Out-Null
}
} finally { $archive.Dispose() }
} finally { $fs.Dispose() }
- name: Stage artifacts
shell: pwsh
run: |
New-Item -ItemType Directory -Force -Path staging
Copy-Item LCREServerWindows64.zip staging/
Write-Host "Created $zip"
- name: Upload artifacts
uses: actions/upload-artifact@v6
with:
name: server-build
path: staging/*
- name: Stage artifacts
shell: pwsh
run: |
New-Item -ItemType Directory -Force -Path staging
Copy-Item LCREServerWindows64.zip staging/
- name: Upload artifacts
uses: actions/upload-artifact@v6
with:
name: server-build
path: staging/*
release-server:
name: Release Server
@ -163,58 +163,58 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Checkout
uses: actions/checkout@v6
- name: Download server artifacts
uses: actions/download-artifact@v7
with:
name: server-build
path: artifacts
- name: Download server artifacts
uses: actions/download-artifact@v7
with:
name: server-build
path: artifacts
- name: Attest artifacts
uses: actions/attest-build-provenance@v2
with:
subject-path: |
artifacts/LCREServerWindows64.zip
- name: Attest artifacts
uses: actions/attest-build-provenance@v2
with:
subject-path: |
artifacts/LCREServerWindows64.zip
- name: Get short SHA
id: sha
run: echo "short=$(echo '${{ github.sha }}' | cut -c1-7)" >> "$GITHUB_OUTPUT"
- name: Get short SHA
id: sha
run: echo "short=$(echo '${{ github.sha }}' | cut -c1-7)" >> "$GITHUB_OUTPUT"
- name: Delete old release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: gh release delete Nightly-Dedicated-Server --yes || true
- name: Delete old release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: gh release delete Nightly-Dedicated-Server --yes || true
- name: Delete old tag
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: gh api repos/${{ github.repository }}/git/refs/tags/Nightly-Dedicated-Server --method DELETE || true
- name: Delete old tag
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: gh api repos/${{ github.repository }}/git/refs/tags/Nightly-Dedicated-Server --method DELETE || true
- name: Import GPG key
uses: crazy-max/ghaction-import-gpg@v6
with:
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
passphrase: ${{ secrets.GPG_PASSPHRASE }}
git_user_signingkey: true
git_tag_gpgsign: true
- name: Import GPG key
uses: crazy-max/ghaction-import-gpg@v6
with:
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
passphrase: ${{ secrets.GPG_PASSPHRASE }}
git_user_signingkey: true
git_tag_gpgsign: true
- name: Create signed tag
run: |
git tag -s -f Nightly-Dedicated-Server -m "Nightly server release ${{ steps.sha.outputs.short }}"
git push origin Nightly-Dedicated-Server --force
- name: Create signed tag
run: |
git tag -s -f Nightly-Dedicated-Server -m "Nightly server release ${{ steps.sha.outputs.short }}"
git push origin Nightly-Dedicated-Server --force
- name: Create release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh release create Nightly-Dedicated-Server artifacts/* \
--title "Server: ${{ steps.sha.outputs.short }}" \
--notes "Dedicated Server runtime for Windows64.
Download \`LCREServerWindows64.zip\` and extract it to a folder where you'd like to keep the server runtime." \
--latest=false
- name: Create release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh release create Nightly-Dedicated-Server artifacts/* \
--title "Server: ${{ steps.sha.outputs.short }}" \
--notes "Dedicated Server runtime for Windows64.
Download \`LCREServerWindows64.zip\` and extract it to a folder where you'd like to keep the server runtime." \
--latest=false
release-client:
name: Release Client
@ -222,103 +222,103 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Checkout
uses: actions/checkout@v6
- name: Download client artifacts
uses: actions/download-artifact@v7
with:
name: client-build
path: artifacts
- name: Download client artifacts
uses: actions/download-artifact@v7
with:
name: client-build
path: artifacts
- name: Attest artifacts
uses: actions/attest-build-provenance@v2
with:
subject-path: |
artifacts/LCREWindows64.zip
artifacts/Minecraft.Client.exe
- name: Attest artifacts
uses: actions/attest-build-provenance@v2
with:
subject-path: |
artifacts/LCREWindows64.zip
artifacts/Minecraft.Client.exe
- name: Get short SHA
id: sha
run: echo "short=$(echo '${{ github.sha }}' | cut -c1-7)" >> "$GITHUB_OUTPUT"
- name: Get short SHA
id: sha
run: echo "short=$(echo '${{ github.sha }}' | cut -c1-7)" >> "$GITHUB_OUTPUT"
- name: Delete old release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: gh release delete Nightly --yes || true
- name: Delete old release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: gh release delete Nightly --yes || true
- name: Delete old tag
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: gh api repos/${{ github.repository }}/git/refs/tags/Nightly --method DELETE || true
- name: Delete old tag
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: gh api repos/${{ github.repository }}/git/refs/tags/Nightly --method DELETE || true
- name: Import GPG key
uses: crazy-max/ghaction-import-gpg@v6
with:
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
passphrase: ${{ secrets.GPG_PASSPHRASE }}
git_user_signingkey: true
git_tag_gpgsign: true
- name: Import GPG key
uses: crazy-max/ghaction-import-gpg@v6
with:
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
passphrase: ${{ secrets.GPG_PASSPHRASE }}
git_user_signingkey: true
git_tag_gpgsign: true
- name: Create signed tag
run: |
git tag -s -f Nightly -m "Nightly release ${{ steps.sha.outputs.short }}"
git push origin Nightly --force
- name: Create signed tag
run: |
git tag -s -f Nightly -m "Nightly release ${{ steps.sha.outputs.short }}"
git push origin Nightly --force
- name: Write release notes
run: |
cat > notes.md <<'NOTES'
# Instructions:
**Newcomers:**
- If this is your first time, download `LCREWindows64.zip` and extract it wherever you would like to keep it.
- I would recommend to set your username prior to launch (create a file called `username.txt`, put your desired username into the file, and save).
- To play, simply run `Minecraft.Client.exe`.
**For those that wish to update their existing installation with the latest build:**
- Download `Minecraft.Client.exe` and `Minecraft.Client.pdb` and copy them over to your existing LCREWindows64 build (overwrite your old version of Minecraft.Client.exe and Minecraft.Client.pdb).
**Steam Deck & Linux:**
- Y'all know the drill. Download the `LCREWindows64.zip`, extract it, add the `Minecraft.Client.exe` as a "Non-Steam Game" within the Steam library, turn on compatibility mode with Proton Experimental, and then run it!
# Multiplayer instructions:
LAN games are natively supported, and any LAN games will appear automatically on the right. However, if you'd like to play with your friends online (and if you don't want to require them to setup a vpn, and/or if you don't want to port forward), I would recommend the following setup. Please keep in mind, you do NOT need to do this to enjoy the game. This is just how I have it setup for me so my friends can join without any hassle:
Prerequisites:
- Premium playit.gg account, costs about $3 USD per month. This is for setting up the tunnel.
- playit.gg agent installed on host PC.
How-to:
- Ensure your playit.gg agent is connected to your playit.gg account
- On the playit.gg website, setup a new tunnel (choose TCP). Ensure the configurable settings are set to the below values, assuming your agent is installed on the same computer as your online LCREMinecraft game is hosted from.
- Configurable settings:
- Local IP: `127.0.0.1`
- Local Port: `25565`
- Proxy Protocol: `None`
- After creating your tunnel, navigate to the "Tunnels" main page. You'll see the IP address and port for your tunnel. This is what your friends will input when adding your server in order to join your online game!
# Why this fork exists:
Changes/additions that stray from the upstream repo (`smartcmd/MinecraftConsoles`:
- See: https://github.com/itsRevela/MinecraftConsoles?tab=readme-ov-file#latest
- I can tweak this fork while staying compatible with the upstream repo without needing to wait on my pull requests to get accepted upstream (while keeping this fork updated with the latest and greatest from upstream)
NOTES
- name: Write release notes
run: |
cat > notes.md <<'NOTES'
# Instructions:
**Newcomers:**
- If this is your first time, download `LCREWindows64.zip` and extract it wherever you would like to keep it.
- I would recommend to set your username prior to launch (create a file called `username.txt`, put your desired username into the file, and save).
- To play, simply run `Minecraft.Client.exe`.
- name: Create release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh release create Nightly artifacts/* \
--title "Client: ${{ steps.sha.outputs.short }}" \
--notes-file notes.md
**For those that wish to update their existing installation with the latest build:**
- Download `Minecraft.Client.exe` and `Minecraft.Client.pdb` and copy them over to your existing LCREWindows64 build (overwrite your old version of Minecraft.Client.exe and Minecraft.Client.pdb).
**Steam Deck & Linux:**
- Y'all know the drill. Download the `LCREWindows64.zip`, extract it, add the `Minecraft.Client.exe` as a "Non-Steam Game" within the Steam library, turn on compatibility mode with Proton Experimental, and then run it!
# Multiplayer instructions:
LAN games are natively supported, and any LAN games will appear automatically on the right. However, if you'd like to play with your friends online (and if you don't want to require them to setup a vpn, and/or if you don't want to port forward), I would recommend the following setup. Please keep in mind, you do NOT need to do this to enjoy the game. This is just how I have it setup for me so my friends can join without any hassle:
Prerequisites:
- Premium playit.gg account, costs about $3 USD per month. This is for setting up the tunnel.
- playit.gg agent installed on host PC.
How-to:
- Ensure your playit.gg agent is connected to your playit.gg account
- On the playit.gg website, setup a new tunnel (choose TCP). Ensure the configurable settings are set to the below values, assuming your agent is installed on the same computer as your online LCREMinecraft game is hosted from.
- Configurable settings:
- Local IP: `127.0.0.1`
- Local Port: `25565`
- Proxy Protocol: `None`
- After creating your tunnel, navigate to the "Tunnels" main page. You'll see the IP address and port for your tunnel. This is what your friends will input when adding your server in order to join your online game!
# Why this fork exists:
Changes/additions that stray from the upstream repo (`smartcmd/MinecraftConsoles`:
- See: https://github.com/itsRevela/MinecraftConsoles?tab=readme-ov-file#latest
- I can tweak this fork while staying compatible with the upstream repo without needing to wait on my pull requests to get accepted upstream (while keeping this fork updated with the latest and greatest from upstream)
NOTES
- name: Create release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh release create Nightly artifacts/* \
--title "Client: ${{ steps.sha.outputs.short }}" \
--notes-file notes.md
cleanup:
needs: [release-client, release-server]
if: always()
runs-on: ubuntu-latest
steps:
- name: Cleanup artifacts
uses: geekyeggo/delete-artifact@v5
with:
name: |
client-build
server-build
- name: Cleanup artifacts
uses: geekyeggo/delete-artifact@v5
with:
name: |
client-build
server-build

8
.gitignore vendored
View file

@ -26,6 +26,7 @@ mono_crash.*
[Rr]elease/
[Rr]eleases/
x64/
x64_*/
x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
@ -421,4 +422,9 @@ result-*
.direnv/
.xwin-cache/
.xwin
.xwin
# Tools build artifacts and intermediates
tools/*.class
tools/*.swf
tools/staging/
tools/server-monitor/

View file

@ -68,9 +68,14 @@ set(MINECRAFT_SHARED_DEFINES
$<$<CONFIG:Debug>:_DEBUG>
_CRT_NON_CONFORMING_SWPRINTFS
_CRT_SECURE_NO_WARNINGS
_HAS_STD_BYTE=0
)
# Add platform-specific defines
if(PLATFORM_NAME STREQUAL "Windows64")
list(APPEND MINECRAFT_SHARED_DEFINES _WINDOWS64)
set(IGGY_LIBS iggy_w64.lib)
endif()
list(APPEND MINECRAFT_SHARED_DEFINES ${PLATFORM_DEFINES})
# ---

View file

@ -52,6 +52,7 @@ set_target_properties(Minecraft.Client PROPERTIES
target_link_libraries(Minecraft.Client PRIVATE
Minecraft.World
d3d11
dxgi
d3dcompiler
XInput9_1_0
wsock32

View file

@ -6,6 +6,7 @@
#include "../Minecraft.World/SharedConstants.h"
#include "../Minecraft.World/StringHelpers.h"
#include "../Minecraft.World/ChatPacket.h"
#include "../Minecraft.World/ArabicShaping.h"
const wstring ChatScreen::allowedChars = SharedConstants::acceptableLetters;
vector<wstring> ChatScreen::s_chatHistory;
@ -14,7 +15,12 @@ wstring ChatScreen::s_historyDraft;
bool ChatScreen::isAllowedChatChar(wchar_t c)
{
return c >= 0x20 && (c == L'\u00A7' || allowedChars.empty() || allowedChars.find(c) != wstring::npos);
if (c < 0x20) return false;
// Block Unicode bidirectional override characters that can be used to
// spoof chat messages or impersonate players.
if (c >= 0x202A && c <= 0x202E) return false; // LRE, RLE, PDF, LRO, RLO
if (c >= 0x2066 && c <= 0x2069) return false; // LRI, RLI, FSI, PDI
return true;
}
ChatScreen::ChatScreen()
@ -93,6 +99,9 @@ void ChatScreen::keyPressed(wchar_t ch, int eventKey)
if (eventKey == Keyboard::KEY_RETURN)
{
wstring trim = trimString(message);
{ char buf[64]; sprintf_s(buf, "[CHAT] Sending (%d chars): ", (int)trim.length()); OutputDebugStringA(buf); }
OutputDebugStringW(trim.c_str());
OutputDebugStringA("\n");
if (trim.length() > 0)
{
if (!minecraft->handleClientSideCommand(trim))
@ -145,14 +154,21 @@ void ChatScreen::render(int xm, int ym, float a)
int x = 4;
drawString(font, prefix, x, height - 12, 0xe0e0e0);
x += font->width(prefix);
wstring beforeCursor = message.substr(0, cursorIndex);
wstring afterCursor = message.substr(cursorIndex);
drawStringLiteral(font, beforeCursor, x, height - 12, 0xe0e0e0);
x += font->widthLiteral(beforeCursor);
// Shape the full message as one unit so letter connections and word order
// are correct. Track where the logical cursor maps in the visual string.
int visualCursorPos = 0;
wstring shaped = shapeArabicText(message, cursorIndex, &visualCursorPos);
// Render the full shaped message without re-shaping it
drawStringPreshaped(font, shaped, x, height - 12, 0xe0e0e0);
// Place the cursor at the correct visual position
wstring beforeCursorVisual = shaped.substr(0, visualCursorPos);
int cursorX = x + font->widthPreshaped(beforeCursorVisual);
if (frame / 6 % 2 == 0)
drawString(font, L"_", x, height - 12, 0xe0e0e0);
x += font->width(L"_");
drawStringLiteral(font, afterCursor, x, height - 12, 0xe0e0e0);
drawString(font, L"_", cursorX, height - 12, 0xe0e0e0);
Screen::render(xm, ym, a);
}

View file

@ -58,6 +58,7 @@
#ifdef _WINDOWS64
#include "Xbox/Network/NetworkPlayerXbox.h"
#include "Common/Network/PlatformNetworkManagerStub.h"
#include "Windows64\Network\WinsockNetLayer.h"
#endif
@ -65,6 +66,7 @@
#include "../Minecraft.World/DurangoStats.h"
#include "../Minecraft.World/GenericStats.h"
#endif
namespace
{
char mapIconToFrame(char iconSlot)
@ -133,6 +135,7 @@ ClientConnection::ClientConnection(Minecraft *minecraft, Socket *socket, int iUs
started = false;
savedDataStorage = new SavedDataStorage(nullptr);
maxPlayers = 20;
m_isForkServer = false;
this->minecraft = minecraft;
@ -365,7 +368,7 @@ void ClientConnection::handleLogin(shared_ptr<LoginPacket> packet)
Level *dimensionLevel = minecraft->getLevel( packet->dimension );
if( dimensionLevel == nullptr )
{
level = new MultiPlayerLevel(this, new LevelSettings(packet->seed, GameType::byId(packet->gameType), false, false, packet->m_newSeaLevel, packet->m_pLevelType, packet->m_xzSize, packet->m_hellScale), packet->dimension, packet->difficulty);
level = new MultiPlayerLevel(this, new LevelSettings(packet->seed, GameType::byId(packet->gameType), false, packet->m_isHardcore, packet->m_newSeaLevel, packet->m_pLevelType, packet->m_xzSize, packet->m_hellScale), packet->dimension, packet->difficulty);
// 4J Stu - We want to share the SavedDataStorage between levels
int otherDimensionId = packet->dimension == 0 ? -1 : 0;
@ -435,7 +438,7 @@ void ClientConnection::handleLogin(shared_ptr<LoginPacket> packet)
activeLevel = minecraft->getLevel(otherDimensionId);
}
MultiPlayerLevel *dimensionLevel = new MultiPlayerLevel(this, new LevelSettings(packet->seed, GameType::byId(packet->gameType), false, false, packet->m_newSeaLevel, packet->m_pLevelType, packet->m_xzSize, packet->m_hellScale), packet->dimension, packet->difficulty);
MultiPlayerLevel *dimensionLevel = new MultiPlayerLevel(this, new LevelSettings(packet->seed, GameType::byId(packet->gameType), false, packet->m_isHardcore, packet->m_newSeaLevel, packet->m_pLevelType, packet->m_xzSize, packet->m_hellScale), packet->dimension, packet->difficulty);
dimensionLevel->savedDataStorage = activeLevel->savedDataStorage;
@ -1139,7 +1142,11 @@ void ClientConnection::handleMoveEntitySmall(shared_ptr<MoveEntityPacketSmall> p
void ClientConnection::handleRemoveEntity(shared_ptr<RemoveEntitiesPacket> packet)
{
#ifdef _WINDOWS64
if (!g_NetworkManager.IsHost())
// On fork servers, IQNet cleanup is handled by the MC|ForkPLeave custom
// payload so players stay in Tab regardless of render distance. On
// upstream servers (no MC|ForkHello received), fall back to the old
// behaviour of cleaning up IQNet here.
if (!m_isForkServer && !g_NetworkManager.IsHost())
{
for (int i = 0; i < packet->ids.length; i++)
{
@ -1149,7 +1156,6 @@ void ClientConnection::handleRemoveEntity(shared_ptr<RemoveEntitiesPacket> packe
shared_ptr<Player> player = dynamic_pointer_cast<Player>(entity);
if (player != nullptr)
{
// Match by gamertag in the IQNet array (XUID may be 0 on dedicated servers)
for (int s = 1; s < MINECRAFT_NET_MAX_PLAYERS; ++s)
{
IQNetPlayer* qp = &IQNet::m_player[s];
@ -2925,7 +2931,7 @@ void ClientConnection::handleRespawn(shared_ptr<RespawnPacket> packet)
MultiPlayerLevel *dimensionLevel = (MultiPlayerLevel *)minecraft->getLevel( packet->dimension );
if( dimensionLevel == nullptr )
{
dimensionLevel = new MultiPlayerLevel(this, new LevelSettings(packet->mapSeed, packet->playerGameType, false, minecraft->level->getLevelData()->isHardcore(), packet->m_newSeaLevel, packet->m_pLevelType, packet->m_xzSize, packet->m_hellScale), packet->dimension, packet->difficulty);
dimensionLevel = new MultiPlayerLevel(this, new LevelSettings(packet->mapSeed, packet->playerGameType, false, packet->m_isHardcore, packet->m_newSeaLevel, packet->m_pLevelType, packet->m_xzSize, packet->m_hellScale), packet->dimension, packet->difficulty);
// 4J Stu - We want to shared the savedDataStorage between both levels
//if( dimensionLevel->savedDataStorage != nullptr )
@ -3370,7 +3376,9 @@ void ClientConnection::handleTileEditorOpen(shared_ptr<TileEditorOpenPacket> pac
void ClientConnection::handleSignUpdate(shared_ptr<SignUpdatePacket> packet)
{
app.DebugPrintf("ClientConnection::handleSignUpdate - ");
app.DebugPrintf("[SIGN] handleSignUpdate at (%d, %d, %d):\n", packet->x, packet->y, packet->z);
for (int i = 0; i < MAX_SIGN_LINES; i++)
app.DebugPrintf("[SIGN] Line%d: \"%ls\"\n", i+1, packet->lines[i].c_str());
if (minecraft->level->hasChunkAt(packet->x, packet->y, packet->z))
{
shared_ptr<TileEntity> te = minecraft->level->getTileEntity(packet->x, packet->y, packet->z);
@ -3384,7 +3392,7 @@ void ClientConnection::handleSignUpdate(shared_ptr<SignUpdatePacket> packet)
ste->SetMessage(i,packet->lines[i]);
}
app.DebugPrintf("verified = %d\tCensored = %d\n",packet->m_bVerified,packet->m_bCensored);
app.DebugPrintf("[SIGN] verified=%d censored=%d\n", packet->m_bVerified, packet->m_bCensored);
ste->SetVerified(packet->m_bVerified);
ste->SetCensored(packet->m_bCensored);
@ -3392,12 +3400,12 @@ void ClientConnection::handleSignUpdate(shared_ptr<SignUpdatePacket> packet)
}
else
{
app.DebugPrintf("dynamic_pointer_cast<SignTileEntity>(te) == nullptr\n");
app.DebugPrintf("[SIGN] ERROR: tile entity is not a SignTileEntity\n");
}
}
else
{
app.DebugPrintf("hasChunkAt failed\n");
app.DebugPrintf("[SIGN] ERROR: chunk not loaded at position\n");
}
}
@ -3784,6 +3792,158 @@ void ClientConnection::handleSoundEvent(shared_ptr<LevelSoundPacket> packet)
void ClientConnection::handleCustomPayload(shared_ptr<CustomPayloadPacket> customPayloadPacket)
{
#ifdef _WINDOWS64
// Build a server-specific identity token file path next to the executable.
// Each server gets its own token file based on a hash of the server address,
// so connecting to multiple secured servers doesn't overwrite tokens.
auto buildIdentityTokenPath = []() -> std::string {
char exePath[MAX_PATH] = {};
DWORD len = GetModuleFileNameA(NULL, exePath, MAX_PATH);
if (len == 0 || len >= MAX_PATH) return std::string();
char *lastSlash = strrchr(exePath, '\\');
if (lastSlash != NULL) *(lastSlash + 1) = 0;
// Hash the server IP:port to create a unique filename per server
char serverAddr[300] = {};
sprintf_s(serverAddr, sizeof(serverAddr), "%s:%d", g_Win64MultiplayerIP, g_Win64MultiplayerPort);
unsigned int hash = 5381;
for (const char *p = serverAddr; *p; ++p)
hash = ((hash << 5) + hash) + static_cast<unsigned char>(*p);
char filename[64] = {};
sprintf_s(filename, sizeof(filename), "identity-token-%08x.dat", hash);
return std::string(exePath) + filename;
};
// Identity token: server issued us a new token - store it locally
if (CustomPayloadPacket::IDENTITY_TOKEN_ISSUE.compare(customPayloadPacket->identifier) == 0)
{
if (customPayloadPacket->data.data != nullptr && customPayloadPacket->length == 32)
{
std::string tokenPath = buildIdentityTokenPath();
if (!tokenPath.empty())
{
FILE *f = nullptr;
fopen_s(&f, tokenPath.c_str(), "wb");
if (f != nullptr)
{
size_t written = fwrite(customPayloadPacket->data.data, 1, 32, f);
fclose(f);
if (written == 32)
{
app.DebugPrintf("Client: Stored identity token to %s\n", tokenPath.c_str());
}
else
{
app.DebugPrintf("Client: Failed to write full identity token (wrote %zu/32)\n", written);
}
}
else
{
app.DebugPrintf("Client: Failed to open %s for writing\n", tokenPath.c_str());
}
}
}
return;
}
// Identity token: server is challenging us to present our stored token
if (CustomPayloadPacket::IDENTITY_TOKEN_CHALLENGE.compare(customPayloadPacket->identifier) == 0)
{
std::string tokenPath = buildIdentityTokenPath();
FILE *f = nullptr;
if (!tokenPath.empty())
fopen_s(&f, tokenPath.c_str(), "rb");
if (f != nullptr)
{
uint8_t token[32] = {};
size_t bytesRead = fread(token, 1, 32, f);
fclose(f);
if (bytesRead == 32)
{
byteArray tokenData(32);
memcpy(tokenData.data, token, 32);
connection->send(std::make_shared<CustomPayloadPacket>(
CustomPayloadPacket::IDENTITY_TOKEN_RESPONSE, tokenData));
app.DebugPrintf("Client: Sent identity token response\n");
}
else
{
app.DebugPrintf("Client: identity-token.dat is invalid (%zu bytes)\n", bytesRead);
connection->send(std::make_shared<CustomPayloadPacket>(
CustomPayloadPacket::IDENTITY_TOKEN_RESPONSE, byteArray()));
}
SecureZeroMemory(token, sizeof(token));
}
else
{
app.DebugPrintf("Client: No identity-token.dat found, sending empty response\n");
connection->send(std::make_shared<CustomPayloadPacket>(
CustomPayloadPacket::IDENTITY_TOKEN_RESPONSE, byteArray()));
}
return;
}
// Stream cipher handshake: server sent us a key
if (CustomPayloadPacket::CIPHER_KEY_CHANNEL.compare(customPayloadPacket->identifier) == 0)
{
if (customPayloadPacket->length == ServerRuntime::Security::StreamCipher::KEY_SIZE &&
customPayloadPacket->data.data != nullptr)
{
app.DebugPrintf("Client: Received MC|CKey from server (%d bytes)\n", customPayloadPacket->length);
// Store key and send ack+activate atomically to prevent ResetClientCipher race
WinsockNetLayer::StoreClientCipherKey(customPayloadPacket->data.data);
if (!WinsockNetLayer::SendAckAndActivateClientSendCipher())
{
app.DebugPrintf("Client: Failed to send cipher ack, connection will be closed\n");
}
}
else
{
app.DebugPrintf("Client: Received malformed MC|CKey (length=%d)\n", customPayloadPacket->length);
}
return;
}
// Fork server identification: enables render-distance-independent player list
if (CustomPayloadPacket::FORK_HELLO_CHANNEL.compare(customPayloadPacket->identifier) == 0)
{
m_isForkServer = true;
app.DebugPrintf("Client: Connected to fork server\n");
return;
}
// Fork server player leave: clean up IQNet slot so player leaves Tab list
if (CustomPayloadPacket::FORK_PLAYER_LEAVE_CHANNEL.compare(customPayloadPacket->identifier) == 0)
{
if (customPayloadPacket->data.data != nullptr && customPayloadPacket->length > 0)
{
int nameLen = customPayloadPacket->length / static_cast<int>(sizeof(wchar_t));
wstring leavingName(reinterpret_cast<const wchar_t*>(customPayloadPacket->data.data), nameLen);
for (int s = 1; s < MINECRAFT_NET_MAX_PLAYERS; ++s)
{
IQNetPlayer* qp = &IQNet::m_player[s];
if (qp->GetCustomDataValue() != 0 &&
_wcsicmp(qp->m_gamertag, leavingName.c_str()) == 0)
{
extern CPlatformNetworkManagerStub* g_pPlatformNetworkManager;
g_pPlatformNetworkManager->NotifyPlayerLeaving(qp);
qp->m_smallId = 0;
qp->m_isRemote = false;
qp->m_isHostPlayer = false;
qp->m_resolvedXuid = INVALID_XUID;
qp->m_gamertag[0] = 0;
qp->SetCustomDataValue(0);
app.DebugPrintf("Client: Player \"%ls\" left fork server, cleared IQNet slot %d\n", leavingName.c_str(), s);
break;
}
}
}
return;
}
#endif
if (CustomPayloadPacket::TRADER_LIST_PACKET.compare(customPayloadPacket->identifier) == 0)
{
ByteArrayInputStream bais(customPayloadPacket->data);
@ -4091,8 +4251,7 @@ void ClientConnection::handleSetPlayerTeamPacket(shared_ptr<SetPlayerTeamPacket>
void ClientConnection::handleParticleEvent(shared_ptr<LevelParticlesPacket> packet)
{
wstring particleName = packet->getName();
ePARTICLE_TYPE particleId = (ePARTICLE_TYPE)Integer::parseInt(particleName);
ePARTICLE_TYPE particleId = (ePARTICLE_TYPE)Integer::parseInt(packet->getName());
for (int i = 0; i < packet->getCount(); i++)
{

View file

@ -48,6 +48,7 @@ private:
std::unordered_set<int> m_trackedEntityIds;
std::unordered_set<int64_t> m_visibleChunks;
bool m_isForkServer; // true when connected to a fork server (received MC|ForkHello)
static int64_t chunkKey(int x, int z) { return ((int64_t)x << 32) | ((int64_t)z & 0xFFFFFFFF); }

View file

@ -1,5 +1,13 @@
#include "stdafx.h"
#include "ClientConstants.h"
#include "Common/BuildVer.h"
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;
// Default value for the toggle. If BuildVer defines VER_SHOW_VERSION_WATERMARK, use that.
#ifdef VER_SHOW_VERSION_WATERMARK
const bool ClientConstants::SHOW_VERSION_WATERMARK = (VER_SHOW_VERSION_WATERMARK != 0);
#else
const bool ClientConstants::SHOW_VERSION_WATERMARK = false;
#endif

View file

@ -15,5 +15,8 @@ public:
static const wstring VERSION_STRING;
static const wstring BRANCH_STRING;
// Toggle to show/hide the version/branch watermark in the debug overlay
static const bool SHOW_VERSION_WATERMARK;
static const bool DEADMAU5_CAMERA_CHEATS = false;
};

View file

@ -45,6 +45,7 @@
#define GAME_HOST_OPTION_BITMASK_DOTILEDROPS 0x08000000
#define GAME_HOST_OPTION_BITMASK_NATURALREGEN 0x10000000
#define GAME_HOST_OPTION_BITMASK_DODAYLIGHTCYCLE 0x20000000
#define GAME_HOST_OPTION_BITMASK_HARDCORE 0x40000000 // 4J Added - for hardcore mode
#define GAME_HOST_OPTION_BITMASK_ALL 0xFFFFFFFF
#define GAME_HOST_OPTION_BITMASK_WORLDSIZE_BITSHIFT 20
@ -104,6 +105,8 @@ enum EGameHostOptionWorldSize
#define GAMESETTING_ANIMATEDCHARACTER 0x00008000
#define GAMESETTING_PS3EULAREAD 0x00010000
#define GAMESETTING_PSVITANETWORKMODEADHOC 0x00020000
#define GAMESETTING_VSYNC 0x01000000
#define GAMESETTING_EXCLUSIVEFULLSCREEN 0x02000000
// defines for languages

View file

@ -178,6 +178,9 @@ enum eGameSetting
// PSVita
eGameSetting_PSVita_NetworkModeAdhoc,
// PC
eGameSetting_VSync,
eGameSetting_ExclusiveFullscreen,
};
@ -660,6 +663,7 @@ enum eGameHostOption
eGameHostOption_DoTileDrops,
eGameHostOption_NaturalRegeneration,
eGameHostOption_DoDaylightCycle,
eGameHostOption_Hardcore, // 4J Added - for hardcore mode
};
// 4J-PB - If any new DLC items are added to the TMSFiles, this array needs updated

View file

@ -207,6 +207,8 @@ CMinecraftApp::CMinecraftApp()
m_dwRequiredTexturePackID=0;
m_bResetNether=false;
m_seedOverride = 0;
m_hasSeedOverride = false;
#ifdef _XBOX
// m_bTransferSavesToXboxOne=false;
@ -1398,6 +1400,7 @@ void CMinecraftApp::ApplyGameSettingsChanged(int iPad)
ActionGameSettings(iPad,eGameSetting_AnimatedCharacter);
ActionGameSettings(iPad,eGameSetting_PS3_EULA_Read);
ActionGameSettings(iPad,eGameSetting_VSync);
}
@ -1633,6 +1636,22 @@ void CMinecraftApp::ActionGameSettings(int iPad,eGameSetting eVal)
case eGameSetting_PSVita_NetworkModeAdhoc:
//nothing to do here
break;
case eGameSetting_VSync:
#ifdef _WINDOWS64
{
extern bool g_bVSync;
g_bVSync = (GetGameSettings(iPad, eGameSetting_VSync) != 0);
}
#endif
break;
case eGameSetting_ExclusiveFullscreen:
#ifdef _WINDOWS64
{
extern void SetExclusiveFullscreen(bool enabled);
SetExclusiveFullscreen(GetGameSettings(iPad, eGameSetting_ExclusiveFullscreen) != 0);
}
#endif
break;
}
}
@ -2344,6 +2363,38 @@ void CMinecraftApp::SetGameSettings(int iPad,eGameSetting eVal,unsigned char ucV
}
break;
case eGameSetting_VSync:
if(((GameSettingsA[iPad]->uiBitmaskValues&GAMESETTING_VSYNC)>>24)!=(ucVal&0x01))
{
if(ucVal==1)
{
GameSettingsA[iPad]->uiBitmaskValues|=GAMESETTING_VSYNC;
}
else
{
GameSettingsA[iPad]->uiBitmaskValues&=~GAMESETTING_VSYNC;
}
ActionGameSettings(iPad,eVal);
GameSettingsA[iPad]->bSettingsChanged=true;
}
break;
case eGameSetting_ExclusiveFullscreen:
if(((GameSettingsA[iPad]->uiBitmaskValues&GAMESETTING_EXCLUSIVEFULLSCREEN)>>25)!=(ucVal&0x01))
{
if(ucVal==1)
{
GameSettingsA[iPad]->uiBitmaskValues|=GAMESETTING_EXCLUSIVEFULLSCREEN;
}
else
{
GameSettingsA[iPad]->uiBitmaskValues&=~GAMESETTING_EXCLUSIVEFULLSCREEN;
}
ActionGameSettings(iPad,eVal);
GameSettingsA[iPad]->bSettingsChanged=true;
}
break;
}
}
@ -2479,6 +2530,12 @@ unsigned char CMinecraftApp::GetGameSettings(int iPad,eGameSetting eVal)
case eGameSetting_PSVita_NetworkModeAdhoc:
return (GameSettingsA[iPad]->uiBitmaskValues&GAMESETTING_PSVITANETWORKMODEADHOC)>>17;
case eGameSetting_VSync:
return (GameSettingsA[iPad]->uiBitmaskValues&GAMESETTING_VSYNC)>>24;
case eGameSetting_ExclusiveFullscreen:
return (GameSettingsA[iPad]->uiBitmaskValues&GAMESETTING_EXCLUSIVEFULLSCREEN)>>25;
}
return 0;
}
@ -8103,6 +8160,16 @@ void CMinecraftApp::SetGameHostOption(unsigned int &uiHostSettings, eGameHostOpt
uiHostSettings&=~GAME_HOST_OPTION_BITMASK_WORLDSIZE;
uiHostSettings|=(GAME_HOST_OPTION_BITMASK_WORLDSIZE & (uiVal<<GAME_HOST_OPTION_BITMASK_WORLDSIZE_BITSHIFT));
break;
case eGameHostOption_Hardcore: // 4J Added - for hardcore mode
if(uiVal!=0)
{
uiHostSettings |= GAME_HOST_OPTION_BITMASK_HARDCORE;
}
else
{
uiHostSettings &= ~GAME_HOST_OPTION_BITMASK_HARDCORE;
}
break;
case eGameHostOption_All:
uiHostSettings=uiVal;
break;
@ -8204,6 +8271,9 @@ unsigned int CMinecraftApp::GetGameHostOption(unsigned int uiHostSettings, eGame
case eGameHostOption_DoDaylightCycle:
return !(uiHostSettings&GAME_HOST_OPTION_BITMASK_DODAYLIGHTCYCLE);
break;
case eGameHostOption_Hardcore: // 4J Added - for hardcore mode
return (uiHostSettings&GAME_HOST_OPTION_BITMASK_HARDCORE) ? 1 : 0;
break;
}
return false;

View file

@ -707,6 +707,8 @@ private:
bool m_bGameNewWorldSizeUseMoat;
unsigned int m_GameNewHellScale;
#endif
int64_t m_seedOverride;
bool m_hasSeedOverride;
unsigned int FromBigEndian(unsigned int uiValue);
public:
@ -724,6 +726,10 @@ public:
void SetGameNewHellScale(unsigned int newScale) { m_GameNewHellScale = newScale; }
unsigned int GetGameNewHellScale() { return m_GameNewHellScale; }
#endif
void SetSeedOverride(int64_t seed) { m_seedOverride = seed; m_hasSeedOverride = true; }
bool HasSeedOverride() { return m_hasSeedOverride; }
int64_t GetSeedOverride() { return m_seedOverride; }
void SetResetNether(bool bResetNether) {m_bResetNether=bResetNether;}
bool GetResetNether() {return m_bResetNether;}
bool CanRecordStatsAndAchievements();
@ -810,6 +816,10 @@ public:
void SetCorruptSaveDeleted(bool bVal) {m_bCorruptSaveDeleted=bVal;}
bool GetCorruptSaveDeleted(void) {return m_bCorruptSaveDeleted;}
// 4J Added: Store save folder name for hardcore world deletion on Win64
void SetCurrentSaveFolderName(const wstring& name) { m_currentSaveFolderName = name; }
const wstring& GetCurrentSaveFolderName() const { return m_currentSaveFolderName; }
void EnterSaveNotificationSection();
void LeaveSaveNotificationSection();
private:
@ -831,6 +841,7 @@ private:
CRITICAL_SECTION csAdditionalSkinBoxes;
CRITICAL_SECTION csAnimOverrideBitmask;
bool m_bCorruptSaveDeleted;
wstring m_currentSaveFolderName; // 4J Added: for hardcore world deletion on Win64
DWORD m_dwAdditionalModelParts[XUSER_MAX_COUNT];

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 B

View file

@ -38220,6 +38220,56 @@
<Time>19</Time>
<Command>stop</Command>
</NamedFrame>
<NamedFrame>
<Name>NormalHardcore</Name>
<Time>20</Time>
<Command>stop</Command>
</NamedFrame>
<NamedFrame>
<Name>HalfHardcore</Name>
<Time>21</Time>
<Command>stop</Command>
</NamedFrame>
<NamedFrame>
<Name>FullHardcore</Name>
<Time>22</Time>
<Command>stop</Command>
</NamedFrame>
<NamedFrame>
<Name>HalfPoisonHardcore</Name>
<Time>23</Time>
<Command>stop</Command>
</NamedFrame>
<NamedFrame>
<Name>FullPoisonHardcore</Name>
<Time>24</Time>
<Command>stop</Command>
</NamedFrame>
<NamedFrame>
<Name>NormalFlashHardcore</Name>
<Time>25</Time>
<Command>stop</Command>
</NamedFrame>
<NamedFrame>
<Name>HalfFlashHardcore</Name>
<Time>26</Time>
<Command>stop</Command>
</NamedFrame>
<NamedFrame>
<Name>FullFlashHardcore</Name>
<Time>27</Time>
<Command>stop</Command>
</NamedFrame>
<NamedFrame>
<Name>HalfPoisonFlashHardcore</Name>
<Time>28</Time>
<Command>stop</Command>
</NamedFrame>
<NamedFrame>
<Name>FullPoisonFlashHardcore</Name>
<Time>29</Time>
<Command>stop</Command>
</NamedFrame>
</NamedFrames>
<Timeline>
<Id>Border</Id>
@ -38274,6 +38324,16 @@
<Interpolation>0</Interpolation>
<Prop>Graphics\HUD\Health_Background_Flash.png</Prop>
</KeyFrame>
<KeyFrame>
<Time>20</Time>
<Interpolation>0</Interpolation>
<Prop>Graphics\HUD\Health_Background_Hardcore.png</Prop>
</KeyFrame>
<KeyFrame>
<Time>25</Time>
<Interpolation>0</Interpolation>
<Prop>Graphics\HUD\Health_Background_Hardcore_Flash.png</Prop>
</KeyFrame>
</Timeline>
<Timeline>
<Id>Heart</Id>
@ -38399,6 +38459,66 @@
<Prop>true</Prop>
<Prop>Graphics\HUD\HorseHealth_Half_Flash.png</Prop>
</KeyFrame>
<KeyFrame>
<Time>20</Time>
<Interpolation>0</Interpolation>
<Prop>false</Prop>
<Prop></Prop>
</KeyFrame>
<KeyFrame>
<Time>21</Time>
<Interpolation>0</Interpolation>
<Prop>true</Prop>
<Prop>Graphics\HUD\Health_Half_Hardcore.png</Prop>
</KeyFrame>
<KeyFrame>
<Time>22</Time>
<Interpolation>0</Interpolation>
<Prop>true</Prop>
<Prop>Graphics\HUD\Health_Full_Hardcore.png</Prop>
</KeyFrame>
<KeyFrame>
<Time>23</Time>
<Interpolation>0</Interpolation>
<Prop>true</Prop>
<Prop>Graphics\HUD\Health_Half_Poison_Hardcore.png</Prop>
</KeyFrame>
<KeyFrame>
<Time>24</Time>
<Interpolation>0</Interpolation>
<Prop>true</Prop>
<Prop>Graphics\HUD\Health_Full_Poison_Hardcore.png</Prop>
</KeyFrame>
<KeyFrame>
<Time>25</Time>
<Interpolation>0</Interpolation>
<Prop>false</Prop>
<Prop></Prop>
</KeyFrame>
<KeyFrame>
<Time>26</Time>
<Interpolation>0</Interpolation>
<Prop>true</Prop>
<Prop>Graphics\HUD\Health_Half_Flash_Hardcore.png</Prop>
</KeyFrame>
<KeyFrame>
<Time>27</Time>
<Interpolation>0</Interpolation>
<Prop>true</Prop>
<Prop>Graphics\HUD\Health_Full_Flash_Hardcore.png</Prop>
</KeyFrame>
<KeyFrame>
<Time>28</Time>
<Interpolation>0</Interpolation>
<Prop>true</Prop>
<Prop>Graphics\HUD\Health_Half_Poison_Flash_Hardcore.png</Prop>
</KeyFrame>
<KeyFrame>
<Time>29</Time>
<Interpolation>0</Interpolation>
<Prop>true</Prop>
<Prop>Graphics\HUD\Health_Full_Poison_Flash_Hardcore.png</Prop>
</KeyFrame>
</Timeline>
</Timelines>
</XuiVisual>

View file

@ -802,6 +802,9 @@ void CGameNetworkManager::CancelJoinGame(LPVOID lpParam)
#ifdef _XBOX_ONE
s_pPlatformNetworkManager->CancelJoinGame();
#endif
#ifdef _WINDOWS64
WinsockNetLayer::CancelJoinGame();
#endif
}
bool CGameNetworkManager::LeaveGame(bool bMigrateHost)

View file

@ -175,8 +175,6 @@ bool CPlatformNetworkManagerStub::Initialise(CGameNetworkManager *pGameNetworkMa
m_bIsOfflineGame = false;
#ifdef _WINDOWS64
m_bJoinPending = false;
m_joinLocalUsersMask = 0;
m_joinHostName[0] = 0;
#endif
m_pSearchParam = nullptr;
m_SessionsUpdatedCallback = nullptr;
@ -288,6 +286,8 @@ void CPlatformNetworkManagerStub::DoWork()
}
}
// Async join finalization: when the background thread reports success,
// register players and transition the session to starting state.
if (m_bJoinPending)
{
WinsockNetLayer::eJoinState state = WinsockNetLayer::GetJoinState();
@ -296,7 +296,6 @@ void CPlatformNetworkManagerStub::DoWork()
WinsockNetLayer::FinalizeJoin();
BYTE localSmallId = WinsockNetLayer::GetLocalSmallId();
IQNet::m_player[localSmallId].m_smallId = localSmallId;
IQNet::m_player[localSmallId].m_isRemote = false;
IQNet::m_player[localSmallId].m_isHostPlayer = false;
@ -548,17 +547,15 @@ int CPlatformNetworkManagerStub::JoinGame(FriendSessionInfo* searchResult, int l
IQNet::m_player[0].m_smallId = 0;
IQNet::m_player[0].m_isRemote = true;
IQNet::m_player[0].m_isHostPlayer = true;
// Remote host still maps to legacy host XUID in mixed old/new sessions.
IQNet::m_player[0].m_resolvedXuid = Win64Xuid::GetLegacyEmbeddedHostXuid();
wcsncpy_s(IQNet::m_player[0].m_gamertag, 32, searchResult->data.hostName, _TRUNCATE);
WinsockNetLayer::StopDiscovery();
wcsncpy_s(m_joinHostName, 32, searchResult->data.hostName, _TRUNCATE);
m_joinLocalUsersMask = localUsersMask;
if (!WinsockNetLayer::BeginJoinGame(hostIP, hostPort))
{
app.DebugPrintf("Win64 LAN: Failed to connect to %s:%d\n", hostIP, hostPort);
app.DebugPrintf("Win64 LAN: Failed to start async join to %s:%d\n", hostIP, hostPort);
return CGameNetworkManager::JOINGAME_FAIL_GENERAL;
}
@ -978,6 +975,13 @@ void CPlatformNetworkManagerStub::ForceFriendsSessionRefresh()
delete m_pSearchResults[i];
m_pSearchResults[i] = nullptr;
}
#ifdef _WINDOWS64
// Immediately rebuild the session list from servers.db so that
// edits/deletions are visible as soon as the UI regains focus,
// rather than waiting for the next TickSearch() cycle.
SearchForGames();
#endif
}
INetworkPlayer *CPlatformNetworkManagerStub::addNetworkPlayer(IQNetPlayer *pQNetPlayer)

View file

@ -76,11 +76,8 @@ private:
bool m_bIsOfflineGame;
bool m_bIsPrivateGame;
int m_flagIndexSize;
#ifdef _WINDOWS64
bool m_bJoinPending;
int m_joinLocalUsersMask;
wchar_t m_joinHostName[32];
#endif
// This is only maintained by the host, and is not valid on client machines

View file

@ -5,6 +5,8 @@
#include "../../../Minecraft.World/net.minecraft.world.item.h"
#include "../../../Minecraft.World/net.minecraft.world.entity.ai.attributes.h"
#include "../../../Minecraft.World/net.minecraft.world.entity.monster.h"
#include "../../MultiPlayerLevel.h"
#include "../../../Minecraft.World\LevelData.h"
#include "IUIScene_HUD.h"
#include "UI.h"
@ -20,6 +22,7 @@ IUIScene_HUD::IUIScene_HUD()
m_lastMaxHealth = 20;
m_lastHealthBlink = false;
m_lastHealthPoison = false;
m_lastHealthHardcore = false;
m_iCurrentFood = -1;
m_lastFoodPoison = false;
m_lastAir = 10;
@ -94,9 +97,10 @@ void IUIScene_HUD::updateFrameTick()
ShowHealth(false);
ShowFood(false);
ShowAir(false);
ShowArmour(false);
ShowArmour(false);
ShowExpBar(false);
SetHealthAbsorb(0);
SetHealthAbsorb(0);
SetHardcoreMode(false);
}
if(pMinecraft->localplayers[iPad]->isRidingJumpable())
@ -206,6 +210,12 @@ void IUIScene_HUD::renderPlayerHealth()
// Update armour
int armor = pMinecraft->localplayers[iPad]->getArmorValue();
// Check hardcore mode
bool bHardcore = pMinecraft->level != nullptr
&& pMinecraft->level->getLevelData() != nullptr
&& pMinecraft->level->getLevelData()->isHardcore();
SetHardcoreMode(bHardcore);
SetHealth(currentHealth, oldHealth, blink, bHasPoison || bHasWither, bHasWither);
SetHealthAbsorb(totalAbsorption);

View file

@ -11,6 +11,7 @@ protected:
int m_iCurrentHealth;
int m_lastMaxHealth;
bool m_lastHealthBlink, m_lastHealthPoison, m_lastHealthWither;
bool m_lastHealthHardcore;
int m_iCurrentFood;
bool m_lastFoodPoison;
int m_lastAir, m_currentExtraAir;
@ -46,6 +47,7 @@ protected:
virtual void SetActiveSlot(int slot) = 0;
virtual void SetHealth(int iHealth, int iLastHealth, bool bBlink, bool bPoison, bool bWither) = 0;
virtual void SetHardcoreMode(bool bHardcore) = 0;
virtual void SetFood(int iFood, int iLastFood, bool bPoison) = 0;
virtual void SetAir(int iAir, int extra) = 0;
virtual void SetArmour(int iArmour) = 0;

View file

@ -410,11 +410,72 @@ int IUIScene_PauseMenu::ExitWorldThreadProc( void* lpParameter )
return S_OK;
}
#ifdef _WINDOWS64
static bool Win64_DeleteSaveDirectory(const wchar_t* wPath)
{
wchar_t wSearch[MAX_PATH];
swprintf_s(wSearch, MAX_PATH, L"%s\\*", wPath);
WIN32_FIND_DATAW fd;
HANDLE hFind = FindFirstFileW(wSearch, &fd);
if (hFind != INVALID_HANDLE_VALUE)
{
do
{
if (wcscmp(fd.cFileName, L".") == 0 || wcscmp(fd.cFileName, L"..") == 0) continue;
wchar_t wChild[MAX_PATH];
swprintf_s(wChild, MAX_PATH, L"%s\\%s", wPath, fd.cFileName);
if (fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
Win64_DeleteSaveDirectory(wChild);
else
DeleteFileW(wChild);
} while (FindNextFileW(hFind, &fd));
FindClose(hFind);
}
return RemoveDirectoryW(wPath) != 0;
}
#endif // _WINDOWS64
// This function performs the meat of exiting from a level. It should be called from a thread other than the main thread.
void IUIScene_PauseMenu::_ExitWorld(LPVOID lpParameter)
{
Minecraft *pMinecraft=Minecraft::GetInstance();
// 4J Added: Capture hardcore delete info before the server is destroyed
#ifdef _WINDOWS64
bool shouldDeleteHardcoreWorld = false;
wstring hardcoreSaveFolderName;
if (MinecraftServer::getInstance() != nullptr && MinecraftServer::getInstance()->getDeleteWorldOnExit())
{
shouldDeleteHardcoreWorld = true;
// Try 1: Use the save folder name stored by UIScene_LoadMenu::StartGameFromSave (works for existing saves)
hardcoreSaveFolderName = app.GetCurrentSaveFolderName();
if (!hardcoreSaveFolderName.empty())
{
app.DebugPrintf("Hardcore mode: save folder from app = '%ls'\n", hardcoreSaveFolderName.c_str());
}
// Try 2: StorageManager (may work for new saves after first autosave)
if (hardcoreSaveFolderName.empty())
{
char szSaveFolder[MAX_SAVEFILENAME_LENGTH] = {};
StorageManager.GetSaveUniqueFilename(szSaveFolder);
if (szSaveFolder[0] != '\0')
{
wchar_t wSaveFolder[MAX_SAVEFILENAME_LENGTH] = {};
mbstowcs(wSaveFolder, szSaveFolder, MAX_SAVEFILENAME_LENGTH - 1);
hardcoreSaveFolderName = wSaveFolder;
app.DebugPrintf("Hardcore mode: save folder from StorageManager = '%s'\n", szSaveFolder);
}
}
// Try 3: Stored during loadLevel
if (hardcoreSaveFolderName.empty())
{
hardcoreSaveFolderName = MinecraftServer::getInstance()->getSaveFolderName();
app.DebugPrintf("Hardcore mode: save folder from server = '%ls'\n", hardcoreSaveFolderName.c_str());
}
MinecraftServer::getInstance()->setDeleteWorldOnExit(false);
}
#endif
int exitReasonStringId = pMinecraft->progressRenderer->getCurrentTitle();
int exitReasonTitleId = IDS_CONNECTION_LOST;
@ -625,6 +686,17 @@ void IUIScene_PauseMenu::_ExitWorld(LPVOID lpParameter)
{
Sleep(1);
}
// 4J Added: Hardcore mode — delete world save data now that the server is fully stopped
#ifdef _WINDOWS64
if (shouldDeleteHardcoreWorld && !hardcoreSaveFolderName.empty())
{
wchar_t wFolderPath[MAX_PATH] = {};
swprintf_s(wFolderPath, MAX_PATH, L"Windows64\\GameHDD\\%s", hardcoreSaveFolderName.c_str());
app.DebugPrintf("Hardcore mode: Deleting world save folder '%ls'\n", wFolderPath);
Win64_DeleteSaveDirectory(wFolderPath);
}
#endif
pMinecraft->setLevel(nullptr,exitReasonStringId,nullptr,saveStats);
TelemetryManager->Flush();

View file

@ -141,6 +141,9 @@ S32 UIBitmapFont::GetCodepointGlyph(U32 codepoint)
// 4J-JEV: Change "right single quotation marks" to apostrophies.
if (codepoint == 0x2019) codepoint = 0x27;
if (!m_cFontData->hasGlyph(codepoint))
return IGGY_GLYPH_INVALID;
return m_cFontData->getGlyphId(codepoint);
}
@ -253,19 +256,6 @@ rrbool UIBitmapFont::GetGlyphBitmap(S32 glyph,F32 pixel_scale,IggyBitmapCharacte
while ( (0.5f + glyphScale) * truePixelScale < pixel_scale)
glyphScale++;
// Debug: log each unique (font, pixel_scale) pair
{
static std::unordered_set<int> s_loggedScaleKeys;
// Encode font pointer + quantized scale into a key to log each combo once
int scaleKey = (int)(pixel_scale * 100.0f) ^ (int)(uintptr_t)m_cFontData;
if (s_loggedScaleKeys.find(scaleKey) == s_loggedScaleKeys.end() && s_loggedScaleKeys.size() < 50) {
s_loggedScaleKeys.insert(scaleKey);
float tps = truePixelScale;
app.DebugPrintf("[FONT-DBG] GetGlyphBitmap: font=%s glyph=%d pixel_scale=%.3f truePixelScale=%.1f glyphScale=%.0f\n",
m_cFontData->getFontName().c_str(), glyph, pixel_scale, tps, glyphScale);
}
}
// 4J-JEV: Debug code to check which font sizes are being used.
#if (!defined _CONTENT_PACKAGE) && (VERBOSE_FONT_OUTPUT > 0)

View file

@ -1,8 +1,10 @@
#include "stdafx.h"
#include "UI.h"
#include "UIControl.h"
#include "../../../Minecraft.World/StringHelpers.h"
#include "../../../Minecraft.World/JavaMath.h"
#include "../../../Minecraft.World/ArabicShaping.h"
UIControl_Base::UIControl_Base()
{
@ -47,13 +49,16 @@ void UIControl_Base::tick()
//app.DebugPrintf("Calling SetLabel - '%ls'\n", m_label.c_str());
m_bLabelChanged = false;
// Shape the text before sending to Iggy; m_label stays unshaped for future updates
wstring shaped = shapeArabicText(m_label.getString());
IggyDataValue result;
IggyDataValue value[1];
value[0].type = IGGY_DATATYPE_string_UTF16;
IggyStringUTF16 stringVal;
stringVal.string = (IggyUTF16*) m_label.c_str();
stringVal.length = m_label.length();
stringVal.string = (IggyUTF16*) shaped.c_str();
stringVal.length = (int)shaped.length();
value[0].string16 = stringVal;
IggyResult out = IggyPlayerCallMethodRS ( m_parentScene->getMovie() , &result, getIggyValuePath() , m_setLabelFunc , 1 , value );
@ -71,13 +76,16 @@ void UIControl_Base::setLabel(UIString label, bool instant, bool force)
{
m_bLabelChanged = false;
// Shape the text before sending to Iggy; m_label stays unshaped for future updates
wstring shaped = shapeArabicText(m_label.getString());
IggyDataValue result;
IggyDataValue value[1];
value[0].type = IGGY_DATATYPE_string_UTF16;
IggyStringUTF16 stringVal;
stringVal.string = (IggyUTF16*)m_label.c_str();
stringVal.length = m_label.length();
stringVal.string = (IggyUTF16*) shaped.c_str();
stringVal.length = (int)shaped.length();
value[0].string16 = stringVal;
IggyResult out = IggyPlayerCallMethodRS ( m_parentScene->getMovie() , &result, getIggyValuePath() , m_setLabelFunc , 1 , value );

View file

@ -2,6 +2,7 @@
#include "UI.h"
#include "UIControl_Label.h"
#include "..\..\..\Minecraft.World\StringHelpers.h"
#include "..\..\..\Minecraft.World\ArabicShaping.h"
UIControl_Label::UIControl_Label()
{
@ -38,13 +39,15 @@ void UIControl_Label::init(UIString label, int id)
m_label = label;
m_id = id;
wstring shaped = shapeArabicText(m_label.getString());
IggyDataValue result;
IggyDataValue value[2];
value[0].type = IGGY_DATATYPE_string_UTF16;
IggyStringUTF16 stringVal;
stringVal.string = (IggyUTF16*)label.c_str();
stringVal.length = label.length();
stringVal.string = (IggyUTF16*)shaped.c_str();
stringVal.length = (int)shaped.length();
value[0].string16 = stringVal;
value[1].type = IGGY_DATATYPE_number;

View file

@ -15,6 +15,56 @@
//#define SKIN_PREVIEW_BOB_ANIM
#define SKIN_PREVIEW_WALKING_ANIM
#ifdef _WINDOWS64
// Frame-rate-independent animation scaling.
// The skin preview animations were designed for ~60fps (VSync on).
// With uncapped FPS, each frame's contribution must be scaled down.
// The scale is computed once per frame (keyed by frame counter) so
// that multiple skin previews rendered in the same frame all use the
// same value instead of measuring near-zero deltas between each other.
static float s_skinAnimCachedScale = 1.0f;
static double s_skinAnimLastTime = 0.0;
static double s_skinAnimFreqInv = 0.0;
static float GetSkinAnimDeltaScale()
{
// Use the main loop's frame counter to detect a new frame.
// GetTickCount changes every ~16ms, but we need per-frame detection.
// Use a simple time threshold: if <0.1ms since last call, same frame.
if (s_skinAnimFreqInv == 0.0)
{
LARGE_INTEGER freq;
QueryPerformanceFrequency(&freq);
s_skinAnimFreqInv = 1.0 / (double)freq.QuadPart;
}
LARGE_INTEGER now;
QueryPerformanceCounter(&now);
double currentTime = (double)now.QuadPart * s_skinAnimFreqInv;
// If less than 0.5ms since last call, assume same frame -- reuse cached scale
double elapsed = currentTime - s_skinAnimLastTime;
if (s_skinAnimLastTime != 0.0 && elapsed < 0.0005)
{
return s_skinAnimCachedScale;
}
if (s_skinAnimLastTime == 0.0)
{
s_skinAnimLastTime = currentTime;
s_skinAnimCachedScale = 1.0f;
return 1.0f;
}
s_skinAnimLastTime = currentTime;
const double kBaselineFrameTime = 1.0 / 60.0;
float scale = static_cast<float>(elapsed / kBaselineFrameTime);
if (scale > 3.0f) scale = 3.0f;
s_skinAnimCachedScale = scale;
return scale;
}
#endif
UIControl_PlayerSkinPreview::UIControl_PlayerSkinPreview()
{
UIControl::setControlType(UIControl::ePlayerSkinPreview);
@ -306,7 +356,11 @@ void UIControl_PlayerSkinPreview::render(EntityRenderer *renderer, double x, dou
break;
case e_SkinPreviewAnimation_Attacking:
model->holdingRightHand = true;
#ifdef _WINDOWS64
m_swingTime += GetSkinAnimDeltaScale();
#else
m_swingTime++;
#endif
if (m_swingTime >= (Player::SWING_DURATION * 3) )
{
m_swingTime = 0;
@ -359,8 +413,16 @@ void UIControl_PlayerSkinPreview::render(EntityRenderer *renderer, double x, dou
#ifdef SKIN_PREVIEW_WALKING_ANIM
m_walkAnimSpeedO = m_walkAnimSpeed;
#ifdef _WINDOWS64
{
float animScale = GetSkinAnimDeltaScale();
m_walkAnimSpeed += (0.1f - m_walkAnimSpeed) * 0.4f * animScale;
m_walkAnimPos += m_walkAnimSpeed * animScale;
}
#else
m_walkAnimSpeed += (0.1f - m_walkAnimSpeed) * 0.4f;
m_walkAnimPos += m_walkAnimSpeed;
#endif
float ws = m_walkAnimSpeedO + (m_walkAnimSpeed - m_walkAnimSpeedO) * a;
float wp = m_walkAnimPos - m_walkAnimSpeed * (1 - a);
#else

View file

@ -1,6 +1,7 @@
#include "stdafx.h"
#include "UI.h"
#include "UIControl_SaveList.h"
#include "..\..\..\Minecraft.World\ArabicShaping.h"
bool UIControl_SaveList::setupControl(UIScene *scene, IggyValuePath *parent, const string &controlName)
{
@ -69,12 +70,14 @@ void UIControl_SaveList::addItem(const string &label, const wstring &iconName, i
void UIControl_SaveList::addItem(const wstring &label, const wstring &iconName, int data)
{
wstring shaped = shapeArabicText(label);
IggyDataValue result;
IggyDataValue value[3];
IggyStringUTF16 stringVal;
stringVal.string = (IggyUTF16*)label.c_str();
stringVal.length = static_cast<S32>(label.length());
stringVal.string = (IggyUTF16*)shaped.c_str();
stringVal.length = static_cast<S32>(shaped.length());
value[0].type = IGGY_DATATYPE_string_UTF16;
value[0].string16 = stringVal;

View file

@ -220,7 +220,7 @@ UIControl_TextInput::EDirectEditResult UIControl_TextInput::tickDirectEdit()
sanitized.reserve(pasted.length());
for (wchar_t pc : pasted)
{
{
if (pc >= 0x20) // Keep printable characters
{
if (m_iCharLimit > 0 && (m_editBuffer.length() + sanitized.length()) >= (size_t)m_iCharLimit)

View file

@ -13,6 +13,7 @@
#include "../../EnderDragonRenderer.h"
#include "../../MultiPlayerLocalPlayer.h"
#include "UIFontData.h"
#include "UIUnicodeBitmapFont.h"
#include "UISplitScreenHelpers.h"
#ifdef _WINDOWS64
#include "../../Windows64/KeyboardMouseInput.h"
@ -193,6 +194,7 @@ UIController::UIController()
m_mcTTFFont = nullptr;
m_moj7 = nullptr;
m_moj11 = nullptr;
m_unicodeBitmapFont = nullptr;
// 4J-JEV: It's important that these remain the same, unless updateCurrentLanguage is going to be called.
m_eCurrentFont = m_eTargetFont = eFont_NotLoaded;
@ -307,6 +309,14 @@ void UIController::postInit()
IggySetAS3ExternalFunctionCallbackUTF16 ( &UIController::ExternalFunctionCallback, this );
IggySetTextureSubstitutionCallbacks ( &UIController::TextureSubstitutionCreateCallback , &UIController::TextureSubstitutionDestroyCallback, this );
// Load a unicode bitmap font as Iggy's global fallback for characters not
// covered by the Mojangles bitmap font (CJK, Thai, Arabic, Korean, etc.).
// Uses the same glyph page PNGs as the legacy Font class, with matching
// Mojangles metrics for correct vertical alignment.
m_unicodeBitmapFont = new UIUnicodeBitmapFont("Mojangles_Unicode_Bitmap", SFontData::Mojangles_7);
m_unicodeBitmapFont->registerFont();
IggyFontSetFallbackFontUTF8("Mojangles_Unicode_Bitmap", -1, IGGY_FONTFLAG_none);
SetupFont();
//
loadSkins();

View file

@ -7,6 +7,7 @@ using namespace std;
class UIAbstractBitmapFont;
class UIBitmapFont;
class UIUnicodeBitmapFont;
class UITTFFont;
class UIComponent_DebugUIConsole;
class UIComponent_DebugUIMarketingGuide;
@ -63,6 +64,7 @@ private:
UIAbstractBitmapFont *m_mcBitmapFont;
UITTFFont *m_mcTTFFont;
UIBitmapFont *m_moj7, *m_moj11;
UIUnicodeBitmapFont *m_unicodeBitmapFont;
std::mt19937 m_randomGenerator;
std::uniform_real_distribution<float> m_randomDistribution;

View file

@ -335,6 +335,11 @@ bool CFontData::unicodeIsWhitespace(unsigned int unicode)
return false;
}
bool CFontData::hasGlyph(unsigned int unicodepoint)
{
return m_unicodeMap.find(unicodepoint) != m_unicodeMap.end();
}
void CFontData::moveCursor(unsigned char *&cursor, unsigned int dx, unsigned int dy)
{
cursor += (dy * m_sFontData->m_uiGlyphMapX) + dx;

View file

@ -125,6 +125,9 @@ public:
// Returns true if this unicodepoint is whitespace
bool unicodeIsWhitespace(unsigned int unicodepoint);
// Returns true if this unicodepoint exists in the font's glyph map.
bool hasGlyph(unsigned int unicodepoint);
private:
// Move a pointer in an image dx pixels right and dy pixels down, wrap around in either dimension leads to unknown behaviour.

View file

@ -4,7 +4,6 @@
#include "..\..\Minecraft.h"
#ifdef _WINDOWS64
#include "..\..\Windows64\Network\WinsockNetLayer.h"
#include "..\..\..\Minecraft.World\DisconnectPacket.h"
static int ConnectingProgress_OnRejectedDialogOK(LPVOID, int iPad, const C4JStorage::EMessageResult)
{
@ -53,10 +52,10 @@ UIScene_ConnectingProgress::UIScene_ConnectingProgress(int iPad, void *_initData
m_cancelFuncParam = param->cancelFuncParam;
m_removeLocalPlayer = false;
m_showingButton = false;
#ifdef _WINDOWS64
WinsockNetLayer::eJoinState initState = WinsockNetLayer::GetJoinState();
m_asyncJoinActive = (initState != WinsockNetLayer::eJoinState_Idle && initState != WinsockNetLayer::eJoinState_Cancelled);
m_asyncJoinActive = (initState == WinsockNetLayer::eJoinState_Connecting ||
initState == WinsockNetLayer::eJoinState_Success);
m_asyncJoinFailed = false;
#endif
}
@ -72,12 +71,12 @@ void UIScene_ConnectingProgress::updateTooltips()
#ifdef _WINDOWS64
if (m_asyncJoinActive)
{
ui.SetTooltips( m_iPad, -1, IDS_TOOLTIPS_BACK);
ui.SetTooltips(m_iPad, -1, IDS_TOOLTIPS_BACK);
return;
}
if (m_asyncJoinFailed)
{
ui.SetTooltips( m_iPad, IDS_TOOLTIPS_SELECT, -1);
ui.SetTooltips(m_iPad, IDS_TOOLTIPS_SELECT, -1);
return;
}
#endif
@ -94,78 +93,54 @@ void UIScene_ConnectingProgress::tick()
if (m_asyncJoinActive)
{
WinsockNetLayer::eJoinState state = WinsockNetLayer::GetJoinState();
if (state == WinsockNetLayer::eJoinState_Connecting)
switch (state)
{
case WinsockNetLayer::eJoinState_Connecting:
{
// connecting.............
int attempt = WinsockNetLayer::GetJoinAttempt();
int maxAttempts = WinsockNetLayer::GetJoinMaxAttempts();
char buf[128];
if (attempt <= 1)
sprintf_s(buf, "Connecting...");
wchar_t buf[128];
if (attempt > 1)
swprintf_s(buf, L"Connecting... (attempt %d/%d)", attempt, maxAttempts);
else
sprintf_s(buf, "Connecting failed, trying again (%d/%d)", attempt, maxAttempts);
wchar_t wbuf[128];
mbstowcs(wbuf, buf, 128);
m_labelTitle.setLabel(wstring(wbuf));
swprintf_s(buf, L"Connecting...");
m_labelTitle.setLabel(buf);
break;
}
else if (state == WinsockNetLayer::eJoinState_Success)
{
case WinsockNetLayer::eJoinState_Success:
m_asyncJoinActive = false;
// go go go
}
else if (state == WinsockNetLayer::eJoinState_Cancelled)
{
// cancel
m_labelTitle.setLabel(L"Joining world...");
break;
case WinsockNetLayer::eJoinState_Cancelled:
m_asyncJoinActive = false;
navigateBack();
}
else if (state == WinsockNetLayer::eJoinState_Rejected)
break;
case WinsockNetLayer::eJoinState_Rejected:
{
// server full and banned are passed differently compared to other disconnects it seems
m_asyncJoinActive = false;
DisconnectPacket::eDisconnectReason reason = WinsockNetLayer::GetJoinRejectReason();
int exitReasonStringId;
switch (reason)
{
case DisconnectPacket::eDisconnect_ServerFull:
exitReasonStringId = IDS_DISCONNECTED_SERVER_FULL;
break;
case DisconnectPacket::eDisconnect_Banned:
exitReasonStringId = IDS_DISCONNECTED_KICKED;
break;
default:
exitReasonStringId = IDS_CONNECTION_LOST_SERVER;
break;
}
int reasonStringId = IDS_CONNECTION_LOST_SERVER;
if (reason == DisconnectPacket::eDisconnect_ServerFull)
reasonStringId = IDS_DISCONNECTED_SERVER_FULL;
else if (reason == DisconnectPacket::eDisconnect_Kicked)
reasonStringId = IDS_DISCONNECTED_KICKED;
UINT uiIDA[1];
uiIDA[0] = IDS_CONFIRM_OK;
ui.RequestErrorMessage(IDS_CONNECTION_FAILED, exitReasonStringId, uiIDA, 1, ProfileManager.GetPrimaryPad(), ConnectingProgress_OnRejectedDialogOK, nullptr, nullptr);
ui.RequestErrorMessage(IDS_CONNECTION_FAILED, reasonStringId, uiIDA, 1, m_iPad, ConnectingProgress_OnRejectedDialogOK, nullptr);
break;
}
else if (state == WinsockNetLayer::eJoinState_Failed)
{
// FAIL
case WinsockNetLayer::eJoinState_Failed:
m_asyncJoinActive = false;
m_asyncJoinFailed = true;
int maxAttempts = WinsockNetLayer::GetJoinMaxAttempts();
char buf[256];
sprintf_s(buf, "Failed to connect after %d attempts. The server may be unavailable.", maxAttempts);
wchar_t wbuf[256];
mbstowcs(wbuf, buf, 256);
// TIL that these exist
// not going to use a actual popup due to it requiring messing with strings which can really mess things up
// i dont trust myself with that
// these need to be touched up later as teh button is a bit offset
m_labelTitle.setLabel(L"Unable to connect to server");
m_progressBar.setLabel(wstring(wbuf));
m_progressBar.showBar(false);
m_progressBar.setVisible(true);
m_labelTitle.setLabel(app.GetString(IDS_CONNECTION_FAILED));
m_buttonConfirm.setVisible(true);
m_showingButton = true;
m_controlTimer.setVisible(false);
updateTooltips();
break;
default:
break;
}
return;
}
#endif
@ -202,7 +177,6 @@ void UIScene_ConnectingProgress::handleGainFocus(bool navBack)
void UIScene_ConnectingProgress::handleLoseFocus()
{
if (!m_runFailTimer) return;
int millisecsLeft = getTimer(0)->targetTime - System::currentTimeMillis();
int millisecsTaken = getTimer(0)->duration - millisecsLeft;
app.DebugPrintf("\n");
@ -316,7 +290,6 @@ void UIScene_ConnectingProgress::handleInput(int iPad, int key, bool repeat, boo
switch(key)
{
// 4J-PB - Removed the option to cancel join - it didn't work anyway
#ifdef _WINDOWS64
case ACTION_MENU_CANCEL:
if (pressed && m_asyncJoinActive)
@ -325,9 +298,11 @@ void UIScene_ConnectingProgress::handleInput(int iPad, int key, bool repeat, boo
WinsockNetLayer::CancelJoinGame();
navigateBack();
handled = true;
return;
}
break;
#endif
// 4J-PB - Removed the option to cancel join - it didn't work anyway
// case ACTION_MENU_CANCEL:
// {
// if(m_cancelFunc != nullptr)
@ -374,8 +349,8 @@ void UIScene_ConnectingProgress::handlePress(F64 controlId, F64 childId)
if (m_asyncJoinFailed)
{
navigateBack();
break;
}
else
#endif
if( m_iPad != ProfileManager.GetPrimaryPad() && g_NetworkManager.IsInSession() )
{

View file

@ -12,7 +12,6 @@ private:
bool m_showingButton;
void (*m_cancelFunc)(LPVOID param);
LPVOID m_cancelFuncParam;
#ifdef _WINDOWS64
bool m_asyncJoinActive;
bool m_asyncJoinFailed;

View file

@ -26,6 +26,8 @@
#define GAME_CREATE_ONLINE_TIMER_ID 0
#define GAME_CREATE_ONLINE_TIMER_TIME 100
static bool s_bHardcore = false; // 4J Added: tracks when difficulty slider is at Hardcore position (file-scope to avoid header layout changes)
int UIScene_CreateWorldMenu::m_iDifficultyTitleSettingA[4]=
{
IDS_DIFFICULTY_TITLE_PEACEFUL,
@ -59,8 +61,9 @@ UIScene_CreateWorldMenu::UIScene_CreateWorldMenu(int iPad, void *initData, UILay
WCHAR TempString[256];
swprintf( (WCHAR *)TempString, 256, L"%ls: %ls", app.GetString( IDS_SLIDER_DIFFICULTY ),app.GetString(m_iDifficultyTitleSettingA[app.GetGameSettings(m_iPad,eGameSetting_Difficulty)]));
m_sliderDifficulty.init(TempString,eControl_Difficulty,0,3,app.GetGameSettings(m_iPad,eGameSetting_Difficulty));
m_sliderDifficulty.init(TempString,eControl_Difficulty,0,4,app.GetGameSettings(m_iPad,eGameSetting_Difficulty));
s_bHardcore = false;
m_MoreOptionsParams.bGenerateOptions=TRUE;
m_MoreOptionsParams.bStructures=TRUE;
m_MoreOptionsParams.bFlatWorld=FALSE;
@ -458,6 +461,8 @@ void UIScene_CreateWorldMenu::handlePress(F64 controlId, F64 childId)
}
break;
case eControl_GameModeToggle:
if (s_bHardcore)
break; // Hardcore mode locks game mode to Survival
switch(m_iGameModeId)
{
case 0: // Creative
@ -470,7 +475,7 @@ void UIScene_CreateWorldMenu::handlePress(F64 controlId, F64 childId)
m_iGameModeId = GameType::ADVENTURE->getId();
m_bGameModeCreative = false;
break;
case 2: // Survival
case 2: // Survival
m_buttonGamemode.setLabel(app.GetString(IDS_GAMEMODE_SURVIVAL));
m_iGameModeId = GameType::SURVIVAL->getId();
m_bGameModeCreative = false;
@ -654,9 +659,22 @@ void UIScene_CreateWorldMenu::handleSliderMove(F64 sliderId, F64 currentValue)
case eControl_Difficulty:
m_sliderDifficulty.handleSliderMove(value);
app.SetGameSettings(m_iPad,eGameSetting_Difficulty,value);
swprintf( (WCHAR *)TempString, 256, L"%ls: %ls", app.GetString( IDS_SLIDER_DIFFICULTY ),app.GetString(m_iDifficultyTitleSettingA[value]));
// 4J Added: Difficulty value 4 = Hardcore (store actual difficulty as Hard, track hardcore separately)
s_bHardcore = (value >= 4);
app.SetGameSettings(m_iPad, eGameSetting_Difficulty, s_bHardcore ? 3 : value);
if (value >= 4)
swprintf( (WCHAR *)TempString, 256, L"%ls: %ls", app.GetString( IDS_SLIDER_DIFFICULTY ), L"Hardcore");
else
swprintf( (WCHAR *)TempString, 256, L"%ls: %ls", app.GetString( IDS_SLIDER_DIFFICULTY ),app.GetString(m_iDifficultyTitleSettingA[value]));
m_sliderDifficulty.setLabel(TempString);
// Hardcore locks game mode to Survival
if (s_bHardcore && m_iGameModeId != GameType::SURVIVAL->getId())
{
m_iGameModeId = GameType::SURVIVAL->getId();
m_bGameModeCreative = false;
m_buttonGamemode.setLabel(app.GetString(IDS_GAMEMODE_SURVIVAL));
}
break;
}
}
@ -823,7 +841,7 @@ void UIScene_CreateWorldMenu::checkStateAndStartGame()
// 4J Stu - This is a bit messy and is due to the library incorrectly returning false for IsSignedInLive if the npAvailability isn't SCE_OK
UINT uiIDA[1];
uiIDA[0]=IDS_OK;
ui.RequestErrorMessage(IDS_ONLINE_SERVICE_TITLE, IDS_CONTENT_RESTRICTION, uiIDA, 1, iPadNotSignedInLive);
ui.RequestAlertMessage(IDS_ONLINE_SERVICE_TITLE, IDS_CONTENT_RESTRICTION, uiIDA, 1, iPadNotSignedInLive);
}
else
{
@ -1113,6 +1131,10 @@ void UIScene_CreateWorldMenu::CreateGame(UIScene_CreateWorldMenu* pClass, DWORD
StorageManager.ResetSaveData();
// Make our next save default to the name of the level
StorageManager.SetSaveTitle((wchar_t *)wWorldName.c_str());
#ifdef _WINDOWS64
// New world — save folder doesn't exist yet, clear for now (will be set after first autosave)
app.SetCurrentSaveFolderName(L"");
#endif
wstring wSeed;
if(!pClass->m_MoreOptionsParams.seed.empty() )
@ -1179,7 +1201,9 @@ void UIScene_CreateWorldMenu::CreateGame(UIScene_CreateWorldMenu* pClass, DWORD
Minecraft *pMinecraft = Minecraft::GetInstance();
pMinecraft->skins->selectTexturePackById(pClass->m_MoreOptionsParams.dwTexturePack);
app.SetGameHostOption(eGameHostOption_Difficulty,Minecraft::GetInstance()->options->difficulty);
// 4J Added: If hardcore was selected on difficulty slider, set difficulty to Hard and enable hardcore flag
app.SetGameHostOption(eGameHostOption_Difficulty, Minecraft::GetInstance()->options->difficulty);
app.SetGameHostOption(eGameHostOption_Hardcore, s_bHardcore ? 1 : 0);
app.SetGameHostOption(eGameHostOption_FriendsOfFriends,pClass->m_MoreOptionsParams.bAllowFriendsOfFriends);
app.SetGameHostOption(eGameHostOption_Gamertags,app.GetGameSettings(pClass->m_iPad,eGameSetting_GamertagsVisible)?1:0);

View file

@ -2,25 +2,46 @@
#include "UI.h"
#include "UIScene_DeathMenu.h"
#include "IUIScene_PauseMenu.h"
#include "../../Minecraft.h"
#include "../../MultiPlayerLocalPlayer.h"
#include "../../MultiPlayerLevel.h"
#include "../../MinecraftServer.h"
#include "../../../Minecraft.World/net.minecraft.world.level.storage.h"
UIScene_DeathMenu::UIScene_DeathMenu(int iPad, void *initData, UILayer *parentLayer) : UIScene(iPad, parentLayer)
{
// Setup all the Iggy references we need for this scene
initialiseMovie();
m_buttonRespawn.init(app.GetString(IDS_RESPAWN),eControl_Respawn);
m_buttonExitGame.init(app.GetString(IDS_EXIT_GAME),eControl_ExitGame);
m_labelTitle.setLabel(app.GetString(IDS_YOU_DIED));
// 4J Added: In hardcore mode, disable respawn and show hardcore death message
Minecraft *pMC = Minecraft::GetInstance();
bool isHardcore = false;
if (pMC != nullptr && pMC->level != nullptr)
{
isHardcore = pMC->level->getLevelData()->isHardcore();
}
if (isHardcore)
{
m_buttonRespawn.init(app.GetString(IDS_HARDCORE_DEATH_MESSAGE), eControl_Respawn);
m_buttonRespawn.setVisible(false);
}
else
{
m_buttonRespawn.init(app.GetString(IDS_RESPAWN), eControl_Respawn);
}
m_bIgnoreInput = false;
Minecraft *pMinecraft = Minecraft::GetInstance();
if(pMinecraft != nullptr && pMinecraft->localgameModes[iPad] != nullptr )
if(pMC != nullptr && pMC->localgameModes[iPad] != nullptr )
{
TutorialMode *gameMode = static_cast<TutorialMode *>(pMinecraft->localgameModes[iPad]);
TutorialMode *gameMode = static_cast<TutorialMode *>(pMC->localgameModes[iPad]);
// This just allows it to be shown
gameMode->getTutorial()->showTutorialPopup(false);
@ -84,8 +105,16 @@ void UIScene_DeathMenu::handlePress(F64 controlId, F64 childId)
switch(static_cast<int>(controlId))
{
case eControl_Respawn:
m_bIgnoreInput = true;
app.SetAction(m_iPad,eAppAction_Respawn);
{
// 4J Added: Safeguard - don't respawn in hardcore mode
Minecraft *pMC = Minecraft::GetInstance();
if (pMC != nullptr && pMC->level != nullptr && pMC->level->getLevelData()->isHardcore())
{
break;
}
m_bIgnoreInput = true;
app.SetAction(m_iPad,eAppAction_Respawn);
}
#ifdef _DURANGO
//InputManager.SetEnabledGtcButtons(_360_GTC_MENU|_360_GTC_PAUSE|_360_GTC_VIEW);
#endif
@ -109,7 +138,16 @@ void UIScene_DeathMenu::handlePress(F64 controlId, F64 childId)
playTime = static_cast<int>(pMinecraft->localplayers[m_iPad]->getSessionTimer());
}
TelemetryManager->RecordLevelExit(m_iPad, eSen_LevelExitStatus_Failed);
// 4J Added: Hardcore mode — skip save dialog, exit without saving, delete world
if (pMinecraft->level != nullptr && pMinecraft->level->getLevelData()->isHardcore() && g_NetworkManager.IsHost())
{
MinecraftServer::getInstance()->setSaveOnExit(false);
MinecraftServer::getInstance()->setDeleteWorldOnExit(true);
app.SetAction(m_iPad, eAppAction_ExitWorld);
break;
}
#if defined (_XBOX_ONE) || defined(__ORBIS__)
if(g_NetworkManager.IsHost() && StorageManager.GetSaveDisabled())
{

View file

@ -50,13 +50,18 @@ UIScene_EndPoem::UIScene_EndPoem(int iPad, void *initData, UILayer *parentLayer)
Minecraft *pMinecraft = Minecraft::GetInstance();
wstring playerName = L"";
if(pMinecraft->localplayers[ui.GetWinUserIndex()] != nullptr)
unsigned int winIdx = ui.GetWinUserIndex();
if(winIdx < XUSER_MAX_COUNT && pMinecraft->localplayers[winIdx] != nullptr)
{
playerName = escapeXML( pMinecraft->localplayers[ui.GetWinUserIndex()]->getDisplayName() );
playerName = escapeXML( pMinecraft->localplayers[winIdx]->getDisplayName() );
}
else if(pMinecraft->localplayers[ProfileManager.GetPrimaryPad()] != nullptr)
{
playerName = escapeXML( pMinecraft->localplayers[ProfileManager.GetPrimaryPad()]->getDisplayName() );
}
else
{
playerName = escapeXML( pMinecraft->localplayers[ProfileManager.GetPrimaryPad()]->getDisplayName() );
playerName = L"Player";
}
noNoiseString = replaceAll(noNoiseString,L"{*PLAYER*}",playerName);

View file

@ -664,6 +664,23 @@ void UIScene_HUD::SetHorseJumpBarProgress(float progress)
}
}
void UIScene_HUD::SetHardcoreMode(bool bHardcore)
{
IggyDataValue result;
IggyDataValue value[1];
value[0].type = IGGY_DATATYPE_boolean;
value[0].boolval = bHardcore;
IggyResult out = IggyPlayerCallMethodRS ( getMovie() , &result, IggyPlayerRootPath( getMovie() ), m_funcSetHardcore , 1 , value );
// When hardcore state changes, invalidate SetHealth's dirty check
// so hearts are redrawn with the correct frame set on the next tick
if(bHardcore != m_lastHealthHardcore)
{
m_lastHealthHardcore = bHardcore;
m_lastMaxHealth = -1;
}
}
void UIScene_HUD::SetHealthAbsorb(int healthAbsorb)
{
if(m_iCurrentHealthAbsorb != healthAbsorb)

View file

@ -25,6 +25,7 @@ protected:
IggyName m_funcRepositionHud, m_funcSetDisplayName, m_funcSetTooltipsEnabled;
IggyName m_funcSetRidingHorse, m_funcSetHorseHealth, m_funcSetHorseJumpBarProgress;
IggyName m_funcSetHealthAbsorb;
IggyName m_funcSetHardcore;
UI_BEGIN_MAP_ELEMENTS_AND_NAMES(UIScene)
UI_MAP_ELEMENT(m_labelChatText[0],"Label1")
UI_MAP_ELEMENT(m_labelChatText[1],"Label2")
@ -89,6 +90,7 @@ protected:
UI_MAP_NAME(m_funcSetHorseJumpBarProgress, L"SetHorseJumpBarProgress")
UI_MAP_NAME(m_funcSetHealthAbsorb, L"SetHealthAbsorb")
UI_MAP_NAME(m_funcSetHardcore, L"SetHardcore")
UI_END_MAP_ELEMENTS_AND_NAMES()
public:
@ -159,6 +161,8 @@ private:
void SetHealthAbsorb(int healthAbsorb);
void SetHardcoreMode(bool bHardcore);
public:
void SetSelectedLabel(const wstring &label);
void ShowDisplayName(bool show);

View file

@ -1,13 +1,19 @@
#include "stdafx.h"
#include "UI.h"
#include "UIScene_JoinMenu.h"
#include "../../Minecraft.h"
#include "../../TexturePackRepository.h"
#include "../../Options.h"
#include "../../MinecraftServer.h"
#include "../../../Minecraft.World/net.minecraft.world.level.h"
#include "../../../Minecraft.World/net.minecraft.world.h"
#ifdef _WINDOWS64
#include "../../Windows64/Network/WinsockNetLayer.h"
#endif
#define UPDATE_PLAYERS_TIMER_ID 0
#define UPDATE_PLAYERS_TIMER_TIME 30000
@ -587,10 +593,9 @@ void UIScene_JoinMenu::JoinGame(UIScene_JoinMenu* pClass)
if (result == CGameNetworkManager::JOINGAME_PENDING)
{
pClass->m_bIgnoreInput = false;
ConnectionProgressParams *param = new ConnectionProgressParams();
param->iPad = ProfileManager.GetPrimaryPad();
param->stringId = -1;
param->stringId = IDS_PROGRESS_CONNECTING;
param->showTooltips = true;
param->setFailTimer = false;
param->timerTime = 0;
@ -655,16 +660,18 @@ void UIScene_JoinMenu::JoinGame(UIScene_JoinMenu* pClass)
if( exitReasonStringId == -1 )
{
ui.NavigateBack(pClass->m_iPad);
// No specific disconnect reason was set — the server was likely
// unreachable. Show a "Connection Failed" dialog instead of
// silently navigating back so the user knows what happened.
exitReasonStringId = IDS_CONNECTION_LOST_SERVER;
}
else
{
UINT uiIDA[1];
uiIDA[0]=IDS_CONFIRM_OK;
ui.RequestErrorMessage( IDS_CONNECTION_FAILED, exitReasonStringId, uiIDA,1,ProfileManager.GetPrimaryPad());
exitReasonStringId = -1;
ui.NavigateToHomeMenu();
pClass->m_bIgnoreInput = false;
}
}
}

View file

@ -24,12 +24,13 @@
#define CHECKFORAVAILABLETEXTUREPACKS_TIMER_TIME 50
#endif
int UIScene_LoadMenu::m_iDifficultyTitleSettingA[4]=
int UIScene_LoadMenu::m_iDifficultyTitleSettingA[5]=
{
IDS_DIFFICULTY_TITLE_PEACEFUL,
IDS_DIFFICULTY_TITLE_EASY,
IDS_DIFFICULTY_TITLE_NORMAL,
IDS_DIFFICULTY_TITLE_HARD
IDS_DIFFICULTY_TITLE_HARD,
IDS_GAMEMODE_HARDCORE
};
int UIScene_LoadMenu::LoadSaveDataThumbnailReturned(LPVOID lpParam,PBYTE pbThumbnail,DWORD dwThumbnailBytes)
@ -110,6 +111,7 @@ UIScene_LoadMenu::UIScene_LoadMenu(int iPad, void *initData, UILayer *parentLaye
m_bThumbnailGetFailed = false;
m_seed = 0;
m_bIsCorrupt = false;
m_bHardcore = false;
m_bMultiplayerAllowed = ProfileManager.IsSignedInLive( m_iPad ) && ProfileManager.AllowedToPlayMultiplayer(m_iPad);
// 4J-PB - read the settings for the online flag. We'll only save this setting if the user changed it.
@ -253,10 +255,34 @@ UIScene_LoadMenu::UIScene_LoadMenu(int iPad, void *initData, UILayer *parentLaye
{
wchar_t wSaveName[128];
ZeroMemory(wSaveName, sizeof(wSaveName));
mbstowcs(wSaveName, params->saveDetails->UTF8SaveName, 127);
MultiByteToWideChar(CP_UTF8, 0, params->saveDetails->UTF8SaveName, -1, wSaveName, 127);
m_levelName = wstring(wSaveName);
m_labelGameName.init(m_levelName);
}
if (params->saveDetails != nullptr)
{
// Set thumbnail name from save filename (needed for texture display in tick)
wchar_t wFilename[MAX_SAVEFILENAME_LENGTH];
ZeroMemory(wFilename, sizeof(wFilename));
mbstowcs(wFilename, params->saveDetails->UTF8SaveFilename, MAX_SAVEFILENAME_LENGTH - 1);
m_thumbnailName = wFilename;
if (params->saveDetails->pbThumbnailData && params->saveDetails->dwThumbnailSize > 0)
{
m_pbThumbnailData = params->saveDetails->pbThumbnailData;
m_uiThumbnailSize = params->saveDetails->dwThumbnailSize;
m_bSaveThumbnailReady = true;
m_bRetrievingSaveThumbnail = false;
}
m_bHardcore = params->saveDetails->isHardcore;
if (m_bHardcore)
{
WCHAR TempString[256];
swprintf((WCHAR *)TempString, 256, L"%ls: %ls", app.GetString(IDS_SLIDER_DIFFICULTY), L"Hardcore");
m_sliderDifficulty.init(TempString, eControl_Difficulty, 0, 4, 4);
}
}
#endif
}
@ -546,6 +572,19 @@ void UIScene_LoadMenu::tick()
{
m_MoreOptionsParams.bAllowFriendsOfFriends = TRUE;
}
m_bHardcore = app.GetGameHostOption(uiHostOptions, eGameHostOption_Hardcore) > 0;
if (m_bHardcore)
{
WCHAR TempString[256];
swprintf( (WCHAR *)TempString, 256, L"%ls: %ls", app.GetString( IDS_SLIDER_DIFFICULTY ), L"Hardcore");
m_sliderDifficulty.init(TempString, eControl_Difficulty, 0, 4, 4);
// Hardcore locks game mode to Survival
m_iGameModeId = GameType::SURVIVAL->getId();
m_bGameModeCreative = false;
m_buttonGamemode.setLabel(app.GetString(IDS_GAMEMODE_SURVIVAL));
}
}
Minecraft *pMinecraft = Minecraft::GetInstance();
@ -699,6 +738,8 @@ void UIScene_LoadMenu::handlePress(F64 controlId, F64 childId)
switch(static_cast<int>(controlId))
{
case eControl_GameMode:
if (m_bHardcore)
break; // Hardcore mode locks game mode to Survival
switch(m_iGameModeId)
{
case 0: // Survival
@ -950,10 +991,15 @@ void UIScene_LoadMenu::handleSliderMove(F64 sliderId, F64 currentValue)
switch(static_cast<int>(sliderId))
{
case eControl_Difficulty:
if (m_bHardcore)
{
m_sliderDifficulty.handleSliderMove(4);
break;
}
m_sliderDifficulty.handleSliderMove(value);
app.SetGameSettings(m_iPad,eGameSetting_Difficulty,value);
swprintf( (WCHAR *)TempString, 256, L"%ls: %ls", app.GetString( IDS_SLIDER_DIFFICULTY ),app.GetString(m_iDifficultyTitleSettingA[value]));
swprintf( (WCHAR *)TempString, 256, L"%ls: %ls", app.GetString( IDS_SLIDER_DIFFICULTY ),app.GetString(m_iDifficultyTitleSettingA[value]));
m_sliderDifficulty.setLabel(TempString);
break;
}
@ -1167,7 +1213,7 @@ void UIScene_LoadMenu::LaunchGame(void)
#if TO_BE_IMPLEMENTED
if(eLoadStatus==C4JStorage::ELoadGame_DeviceRemoved)
{
// disable saving
// disable saving
StorageManager.SetSaveDisabled(true);
StorageManager.SetSaveDeviceSelected(m_iPad,false);
UINT uiIDA[1];
@ -1580,6 +1626,24 @@ void UIScene_LoadMenu::StartGameFromSave(UIScene_LoadMenu* pClass, DWORD dwLocal
PSAVE_DETAILS pSaveDetails=StorageManager.ReturnSavesInfo();
#ifdef _WINDOWS64
// 4J Added: Store save folder name for potential hardcore world deletion
app.DebugPrintf("StartGameFromSave: pSaveDetails=%p, levelGen=%p, saveInfoIndex=%d\n", pSaveDetails, pClass->m_levelGen, pClass->m_iSaveGameInfoIndex);
if (pSaveDetails != nullptr && pClass->m_levelGen == nullptr)
{
app.DebugPrintf("StartGameFromSave: UTF8SaveFilename='%s'\n", pSaveDetails->SaveInfoA[(int)pClass->m_iSaveGameInfoIndex].UTF8SaveFilename);
wchar_t wFolder[MAX_SAVEFILENAME_LENGTH] = {};
mbstowcs(wFolder, pSaveDetails->SaveInfoA[(int)pClass->m_iSaveGameInfoIndex].UTF8SaveFilename, MAX_SAVEFILENAME_LENGTH - 1);
app.SetCurrentSaveFolderName(wFolder);
app.DebugPrintf("StartGameFromSave: stored folder name '%ls'\n", wFolder);
}
else
{
app.DebugPrintf("StartGameFromSave: no save details or is levelGen, clearing folder name\n");
app.SetCurrentSaveFolderName(L"");
}
#endif
NetworkGameInitData *param = new NetworkGameInitData();
param->seed = pClass->m_seed;
param->saveData = nullptr;
@ -1612,6 +1676,7 @@ void UIScene_LoadMenu::StartGameFromSave(UIScene_LoadMenu* pClass, DWORD dwLocal
app.SetGameHostOption(eGameHostOption_DoTileDrops, pClass->m_MoreOptionsParams.bDoTileDrops);
app.SetGameHostOption(eGameHostOption_NaturalRegeneration, pClass->m_MoreOptionsParams.bNaturalRegeneration);
app.SetGameHostOption(eGameHostOption_DoDaylightCycle, pClass->m_MoreOptionsParams.bDoDaylightCycle);
app.SetGameHostOption(eGameHostOption_Hardcore, pClass->m_bHardcore ? 1 : 0);
#ifdef _LARGE_WORLDS
app.SetGameHostOption(eGameHostOption_WorldSize, pClass->m_MoreOptionsParams.worldSize+1 ); // 0 is GAME_HOST_OPTION_WORLDSIZE_UNKNOWN

View file

@ -15,7 +15,7 @@ private:
eControl_OnlineGame,
};
static int m_iDifficultyTitleSettingA[4];
static int m_iDifficultyTitleSettingA[5];
UIControl m_controlMainPanel;
UIControl_Label m_labelGameName, m_labelSeed, m_labelCreatedMode;
@ -71,6 +71,7 @@ private:
wstring m_thumbnailName;
bool m_bRebuildTouchBoxes;
bool m_bHardcore;
public:
UIScene_LoadMenu(int iPad, void *initData, UILayer *parentLayer);

View file

@ -29,7 +29,7 @@
#include "../../../Minecraft.World/NbtIo.h"
#include "../../../Minecraft.World/compression.h"
static wstring ReadLevelNameFromSaveFile(const wstring& filePath)
static wstring ReadLevelNameFromSaveFile(const wstring& filePath, bool *outHardcore = nullptr)
{
// Check for a worldname.txt sidecar written by the rename feature first
size_t slashPos = filePath.rfind(L'\\');
@ -49,7 +49,7 @@ static wstring ReadLevelNameFromSaveFile(const wstring& filePath)
if (len > 0)
{
wchar_t wbuf[128] = {};
mbstowcs(wbuf, buf, 127);
MultiByteToWideChar(CP_UTF8, 0, buf, -1, wbuf, 127);
return wstring(wbuf);
}
}
@ -124,7 +124,11 @@ static wstring ReadLevelNameFromSaveFile(const wstring& filePath)
{
CompoundTag *dataTag = root->getCompound(L"Data");
if (dataTag != nullptr)
{
result = dataTag->getString(L"LevelName");
if (outHardcore)
*outHardcore = dataTag->getBoolean(L"hardcore");
}
delete root;
}
}
@ -633,6 +637,11 @@ void UIScene_LoadOrJoinMenu::handleGainFocus(bool navBack)
if( m_bMultiplayerAllowed )
{
#ifdef _WINDOWS64
// Refresh the games list immediately so that any server
// edits/deletions made in the JoinMenu are visible now.
UpdateGamesList();
#endif
#if TO_BE_IMPLEMENTED
HXUICLASS hClassFullscreenProgress = XuiFindClass( L"CScene_FullscreenProgress" );
HXUICLASS hClassConnectingProgress = XuiFindClass( L"CScene_ConnectingProgress" );
@ -788,13 +797,17 @@ void UIScene_LoadOrJoinMenu::tick()
#else
#ifdef _WINDOWS64
{
wstring levelName = ReadLevelNameFromSaveFile(filePath);
bool saveHardcore = false;
wstring levelName = ReadLevelNameFromSaveFile(filePath, &saveHardcore);
m_saveDetails[i].isHardcore = saveHardcore;
if (!levelName.empty())
{
m_buttonListSaves.addItem(levelName, wstring(L""));
wcstombs(m_saveDetails[i].UTF8SaveName, levelName.c_str(), 127);
m_saveDetails[i].UTF8SaveName[127] = '\0';
{
int n = WideCharToMultiByte(CP_UTF8, 0, levelName.c_str(), -1, m_saveDetails[i].UTF8SaveName, 127, nullptr, nullptr);
if (n <= 0) m_saveDetails[i].UTF8SaveName[0] = '\0';
m_saveDetails[i].UTF8SaveName[127] = '\0';
}
}
else
{
@ -1439,9 +1452,9 @@ int UIScene_LoadOrJoinMenu::KeyboardCompleteWorldNameCallback(LPVOID lpParam,boo
for (int k = 0; k < 127 && ui16Text[k]; k++)
wNewName[k] = static_cast<wchar_t>(ui16Text[k]);
// Convert to narrow for storage and in-memory update
char narrowName[128] = {};
wcstombs(narrowName, wNewName, 127);
// Convert to narrow for storage and in-memory update (UTF-8 to preserve Unicode)
char narrowName[256] = {};
WideCharToMultiByte(CP_UTF8, 0, wNewName, -1, narrowName, 255, nullptr, nullptr);
// Build the sidecar path: Windows64\GameHDD\{folder}\worldname.txt
wchar_t wFilename[MAX_SAVEFILENAME_LENGTH] = {};
@ -1457,7 +1470,7 @@ int UIScene_LoadOrJoinMenu::KeyboardCompleteWorldNameCallback(LPVOID lpParam,boo
// Update the in-memory display name so the list reflects it immediately
strncpy_s(pClass->m_saveDetails[listPos].UTF8SaveName, narrowName, 127);
pClass->m_saveDetails[listPos].UTF8SaveName[127] = '\0';
pClass->m_saveDetails[listPos].UTF8SaveName[127] = '\0'; // UTF8SaveName is still 128 bytes; narrowName fits as Arabic is <=2 bytes/char in UTF-8
// Reuse the existing callback to trigger the list repopulate
UIScene_LoadOrJoinMenu::RenameSaveDataReturned(pClass, true);
@ -2525,7 +2538,7 @@ int UIScene_LoadOrJoinMenu::SaveOptionsDialogReturned(void *pParam,int iPad,C4JS
{
wchar_t wSaveName[128];
ZeroMemory(wSaveName, 128 * sizeof(wchar_t));
mbstowcs_s(nullptr, wSaveName, 128, pClass->m_saveDetails[pClass->m_iSaveListIndex - pClass->m_iDefaultButtonsC].UTF8SaveName, _TRUNCATE);
MultiByteToWideChar(CP_UTF8, 0, pClass->m_saveDetails[pClass->m_iSaveListIndex - pClass->m_iDefaultButtonsC].UTF8SaveName, -1, wSaveName, 127);
UIKeyboardInitData kbData;
kbData.title = app.GetString(IDS_RENAME_WORLD_TITLE);
kbData.defaultText = wSaveName;

View file

@ -5,6 +5,11 @@
#include "../../Options.h"
#include "../../GameRenderer.h"
#ifdef _WINDOWS64
extern bool g_bVSync;
extern void SetExclusiveFullscreen(bool enabled);
#endif
namespace
{
constexpr int FOV_MIN = 70;
@ -62,12 +67,14 @@ UIScene_SettingsGraphicsMenu::UIScene_SettingsGraphicsMenu(int iPad, void *initD
m_checkboxClouds.init(app.GetString(IDS_CHECKBOX_RENDER_CLOUDS),eControl_Clouds,(app.GetGameSettings(m_iPad,eGameSetting_Clouds)!=0));
m_checkboxBedrockFog.init(app.GetString(IDS_CHECKBOX_RENDER_BEDROCKFOG),eControl_BedrockFog,(app.GetGameSettings(m_iPad,eGameSetting_BedrockFog)!=0));
m_checkboxCustomSkinAnim.init(app.GetString(IDS_CHECKBOX_CUSTOM_SKIN_ANIM),eControl_CustomSkinAnim,(app.GetGameSettings(m_iPad,eGameSetting_CustomSkinAnim)!=0));
m_checkboxVSync.init(L"VSync",eControl_VSync,(app.GetGameSettings(m_iPad,eGameSetting_VSync)!=0));
m_checkboxExclusiveFullscreen.init(L"Fullscreen",eControl_ExclusiveFullscreen,(app.GetGameSettings(m_iPad,eGameSetting_ExclusiveFullscreen)!=0));
WCHAR TempString[256];
swprintf(TempString, 256, L"Render Distance: %d",app.GetGameSettings(m_iPad,eGameSetting_RenderDistance));
m_sliderRenderDistance.init(TempString,eControl_RenderDistance,0,5,DistanceToLevel(app.GetGameSettings(m_iPad,eGameSetting_RenderDistance)));
m_sliderRenderDistance.init(TempString,eControl_RenderDistance,0,3,DistanceToLevel(app.GetGameSettings(m_iPad,eGameSetting_RenderDistance)));
swprintf( TempString, 256, L"%ls: %d%%", app.GetString( IDS_SLIDER_GAMMA ),app.GetGameSettings(m_iPad,eGameSetting_Gamma));
m_sliderGamma.init(TempString,eControl_Gamma,0,100,app.GetGameSettings(m_iPad,eGameSetting_Gamma));
@ -82,28 +89,50 @@ UIScene_SettingsGraphicsMenu::UIScene_SettingsGraphicsMenu(int iPad, void *initD
doHorizontalResizeCheck();
#ifndef _WINDOWS64
// VSync and Exclusive Fullscreen are only available on PC
removeControl(&m_checkboxVSync, true);
removeControl(&m_checkboxExclusiveFullscreen, true);
#else
// The SWF's original focus chain skips VSync, Fullscreen, and RenderDistance
// (CustomSkinAnim -> Gamma). Rewire the navigation so all controls are reachable:
// CustomSkinAnim -> VSync -> Fullscreen -> RenderDistance -> Gamma
{
IggyName navDown = registerFastName(L"m_objNavDown");
IggyName navUp = registerFastName(L"m_objNavUp");
IggyValueSetStringUTF8RS(m_checkboxCustomSkinAnim.getIggyValuePath(), navDown, nullptr, "VSync", -1);
IggyValueSetStringUTF8RS(m_checkboxVSync.getIggyValuePath(), navUp, nullptr, "CustomSkinAnim", -1);
IggyValueSetStringUTF8RS(m_checkboxVSync.getIggyValuePath(), navDown, nullptr, "ExclusiveFullscreen", -1);
IggyValueSetStringUTF8RS(m_checkboxExclusiveFullscreen.getIggyValuePath(), navUp, nullptr, "VSync", -1);
IggyValueSetStringUTF8RS(m_checkboxExclusiveFullscreen.getIggyValuePath(), navDown, nullptr, "RenderDistance", -1);
IggyValueSetStringUTF8RS(m_sliderRenderDistance.getIggyValuePath(), navUp, nullptr, "ExclusiveFullscreen", -1);
}
#endif
const bool bInGame=(Minecraft::GetInstance()->level!=nullptr);
const bool bIsPrimaryPad=(ProfileManager.GetPrimaryPad()==m_iPad);
// if we're not in the game, we need to use basescene 0
// if we're not in the game, we need to use basescene 0
if(bInGame)
{
// If the game has started, then you need to be the host to change the in-game gamertags
#ifndef _WINDOWS64
// Console splitscreen: non-host and non-primary players can't change world-level settings
if(bIsPrimaryPad)
{
// we are the primary player on this machine, but not the game host
// are we the game host? If not, we need to remove the bedrockfog setting
{
if(!g_NetworkManager.IsHost())
{
// hide the in-game bedrock fog setting
removeControl(&m_checkboxBedrockFog, true);
}
}
else
{
// We shouldn't have the bedrock fog option, or the m_CustomSkinAnim option
removeControl(&m_checkboxBedrockFog, true);
removeControl(&m_checkboxCustomSkinAnim, true);
}
#endif
}
if(app.GetLocalPlayerCount()>1)
@ -165,6 +194,12 @@ void UIScene_SettingsGraphicsMenu::handleInput(int iPad, int key, bool repeat, b
app.SetGameSettings(m_iPad,eGameSetting_Clouds,m_checkboxClouds.IsChecked()?1:0);
app.SetGameSettings(m_iPad,eGameSetting_BedrockFog,m_checkboxBedrockFog.IsChecked()?1:0);
app.SetGameSettings(m_iPad,eGameSetting_CustomSkinAnim,m_checkboxCustomSkinAnim.IsChecked()?1:0);
app.SetGameSettings(m_iPad,eGameSetting_VSync,m_checkboxVSync.IsChecked()?1:0);
app.SetGameSettings(m_iPad,eGameSetting_ExclusiveFullscreen,m_checkboxExclusiveFullscreen.IsChecked()?1:0);
#ifdef _WINDOWS64
g_bVSync = m_checkboxVSync.IsChecked();
SetExclusiveFullscreen(m_checkboxExclusiveFullscreen.IsChecked());
#endif
navigateBack();
handled = true;

View file

@ -12,18 +12,22 @@ private:
eControl_Clouds,
eControl_BedrockFog,
eControl_CustomSkinAnim,
eControl_VSync,
eControl_ExclusiveFullscreen,
eControl_RenderDistance,
eControl_Gamma,
eControl_FOV,
eControl_InterfaceOpacity
};
UIControl_CheckBox m_checkboxClouds, m_checkboxBedrockFog, m_checkboxCustomSkinAnim; // Checkboxes
UIControl_CheckBox m_checkboxClouds, m_checkboxBedrockFog, m_checkboxCustomSkinAnim, m_checkboxVSync, m_checkboxExclusiveFullscreen; // Checkboxes
UIControl_Slider m_sliderRenderDistance, m_sliderGamma, m_sliderFOV, m_sliderInterfaceOpacity; // Sliders
UI_BEGIN_MAP_ELEMENTS_AND_NAMES(UIScene)
UI_MAP_ELEMENT( m_checkboxClouds, "Clouds")
UI_MAP_ELEMENT( m_checkboxBedrockFog, "BedrockFog")
UI_MAP_ELEMENT( m_checkboxCustomSkinAnim, "CustomSkinAnim")
UI_MAP_ELEMENT( m_checkboxVSync, "VSync")
UI_MAP_ELEMENT( m_checkboxExclusiveFullscreen, "ExclusiveFullscreen")
UI_MAP_ELEMENT( m_sliderRenderDistance, "RenderDistance")
UI_MAP_ELEMENT( m_sliderGamma, "Gamma")
UI_MAP_ELEMENT(m_sliderFOV, "FOV")

View file

@ -257,11 +257,14 @@ typedef struct _SaveListDetails
#endif
#endif
bool isHardcore;
_SaveListDetails()
{
saveId = 0;
pbThumbnailData = nullptr;
dwThumbnailSize = 0;
isHardcore = false;
#ifdef _DURANGO
ZeroMemory(UTF16SaveName,sizeof(wchar_t)*128);
ZeroMemory(UTF16SaveFilename,sizeof(wchar_t)*MAX_SAVEFILENAME_LENGTH);

View file

@ -4,7 +4,7 @@
#include "../../../Minecraft.World/File.h"
#include "UITTFFont.h"
UITTFFont::UITTFFont(const string &name, const string &path, S32 fallbackCharacter)
UITTFFont::UITTFFont(const string &name, const string &path, S32 fallbackCharacter, bool registerAsDefaultFonts)
: m_strFontName(name)
{
app.DebugPrintf("UITTFFont opening %s\n",path.c_str());
@ -41,9 +41,12 @@ UITTFFont::UITTFFont(const string &name, const string &path, S32 fallbackCharact
IggyFontInstallTruetypeFallbackCodepointUTF8( m_strFontName.c_str(), -1, IGGY_FONTFLAG_none, fallbackCharacter );
// 4J Stu - These are so we can use the default flash controls
IggyFontInstallTruetypeUTF8 ( (void *)pbData, IGGY_TTC_INDEX_none, "Times New Roman", -1, IGGY_FONTFLAG_none );
IggyFontInstallTruetypeUTF8 ( (void *)pbData, IGGY_TTC_INDEX_none, "Arial", -1, IGGY_FONTFLAG_none );
if (registerAsDefaultFonts)
{
// 4J Stu - These are so we can use the default flash controls
IggyFontInstallTruetypeUTF8 ( (void *)pbData, IGGY_TTC_INDEX_none, "Times New Roman", -1, IGGY_FONTFLAG_none );
IggyFontInstallTruetypeUTF8 ( (void *)pbData, IGGY_TTC_INDEX_none, "Arial", -1, IGGY_FONTFLAG_none );
}
}
}

View file

@ -9,7 +9,7 @@ private:
//DWORD dwDataSize;
public:
UITTFFont(const string &name, const string &path, S32 fallbackCharacter);
UITTFFont(const string &name, const string &path, S32 fallbackCharacter, bool registerAsDefaultFonts = true);
~UITTFFont();
string getFontName();

View file

@ -0,0 +1,164 @@
#include "stdafx.h"
#include "BufferedImage.h"
#include "UIFontData.h"
#include "UIUnicodeBitmapFont.h"
UIUnicodeBitmapFont::UIUnicodeBitmapFont(const string &fontname, SFontData &referenceFontData)
: UIAbstractBitmapFont(fontname)
{
m_numGlyphs = 65536;
m_referenceFontData = &referenceFontData;
memset(m_glyphPages, 0, sizeof(m_glyphPages));
memset(m_unicodeWidth, 0, sizeof(m_unicodeWidth));
FILE *f = nullptr;
fopen_s(&f, "Common/res/1_2_2/font/glyph_sizes.bin", "rb");
if (f)
{
fread(m_unicodeWidth, 1, 65536, f);
fclose(f);
}
}
UIUnicodeBitmapFont::~UIUnicodeBitmapFont()
{
for (int i = 0; i < 256; i++)
delete[] m_glyphPages[i];
}
void UIUnicodeBitmapFont::loadGlyphPage(int page)
{
wchar_t fileName[64];
swprintf(fileName, 64, L"/1_2_2/font/glyph_%02X.png", page);
BufferedImage bimg(fileName);
int *rawData = bimg.getData();
if (!rawData) return;
int size = 256 * 256;
m_glyphPages[page] = new unsigned char[size];
for (int i = 0; i < size; i++)
m_glyphPages[page][i] = (rawData[i] & 0xFF000000) >> 24;
}
IggyFontMetrics *UIUnicodeBitmapFont::GetFontMetrics(IggyFontMetrics *metrics)
{
metrics->ascent = m_referenceFontData->m_fAscent;
metrics->descent = m_referenceFontData->m_fDescent;
metrics->average_glyph_width_for_tab_stops = 8.0f;
metrics->largest_glyph_bbox_y1 = metrics->descent;
return metrics;
}
S32 UIUnicodeBitmapFont::GetCodepointGlyph(U32 codepoint)
{
if (codepoint < 65536 && m_unicodeWidth[codepoint] != 0)
return (S32)codepoint;
return IGGY_GLYPH_INVALID;
}
IggyGlyphMetrics *UIUnicodeBitmapFont::GetGlyphMetrics(S32 glyph, IggyGlyphMetrics *metrics)
{
if (glyph < 0 || glyph >= 65536) { metrics->x0 = metrics->x1 = metrics->advance = metrics->y0 = metrics->y1 = 0; return metrics; }
int left = m_unicodeWidth[glyph] >> 4;
int right = (m_unicodeWidth[glyph] & 0xF) + 1;
float pixelWidth = (right - left) / 2.0f + 1.0f;
float advance = pixelWidth * m_referenceFontData->m_fAdvPerPixel;
metrics->x0 = 0.0f;
metrics->x1 = advance;
metrics->advance = advance;
metrics->y0 = 0.0f;
metrics->y1 = 1.0f;
return metrics;
}
rrbool UIUnicodeBitmapFont::IsGlyphEmpty(S32 glyph)
{
if (glyph < 0 || glyph >= 65536) return true;
return m_unicodeWidth[glyph] == 0;
}
F32 UIUnicodeBitmapFont::GetKerningForGlyphPair(S32 first_glyph, S32 second_glyph)
{
return 0.0f;
}
rrbool UIUnicodeBitmapFont::CanProvideBitmap(S32 glyph, F32 pixel_scale)
{
return glyph >= 0 && glyph < 65536;
}
rrbool UIUnicodeBitmapFont::GetGlyphBitmap(S32 glyph, F32 pixel_scale, IggyBitmapCharacter *bitmap)
{
if (glyph < 0 || glyph >= 65536) return false;
int page = glyph / 256;
if (!m_glyphPages[page])
{
loadGlyphPage(page);
if (!m_glyphPages[page]) return false;
}
int cx = (glyph % 16) * 16;
int cy = ((glyph & 0xFF) / 16) * 16;
bitmap->pixels_one_per_byte = m_glyphPages[page] + (cy * 256) + cx;
bitmap->width_in_pixels = 16;
bitmap->height_in_pixels = 16;
bitmap->stride_in_bytes = 256;
bitmap->top_left_x = 0;
bitmap->top_left_y = -static_cast<S32>(16) * m_referenceFontData->m_fAscent;
bitmap->oversample = 0;
// Scale parameters: match UIBitmapFont's approach.
// truePixelScale = the pixel_scale at which 1 glyph pixel = 1 screen pixel.
// For 16px glyphs displayed at the same visual size as Mojangles_7 (8px glyphs with advPerPixel 1/10):
// The reference truePixelScale for Mojangles_7 is 1.0f/m_fAdvPerPixel = 10.0f
// Since our glyphs are 16px (2x the Mojangles 8px), our truePixelScale is 20.0f
float truePixelScale = 2.0f / m_referenceFontData->m_fAdvPerPixel;
#ifdef _WINDOWS64
bitmap->pixel_scale_correct = truePixelScale;
if (pixel_scale < truePixelScale)
{
bitmap->pixel_scale_min = 0.0f;
bitmap->pixel_scale_max = truePixelScale;
bitmap->point_sample = false;
}
else
{
bitmap->pixel_scale_min = truePixelScale;
bitmap->pixel_scale_max = 99.0f;
bitmap->point_sample = true;
}
#else
float glyphScale = 1.0f;
while ((0.5f + glyphScale) * truePixelScale < pixel_scale)
glyphScale++;
if (glyphScale <= 1 && pixel_scale < truePixelScale)
{
bitmap->pixel_scale_correct = truePixelScale;
bitmap->pixel_scale_min = 0.0f;
bitmap->pixel_scale_max = truePixelScale * 1.001f;
bitmap->point_sample = false;
}
else
{
float actualScale = pixel_scale / glyphScale;
bitmap->pixel_scale_correct = actualScale;
bitmap->pixel_scale_min = truePixelScale;
bitmap->pixel_scale_max = 99.0f;
bitmap->point_sample = true;
}
#endif
bitmap->user_context_for_free = nullptr;
return true;
}
void UIUnicodeBitmapFont::FreeGlyphBitmap(S32 glyph, F32 pixel_scale, IggyBitmapCharacter *bitmap)
{
// Pixel data lives in m_glyphPages -- nothing to free.
}

View file

@ -0,0 +1,27 @@
#pragma once
#include "UIBitmapFont.h"
struct SFontData;
class UIUnicodeBitmapFont : public UIAbstractBitmapFont
{
private:
unsigned char m_unicodeWidth[65536];
unsigned char* m_glyphPages[256];
SFontData* m_referenceFontData;
void loadGlyphPage(int page);
public:
UIUnicodeBitmapFont(const string &fontname, SFontData &referenceFontData);
~UIUnicodeBitmapFont();
virtual IggyFontMetrics *GetFontMetrics(IggyFontMetrics *metrics);
virtual S32 GetCodepointGlyph(U32 codepoint);
virtual IggyGlyphMetrics *GetGlyphMetrics(S32 glyph, IggyGlyphMetrics *metrics);
virtual rrbool IsGlyphEmpty(S32 glyph);
virtual F32 GetKerningForGlyphPair(S32 first_glyph, S32 second_glyph);
virtual rrbool CanProvideBitmap(S32 glyph, F32 pixel_scale);
virtual rrbool GetGlyphBitmap(S32 glyph, F32 pixel_scale, IggyBitmapCharacter *bitmap);
virtual void FreeGlyphBitmap(S32 glyph, F32 pixel_scale, IggyBitmapCharacter *bitmap);
};

View file

@ -2,6 +2,7 @@
#include "XUI_Chat.h"
#include "../../Minecraft.h"
#include "../../Gui.h"
#include "../../../Minecraft.World\ArabicShaping.h"
HRESULT CScene_Chat::OnInit( XUIMessageInit* pInitData, BOOL& bHandled )
{
@ -29,7 +30,8 @@ HRESULT CScene_Chat::OnTimer( XUIMessageTimer *pXUIMessageTimer, BOOL &bHandled)
{
m_Backgrounds[i].SetOpacity(opacity);
m_Labels[i].SetOpacity(opacity);
m_Labels[i].SetText( pGui->getMessage(m_iPad,i).c_str() );
wstring shaped = shapeArabicText(pGui->getMessage(m_iPad, i));
m_Labels[i].SetText( shaped.c_str() );
}
else
{

View file

@ -1,5 +1,6 @@
#include "stdafx.h"
#include "XUI_Ctrl_4JList.h"
#include "..\..\..\Minecraft.World\ArabicShaping.h"
static bool TimeSortFn(const void *a, const void *b);
@ -294,8 +295,16 @@ HRESULT CXuiCtrl4JList::OnGetSourceDataText(XUIMessageGetSourceText *pGetSourceT
if( ( 0 == pGetSourceTextData->iData ) && ( ( pGetSourceTextData->bItemData ) ) )
{
EnterCriticalSection(&m_AccessListData);
pGetSourceTextData->szText =
GetData(pGetSourceTextData->iItem).pwszText;
LPCWSTR rawText = GetData(pGetSourceTextData->iItem).pwszText;
if (rawText)
{
m_shapedTextCache = shapeArabicText(rawText);
pGetSourceTextData->szText = m_shapedTextCache.c_str();
}
else
{
pGetSourceTextData->szText = rawText;
}
LeaveCriticalSection(&m_AccessListData);
bHandled = TRUE;
}

View file

@ -75,4 +75,5 @@ private:
static bool IndexSortFn(const void *a, const void *b);
HXUIOBJ m_hSelectionChangedHandlerObj;
std::wstring m_shapedTextCache; // temp buffer for Arabic-shaped text in OnGetSourceDataText
};

View file

@ -4,6 +4,7 @@
#include "stdafx.h"
#include "../XUI/XUI_Death.h"
#include <assert.h>
#include "../../../Minecraft.World/AABB.h"
#include "../../../Minecraft.World/Vec3.h"
#include "../../../Minecraft.World/net.minecraft.stats.h"
@ -17,10 +18,13 @@
#include "../../../Minecraft.Client/LevelRenderer.h"
#include "../../../Minecraft.World/Pos.h"
#include "../../../Minecraft.World/Dimension.h"
#include "../../../Minecraft.World/net.minecraft.world.level.storage.h"
#include "../../../Minecraft.World/compression.h"
#include "../../Minecraft.h"
#include "../../MinecraftServer.h"
#include "../../Options.h"
#include "../../LocalPlayer.h"
#include "../../../Minecraft.World/compression.h"
//----------------------------------------------------------------------------------
// Performs initialization tasks - retrieves controls.
//----------------------------------------------------------------------------------
@ -37,9 +41,26 @@ HRESULT CScene_Death::OnInit( XUIMessageInit* pInitData, BOOL& bHandled )
}
XuiControlSetText(m_Title,app.GetString(IDS_YOU_DIED));
XuiControlSetText(m_Buttons[BUTTON_DEATH_RESPAWN],app.GetString(IDS_RESPAWN));
XuiControlSetText(m_Buttons[BUTTON_DEATH_EXITGAME],app.GetString(IDS_EXIT_GAME));
// 4J Added: In hardcore mode, disable respawn and show hardcore death message
Minecraft *pMinecraft = Minecraft::GetInstance();
bool isHardcore = false;
if (pMinecraft != nullptr && pMinecraft->level != nullptr)
{
isHardcore = pMinecraft->level->getLevelData()->isHardcore();
}
if (isHardcore)
{
XuiControlSetText(m_Buttons[BUTTON_DEATH_RESPAWN], app.GetString(IDS_HARDCORE_DEATH_MESSAGE));
XuiElementSetShow(m_Buttons[BUTTON_DEATH_RESPAWN], FALSE);
}
else
{
XuiControlSetText(m_Buttons[BUTTON_DEATH_RESPAWN], app.GetString(IDS_RESPAWN));
}
// Display the tooltips
ui.SetTooltips( m_iPad, IDS_TOOLTIPS_SELECT);
@ -110,7 +131,16 @@ HRESULT CScene_Death::OnNotifyPressEx(HXUIOBJ hObjPressed, XUINotifyPress* pNoti
playTime = static_cast<int>(pMinecraft->localplayers[pNotifyPressData->UserIndex]->getSessionTimer());
}
TelemetryManager->RecordLevelExit(pNotifyPressData->UserIndex, eSen_LevelExitStatus_Failed);
// 4J Added: Hardcore mode — skip save dialog, exit without saving, delete world
if (pMinecraft->level != nullptr && pMinecraft->level->getLevelData()->isHardcore() && g_NetworkManager.IsHost())
{
MinecraftServer::getInstance()->setSaveOnExit(false);
MinecraftServer::getInstance()->setDeleteWorldOnExit(true);
app.SetAction(pNotifyPressData->UserIndex, eAppAction_ExitWorld);
break;
}
if(StorageManager.GetSaveDisabled())
{
uiIDA[0]=IDS_CONFIRM_CANCEL;
@ -172,6 +202,12 @@ HRESULT CScene_Death::OnNotifyPressEx(HXUIOBJ hObjPressed, XUINotifyPress* pNoti
break;
case BUTTON_DEATH_RESPAWN:
{
// 4J Added: Safeguard - don't respawn in hardcore mode
Minecraft *pMC = Minecraft::GetInstance();
if (pMC != nullptr && pMC->level != nullptr && pMC->level->getLevelData()->isHardcore())
{
break;
}
m_bIgnoreInput = true;
app.SetAction(pNotifyPressData->UserIndex,eAppAction_Respawn);
}

View file

@ -186,7 +186,8 @@ HRESULT CXuiSceneHud::OnCustomMessage_TickScene()
if (pMinecraft->localplayers[m_iPad]->invulnerableTime < 10) blink = false;
int iHealth = pMinecraft->localplayers[m_iPad]->getHealth();
int iLastHealth = pMinecraft->localplayers[m_iPad]->lastHealth;
bool bHasPoison = pMinecraft->localplayers[m_iPad]->hasEffect(MobEffect::poison);
bool bHasPoison = pMinecraft->localplayers[m_iPad]->hasEffect(MobEffect::poison);
bool isHardcore = pMinecraft->level != nullptr && pMinecraft->level->getLevelData()->isHardcore();
for (int icon = 0; icon < Player::MAX_HEALTH / 2; icon++)
{
if(blink)
@ -196,11 +197,15 @@ HRESULT CXuiSceneHud::OnCustomMessage_TickScene()
// Full
if(bHasPoison)
{
m_healthIcon[icon].PlayVisualRange(L"FullPoisonFlash",nullptr,L"FullPoisonFlash");
m_healthIcon[icon].PlayVisualRange(
isHardcore ? L"FullPoisonFlashHardcore" : L"FullPoisonFlash", nullptr,
isHardcore ? L"FullPoisonFlashHardcore" : L"FullPoisonFlash");
}
else
{
m_healthIcon[icon].PlayVisualRange(L"FullFlash",nullptr,L"FullFlash");
m_healthIcon[icon].PlayVisualRange(
isHardcore ? L"FullFlashHardcore" : L"FullFlash", nullptr,
isHardcore ? L"FullFlashHardcore" : L"FullFlash");
}
}
else if (icon * 2 + 1 == iLastHealth || icon * 2 + 1 == iHealth)
@ -208,17 +213,23 @@ HRESULT CXuiSceneHud::OnCustomMessage_TickScene()
// Half
if(bHasPoison)
{
m_healthIcon[icon].PlayVisualRange(L"HalfPoisonFlash",nullptr,L"HalfPoisonFlash");
m_healthIcon[icon].PlayVisualRange(
isHardcore ? L"HalfPoisonFlashHardcore" : L"HalfPoisonFlash", nullptr,
isHardcore ? L"HalfPoisonFlashHardcore" : L"HalfPoisonFlash");
}
else
{
m_healthIcon[icon].PlayVisualRange(L"HalfFlash",nullptr,L"HalfFlash");
m_healthIcon[icon].PlayVisualRange(
isHardcore ? L"HalfFlashHardcore" : L"HalfFlash", nullptr,
isHardcore ? L"HalfFlashHardcore" : L"HalfFlash");
}
}
else
{
// Empty
m_healthIcon[icon].PlayVisualRange(L"NormalFlash",nullptr,L"NormalFlash");
m_healthIcon[icon].PlayVisualRange(
isHardcore ? L"NormalFlashHardcore" : L"NormalFlash", nullptr,
isHardcore ? L"NormalFlashHardcore" : L"NormalFlash");
}
}
else
@ -228,11 +239,15 @@ HRESULT CXuiSceneHud::OnCustomMessage_TickScene()
// Full
if(bHasPoison)
{
m_healthIcon[icon].PlayVisualRange(L"FullPoison",nullptr,L"FullPoison");
m_healthIcon[icon].PlayVisualRange(
isHardcore ? L"FullPoisonHardcore" : L"FullPoison", nullptr,
isHardcore ? L"FullPoisonHardcore" : L"FullPoison");
}
else
{
m_healthIcon[icon].PlayVisualRange(L"Full",nullptr,L"Full");
m_healthIcon[icon].PlayVisualRange(
isHardcore ? L"FullHardcore" : L"Full", nullptr,
isHardcore ? L"FullHardcore" : L"Full");
}
}
else if (icon * 2 + 1 == iHealth)
@ -240,17 +255,23 @@ HRESULT CXuiSceneHud::OnCustomMessage_TickScene()
// Half
if(bHasPoison)
{
m_healthIcon[icon].PlayVisualRange(L"HalfPoison",nullptr,L"HalfPoison");
m_healthIcon[icon].PlayVisualRange(
isHardcore ? L"HalfPoisonHardcore" : L"HalfPoison", nullptr,
isHardcore ? L"HalfPoisonHardcore" : L"HalfPoison");
}
else
{
m_healthIcon[icon].PlayVisualRange(L"Half",nullptr,L"Half");
m_healthIcon[icon].PlayVisualRange(
isHardcore ? L"HalfHardcore" : L"Half", nullptr,
isHardcore ? L"HalfHardcore" : L"Half");
}
}
else
{
// Empty
m_healthIcon[icon].PlayVisualRange(L"Normal",nullptr,L"Normal");
m_healthIcon[icon].PlayVisualRange(
isHardcore ? L"NormalHardcore" : L"Normal", nullptr,
isHardcore ? L"NormalHardcore" : L"Normal");
}
}

View file

@ -89,6 +89,11 @@ void EnderDragonRenderer::render(shared_ptr<Entity> _mob, double x, double y, do
// 4J - dynamic cast required because we aren't using templates/generics in our version
shared_ptr<EnderDragon> mob = dynamic_pointer_cast<EnderDragon>(_mob);
BossMobGuiInfo::setBossHealth(mob, false);
if (!mob->getCustomName().empty())
{
BossMobGuiInfo::name = mob->getCustomName();
}
MobRenderer::render(mob, x, y, z, rot, a);
if (mob->nearestCrystal != nullptr)
{

View file

@ -21,6 +21,10 @@
#include "../Minecraft.World/net.minecraft.world.level.chunk.h"
#include "PlayerConnection.h"
#ifdef _WINDOWS64
extern bool g_Win64DedicatedServer;
#endif
EntityTracker::EntityTracker(ServerLevel *level)
{
this->level = level;
@ -140,32 +144,40 @@ void EntityTracker::tick()
// 4J Stu - If one player on a system is updated, then make sure they all are as they all have their
// range extended to include entities visible by any other player on the system
// Fix for #11194 - Gameplay: Host player and their split-screen avatars can become invisible and invulnerable to client.
MinecraftServer *server = MinecraftServer::getInstance();
for( unsigned int i = 0; i < server->getPlayers()->players.size(); i++ )
// NOTE: On dedicated servers, IsSameSystem() always returns false for remote
// players (no split-screen), so this loop does nothing. Skip it entirely to
// avoid the O(players * movedPlayers) overhead.
#ifdef _WINDOWS64
if (!g_Win64DedicatedServer)
#endif
{
shared_ptr<ServerPlayer> ep = server->getPlayers()->players[i];
if( ep->dimension != level->dimension->id ) continue;
if( ep->connection == nullptr ) continue;
INetworkPlayer *thisPlayer = ep->connection->getNetworkPlayer();
if( thisPlayer == nullptr ) continue;
bool addPlayer = false;
for (unsigned int j = 0; j < movedPlayers.size(); j++)
MinecraftServer *server = MinecraftServer::getInstance();
for( unsigned int i = 0; i < server->getPlayers()->players.size(); i++ )
{
shared_ptr<ServerPlayer> sp = movedPlayers[j];
shared_ptr<ServerPlayer> ep = server->getPlayers()->players[i];
if( ep->dimension != level->dimension->id ) continue;
if( sp == ep ) break;
if( ep->connection == nullptr ) continue;
INetworkPlayer *thisPlayer = ep->connection->getNetworkPlayer();
if( thisPlayer == nullptr ) continue;
if(sp->connection == nullptr) continue;
INetworkPlayer *otherPlayer = sp->connection->getNetworkPlayer();
if( otherPlayer != nullptr && thisPlayer->IsSameSystem(otherPlayer) )
bool addPlayer = false;
for (unsigned int j = 0; j < movedPlayers.size(); j++)
{
addPlayer = true;
break;
shared_ptr<ServerPlayer> sp = movedPlayers[j];
if( sp == ep ) break;
if(sp->connection == nullptr) continue;
INetworkPlayer *otherPlayer = sp->connection->getNetworkPlayer();
if( otherPlayer != nullptr && thisPlayer->IsSameSystem(otherPlayer) )
{
addPlayer = true;
break;
}
}
}
if( addPlayer ) movedPlayers.push_back( ep );
if( addPlayer ) movedPlayers.push_back( ep );
}
}
for (unsigned int i = 0; i < movedPlayers.size(); i++)

View file

@ -196,9 +196,29 @@ void IQNetPlayer::SendData(IQNetPlayer * player, const void* pvData, DWORD dwDat
{
if (!WinsockNetLayer::IsHosting() && !m_isRemote)
{
// Client sending to server via local socket (bypasses SendToSmallId)
SOCKET sock = WinsockNetLayer::GetLocalSocket(m_smallId);
if (sock != INVALID_SOCKET)
WinsockNetLayer::SendOnSocket(sock, pvData, dwDataSize);
{
// Encrypt if client send cipher is active
if (dwDataSize > 0)
{
std::vector<BYTE> buf(static_cast<const BYTE*>(pvData),
static_cast<const BYTE*>(pvData) + dwDataSize);
if (WinsockNetLayer::TryEncryptClientOutgoing(buf.data(), static_cast<int>(dwDataSize)))
{
WinsockNetLayer::SendOnSocket(sock, buf.data(), static_cast<int>(dwDataSize));
}
else
{
WinsockNetLayer::SendOnSocket(sock, pvData, dwDataSize);
}
}
else
{
WinsockNetLayer::SendOnSocket(sock, pvData, dwDataSize);
}
}
}
else
{

View file

@ -8,6 +8,7 @@
#include "../Minecraft.World/net.minecraft.h"
#include "../Minecraft.World/StringHelpers.h"
#include "../Minecraft.World/Random.h"
#include "..\Minecraft.World\ArabicShaping.h"
Font::Font(Options *options, const wstring& name, Textures* textures, bool enforceUnicode, ResourceLocation *textureLocation, int cols, int rows, int charWidth, int charHeight, unsigned short charMap[]/* = nullptr */) : textures(textures)
{
@ -16,7 +17,7 @@ Font::Font(Options *options, const wstring& name, Textures* textures, bool enfor
charWidths = new int[charC];
// 4J - added initialisers
memset(charWidths, 0, charC);
memset(charWidths, 0, charC * sizeof(int));
enforceUnicodeSheet = false;
bidirectional = false;
@ -26,6 +27,19 @@ Font::Font(Options *options, const wstring& name, Textures* textures, bool enfor
m_underline = false;
m_strikethrough = false;
memset(unicodeTexID, 0, sizeof(unicodeTexID));
memset(unicodeWidth, 0, sizeof(unicodeWidth));
lastBoundTexture = 0;
// Load unicode glyph sizes
FILE *glyphFile = nullptr;
fopen_s(&glyphFile, "Common/res/1_2_2/font/glyph_sizes.bin", "rb");
if (glyphFile)
{
fread(unicodeWidth, 1, 65536, glyphFile);
fclose(glyphFile);
}
// Set up member variables
m_cols = cols;
m_rows = rows;
@ -268,7 +282,87 @@ void Font::drawLiteral(const wstring& str, int x, int y, int color)
yPos = static_cast<float>(y);
wstring cleanStr = sanitize(str);
for (size_t i = 0; i < cleanStr.length(); ++i)
renderCharacter(cleanStr.at(i));
{
wchar_t c = cleanStr.at(i);
if (isUnicodeGlyphChar(c))
{
renderUnicodeCharacter(c);
textures->bindTexture(m_textureLocation);
lastBoundTexture = fontTexture;
}
else
{
renderCharacter(c);
}
}
}
// Like sanitize() but skips the shapeArabicText() call - for pre-shaped strings.
wstring Font::sanitizePreshaped(const wstring& str)
{
wstring sb = str;
for (unsigned int i = 0; i < sb.length(); i++)
{
if (CharacterExists(sb[i]))
sb[i] = MapCharacter(sb[i]);
else if (unicodeWidth[sb[i]] != 0)
{
// Leave as-is: raw codepoint for glyph page rendering
}
else
{
sb[i] = 0;
}
}
return sb;
}
void Font::drawLiteralPreshaped(const wstring& str, int x, int y, int color)
{
if (str.empty()) return;
if ((color & 0xFC000000) == 0) color |= 0xFF000000;
textures->bindTexture(m_textureLocation);
glColor4f((color >> 16 & 255) / 255.0F, (color >> 8 & 255) / 255.0F, (color & 255) / 255.0F, (color >> 24 & 255) / 255.0F);
xPos = static_cast<float>(x);
yPos = static_cast<float>(y);
wstring cleanStr = sanitizePreshaped(str);
for (size_t i = 0; i < cleanStr.length(); ++i)
{
wchar_t c = cleanStr.at(i);
if (isUnicodeGlyphChar(c))
{
renderUnicodeCharacter(c);
textures->bindTexture(m_textureLocation);
lastBoundTexture = fontTexture;
}
else
{
renderCharacter(c);
}
}
}
void Font::drawShadowLiteralPreshaped(const wstring& str, int x, int y, int color)
{
int shadowColor = (color & 0xFCFCFC) >> 2 | (color & 0xFF000000);
drawLiteralPreshaped(str, x + 1, y + 1, shadowColor);
drawLiteralPreshaped(str, x, y, color);
}
int Font::widthPreshaped(const wstring& str)
{
wstring cleanStr = sanitizePreshaped(str);
if (cleanStr.empty()) return 0;
int len = 0;
for (size_t i = 0; i < cleanStr.length(); ++i)
{
wchar_t wc = cleanStr.at(i);
if (isUnicodeGlyphChar(wc))
len += (int)unicodeCharWidth(wc);
else
len += charWidths[static_cast<unsigned>(wc)];
}
return len;
}
void Font::drawShadowWordWrap(const wstring &str, int x, int y, int w, int color, int h)
@ -358,7 +452,7 @@ void Font::draw(const wstring &str, bool dropShadow, int initialColor)
}
// "noise" for crazy splash screen message
if (noise)
if (noise && !isUnicodeGlyphChar(c))
{
int newc;
do
@ -368,7 +462,23 @@ void Font::draw(const wstring &str, bool dropShadow, int initialColor)
c = newc;
}
addCharacterQuad(c);
if (isUnicodeGlyphChar(c))
{
t->end();
// renderUnicodeCharacter uses its own begin/end and relies on glColor
glColor4f((currentColor >> 16 & 255) / 255.0F, (currentColor >> 8 & 255) / 255.0F,
(currentColor & 255) / 255.0F, (currentColor >> 24 & 255) / 255.0F);
renderUnicodeCharacter(c);
glColor4f(1.0F, 1.0F, 1.0F, 1.0F);
textures->bindTexture(m_textureLocation);
lastBoundTexture = fontTexture;
t->begin();
t->color(currentColor & 0x00ffffff, (currentColor >> 24) & 255);
}
else
{
addCharacterQuad(c);
}
}
t->end();
@ -409,11 +519,22 @@ int Font::width(const wstring& str)
++i;
else
{
len += charWidths[167];
if (isUnicodeGlyphChar(167))
len += (int)unicodeCharWidth(167);
else
len += charWidths[167];
if (i + 1 < cleanStr.length())
len += charWidths[static_cast<unsigned>(cleanStr[++i])];
{
wchar_t nextC = cleanStr[++i];
if (isUnicodeGlyphChar(nextC))
len += (int)unicodeCharWidth(nextC);
else if (static_cast<unsigned>(nextC) < static_cast<unsigned>(m_cols * m_rows))
len += charWidths[static_cast<unsigned>(nextC)];
}
}
}
else if (isUnicodeGlyphChar(c))
len += (int)unicodeCharWidth(c);
else
len += charWidths[c];
}
@ -427,13 +548,19 @@ int Font::widthLiteral(const wstring& str)
if (cleanStr == L"") return 0;
int len = 0;
for (size_t i = 0; i < cleanStr.length(); ++i)
len += charWidths[static_cast<unsigned>(cleanStr.at(i))];
{
wchar_t wc = cleanStr.at(i);
if (isUnicodeGlyphChar(wc))
len += (int)unicodeCharWidth(wc);
else
len += charWidths[static_cast<unsigned>(wc)];
}
return len;
}
wstring Font::sanitize(const wstring& str)
{
wstring sb = str;
wstring sb = shapeArabicText(str);
for (unsigned int i = 0; i < sb.length(); i++)
{
@ -441,6 +568,10 @@ wstring Font::sanitize(const wstring& str)
{
sb[i] = MapCharacter(sb[i]);
}
else if (unicodeWidth[sb[i]] != 0)
{
// Leave as-is: raw codepoint for glyph page rendering
}
else
{
// If this character isn't supported, just show the first character (empty square box character)
@ -684,33 +815,22 @@ void Font::renderFakeCB(IntBuffer *ib)
}
}
}
*/
void Font::loadUnicodePage(int page)
{
wchar_t fileName[25];
//String fileName = String.format("/1_2_2/font/glyph_%02X.png", page);
swprintf(fileName,25,L"/1_2_2/font/glyph_%02X.png",page);
wchar_t fileName[40];
swprintf(fileName, 40, L"/1_2_2/font/glyph_%02X.png", page);
BufferedImage *image = new BufferedImage(fileName);
//try
//{
// image = ImageIO.read(Textures.class.getResourceAsStream(fileName.toString()));
//}
//catch (IOException e)
//{
// throw new RuntimeException(e);
//}
unicodeTexID[page] = textures->getTexture(image);
lastBoundTexture = unicodeTexID[page];
delete image;
}
void Font::renderUnicodeCharacter(wchar_t c)
{
if (unicodeWidth[c] == 0)
{
// System.out.println("no-width char " + c);
return;
}
int page = c / 256;
@ -722,19 +842,17 @@ void Font::renderUnicodeCharacter(wchar_t c)
lastBoundTexture = unicodeTexID[page];
}
// first column with non-trans pixels
int firstLeft = unicodeWidth[c] >> 4;
// last column with non-trans pixels
int firstRight = unicodeWidth[c] & 0xF;
float left = firstLeft;
float right = firstRight + 1;
float left = (float)firstLeft;
float right = (float)(firstRight + 1);
float xOff = c % 16 * 16 + left;
float yOff = (c & 0xFF) / 16 * 16;
float xOff = (c % 16) * 16 + left;
float yOff = ((c & 0xFF) / 16) * 16;
float width = right - left - .02f;
Tesselator *t = Tesselator::getInstance();
Tesselator *t = Tesselator::getInstance();
t->begin(GL_TRIANGLE_STRIP);
t->tex(xOff / 256.0F, yOff / 256.0F);
t->vertex(xPos, yPos, 0.0f);
@ -748,5 +866,17 @@ void Font::renderUnicodeCharacter(wchar_t c)
xPos += (right - left) / 2 + 1;
}
*/
float Font::unicodeCharWidth(wchar_t c)
{
if (unicodeWidth[c] == 0) return 0;
int firstLeft = unicodeWidth[c] >> 4;
int firstRight = unicodeWidth[c] & 0xF;
return (firstRight + 1 - firstLeft) / 2.0f + 1;
}
bool Font::isUnicodeGlyphChar(wchar_t c)
{
return c >= m_cols * m_rows && unicodeWidth[c] != 0;
}

View file

@ -18,6 +18,10 @@ private:
Textures *textures;
int unicodeTexID[256];
unsigned char unicodeWidth[65536];
int lastBoundTexture;
float xPos;
float yPos;
@ -72,10 +76,18 @@ private:
void drawLiteral(const wstring& str, int x, int y, int color); // no § parsing
int MapCharacter(wchar_t c); // 4J added
bool CharacterExists(wchar_t c); // 4J added
void loadUnicodePage(int page);
void renderUnicodeCharacter(wchar_t c);
float unicodeCharWidth(wchar_t c);
bool isUnicodeGlyphChar(wchar_t c);
wstring sanitizePreshaped(const wstring& str); // sanitize without re-shaping Arabic
void drawLiteralPreshaped(const wstring& str, int x, int y, int color);
public:
int width(const wstring& str);
int widthLiteral(const wstring& str); // width without skipping § codes (for chat input)
int widthPreshaped(const wstring& str); // width of already-shaped text, no re-shaping
void drawShadowLiteralPreshaped(const wstring& str, int x, int y, int color);
wstring sanitize(const wstring& str);
void drawWordWrap(const wstring &string, int x, int y, int w, int col, int h); // 4J Added h param

View file

@ -1546,7 +1546,7 @@ void GameRenderer::renderLevel(float a, int64_t until)
if (visibleWaterChunks > 0)
{
PIXBeginNamedEvent(0,"Fancy second pass - actual rendering");
levelRenderer->render(cameraEntity, 1, a, updateChunks); // 4J - chanaged, used to be renderSameAsLast but we don't support that anymore
levelRenderer->renderChunksDirect(1, a); // Lightweight path — skips redundant allChanged/resortChunks checks
PIXEndNamedEvent();
}

View file

@ -499,11 +499,10 @@ void Gui::render(float a, bool mouseFree, int xMouse, int yMouse)
int y0 = 0;
// No hardcore on console
/*if (minecraft->level.getLevelData().isHardcore())
{
y0 = 5;
}*/
//if (minecraft->level->getLevelData()->isHardcore())
//{
// y0 = 5;
//}
blit(xo, yo, 16 + bg * 9, 9 * y0, 9, 9);
if (blink)
@ -854,10 +853,11 @@ void Gui::render(float a, bool mouseFree, int xMouse, int yMouse)
// font.draw(str, x + 1, y, 0xffffff);
// }
lastTickA = a;
// 4J Stu - This is now displayed in a xui scene
#if 0
// Jukebox CD message
// Jukebox CD message
if (overlayMessageTime > 0)
{
float t = overlayMessageTime - a;
@ -1064,6 +1064,13 @@ void Gui::render(float a, bool mouseFree, int xMouse, int yMouse)
vector<wstring> lines;
// Only show version/branch for player 0 to avoid cluttering each splitscreen viewport
if (iPad == 0 && ClientConstants::SHOW_VERSION_WATERMARK)
{
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);
@ -1418,6 +1425,9 @@ void Gui::clearMessages(int iPad)
void Gui::addMessage(const wstring& _string,int iPad,bool bIsDeathMessage)
{
{ char buf[32]; sprintf_s(buf, "[CHAT] Display (pad=%d): ", iPad); OutputDebugStringA(buf); }
OutputDebugStringW(_string.c_str());
OutputDebugStringA("\n");
wstring string = _string; // 4J - Take copy of input as it is const
//int iScale=1;

View file

@ -110,6 +110,11 @@ void GuiComponent::drawStringLiteral(Font *font, const wstring& str, int x, int
font->drawShadowLiteral(str, x, y, color);
}
void GuiComponent::drawStringPreshaped(Font *font, const wstring& str, int x, int y, int color)
{
font->drawShadowLiteralPreshaped(str, x, y, color);
}
void GuiComponent::blit(int x, int y, int sx, int sy, int w, int h)
{
float us = 1 / 256.0f;

View file

@ -16,5 +16,6 @@ public:
void drawCenteredString(Font *font, const wstring& str, int x, int y, int color);
void drawString(Font *font, const wstring& str, int x, int y, int color);
void drawStringLiteral(Font* font, const wstring& str, int x, int y, int color);
void drawStringPreshaped(Font* font, const wstring& str, int x, int y, int color);
void blit(int x, int y, int sx, int sy, int w, int h);
};

View file

@ -442,7 +442,7 @@ void HumanoidModel::setupAnim(float time, float r, float bob, float yRot, float
if (riding)
{
if(uiBitmaskOverrideAnim&(1<<eAnim_SmallModel) == 0)
if ((uiBitmaskOverrideAnim&(1<<eAnim_SmallModel)) == 0)
{
arm0->xRot += -HALF_PI * 0.4f;
arm1->xRot += -HALF_PI * 0.4f;

View file

@ -289,6 +289,20 @@ void ItemInHandRenderer::renderItem(shared_ptr<LivingEntity> mob, shared_ptr<Ite
float xo = 0.0f;
float yo = 0.3f;
// Re position height of held item if skin is small
if (mob->getAnimOverrideBitmask() & (1 << HumanoidModel::eAnim_SmallModel))
{
if (mob->isRiding())
{
std::shared_ptr<Entity> ridingEntity = mob->riding;
if (ridingEntity != nullptr) // Safety check;
{
yo += 0.3f; // reverts the change in Boat.cpp for smaller models.
}
}
}
glEnable(GL_RESCALE_NORMAL);
glTranslatef(-xo, -yo, 0);

View file

@ -157,6 +157,11 @@ LevelRenderer::LevelRenderer(Minecraft *mc, Textures *textures)
dirtyChunkPresent = false;
lastDirtyChunkFound = 0;
visibleLists_layer0 = nullptr;
visibleLists_layer1 = nullptr;
visibleCount_layer0 = 0;
visibleCount_layer1 = 0;
this->mc = mc;
this->textures = textures;
@ -455,6 +460,12 @@ void LevelRenderer::allChanged(int playerIndex)
// delete sortedChunks[playerIndex]; // 4J - removed - not sorting our chunks anymore
}
// Free old visible chunk lists
delete[] visibleLists_layer0;
delete[] visibleLists_layer1;
visibleLists_layer0 = nullptr;
visibleLists_layer1 = nullptr;
chunks[playerIndex] = ClipChunkArray(xChunks * yChunks * zChunks);
// sortedChunks[playerIndex] = new vector<Chunk *>(xChunks * yChunks * zChunks); // 4J - removed - not sorting our chunks anymore
int id = 0;
@ -487,6 +498,13 @@ void LevelRenderer::allChanged(int playerIndex)
}
nonStackDirtyChunksAdded();
// Allocate visible chunk lists (worst case: all chunks visible)
int totalChunkCount = xChunks * yChunks * zChunks;
visibleLists_layer0 = new int[totalChunkCount];
visibleLists_layer1 = new int[totalChunkCount];
visibleCount_layer0 = 0;
visibleCount_layer1 = 0;
if (level != nullptr)
{
shared_ptr<Entity> player = mc->cameraTargetPlayer;
@ -705,42 +723,42 @@ int LevelRenderer::render(shared_ptr<LivingEntity> player, int layer, double alp
{
int playerIndex = mc->player->GetXboxPad();
// 4J - added - if the number of players has changed, we need to rebuild things for the new draw distance this will require
if( lastPlayerCount[playerIndex] != activePlayers() )
{
allChanged();
}
else if (mc->options->viewDistance != lastViewDistance)
{
allChanged();
}
// Only check allChanged/resortChunks on layer 0 — they only need to run once per frame
if (layer == 0)
{
// 4J - added - if the number of players has changed, we need to rebuild things for the new draw distance this will require
if( lastPlayerCount[playerIndex] != activePlayers() )
{
allChanged();
}
else if (mc->options->viewDistance != lastViewDistance)
{
allChanged();
}
totalChunks = 0;
offscreenChunks = 0;
occludedChunks = 0;
renderedChunks = 0;
emptyChunks = 0;
double xd = player->x - xOld[playerIndex];
double yd = player->y - yOld[playerIndex];
double zd = player->z - zOld[playerIndex];
if (xd * xd + yd * yd + zd * zd > 4 * 4)
{
xOld[playerIndex] = player->x;
yOld[playerIndex] = player->y;
zOld[playerIndex] = player->z;
resortChunks(Mth::floor(player->x), Mth::floor(player->y), Mth::floor(player->z));
}
}
double xOff = player->xOld + (player->x - player->xOld) * alpha;
double yOff = player->yOld + (player->y - player->yOld) * alpha;
double zOff = player->zOld + (player->z - player->zOld) * alpha;
double xd = player->x - xOld[playerIndex];
double yd = player->y - yOld[playerIndex];
double zd = player->z - zOld[playerIndex];
if (xd * xd + yd * yd + zd * zd > 4 * 4)
{
xOld[playerIndex] = player->x;
yOld[playerIndex] = player->y;
zOld[playerIndex] = player->z;
resortChunks(Mth::floor(player->x), Mth::floor(player->y), Mth::floor(player->z));
// sort(sortedChunks[playerIndex]->begin(),sortedChunks[playerIndex]->end(), DistanceChunkSorter(player)); // 4J - removed - not sorting our chunks anymore
}
Lighting::turnOff();
int count = renderChunks(0, static_cast<int>(chunks[playerIndex].length), layer, alpha);
@ -749,6 +767,38 @@ int LevelRenderer::render(shared_ptr<LivingEntity> player, int layer, double alp
}
// Lightweight render path for the second layer 1 pass — skips allChanged/resortChunks checks
// and just does GL setup + visible list iteration + cleanup.
// Assumes Lighting::turnOff() was already called by the prior render() invocation.
void LevelRenderer::renderChunksDirect(int layer, double alpha)
{
shared_ptr<LivingEntity> player = mc->cameraTargetPlayer;
if (player == nullptr) return;
mc->gameRenderer->turnOnLightLayer(alpha);
double xOff = player->xOld + (player->x - player->xOld) * alpha;
double yOff = player->yOld + (player->y - player->yOld) * alpha;
double zOff = player->zOld + (player->z - player->zOld) * alpha;
glPushMatrix();
glTranslatef(static_cast<float>(-xOff), static_cast<float>(-yOff), static_cast<float>(-zOff));
int *lists = (layer == 0) ? visibleLists_layer0 : visibleLists_layer1;
int numVisible = (layer == 0) ? visibleCount_layer0 : visibleCount_layer1;
bool first = true;
if (lists != nullptr)
{
for (int i = 0; i < numVisible; i++)
{
if (RenderManager.CBuffCall(lists[i], first))
first = false;
}
}
glPopMatrix();
mc->gameRenderer->turnOffLightLayer(alpha);
}
#ifdef __PSVITA__
#include <stdlib.h>
@ -819,23 +869,35 @@ int LevelRenderer::renderChunks(int from, int to, int layer, double alpha)
bool first = true;
int count = 0;
ClipChunk *pClipChunk = chunks[playerIndex].data;
unsigned char emptyFlag = LevelRenderer::CHUNK_FLAG_EMPTY0 << layer;
for( int i = 0; i < chunks[playerIndex].length; i++, pClipChunk++ )
// Use compact visible lists built during cull() instead of iterating all chunks
int *lists = (layer == 0) ? visibleLists_layer0 : visibleLists_layer1;
int numVisible = (layer == 0) ? visibleCount_layer0 : visibleCount_layer1;
if (lists != nullptr)
{
if( !pClipChunk->visible ) continue; // This will be set if the chunk isn't visible, or isn't compiled, or has both empty flags set
if( pClipChunk->globalIdx == -1 ) continue; // Not sure if we should ever encounter this... TODO check
if( ( globalChunkFlags[pClipChunk->globalIdx] & emptyFlag ) == emptyFlag ) continue; // Check that this particular layer isn't empty
// List can be calculated directly from the chunk's global idex
int list = pClipChunk->globalIdx * 2 + layer;
list += chunkLists;
if(RenderManager.CBuffCall(list, first))
for (int i = 0; i < numVisible; i++)
{
first = false;
if (RenderManager.CBuffCall(lists[i], first))
first = false;
count++;
}
}
else
{
// Fallback: iterate all chunks (before visible lists are allocated)
ClipChunk *pClipChunk = chunks[playerIndex].data;
unsigned char emptyFlag = LevelRenderer::CHUNK_FLAG_EMPTY0 << layer;
for( int i = 0; i < chunks[playerIndex].length; i++, pClipChunk++ )
{
if( !pClipChunk->visible ) continue;
if( pClipChunk->globalIdx == -1 ) continue;
if( ( globalChunkFlags[pClipChunk->globalIdx] & emptyFlag ) == emptyFlag ) continue;
int list = pClipChunk->globalIdx * 2 + layer;
list += chunkLists;
if(RenderManager.CBuffCall(list, first))
first = false;
count++;
}
count++;
}
#ifdef __PSVITA__
@ -1968,6 +2030,9 @@ bool LevelRenderer::updateDirtyChunks()
for( int y = 0; y < CHUNK_Y_COUNT; y++ )
{
ClipChunk *pClipChunk = &chunks[p][(z * yChunks + y) * xChunks + x];
// Early-out for non-dirty chunks - avoids distance calculation for the vast majority at steady state
if( !(globalChunkFlags[ pClipChunk->globalIdx ] & CHUNK_FLAG_DIRTY) )
continue;
// Get distance to this chunk - deliberately not calling the chunk's method of doing this to avoid overheads (passing entitie, type conversion etc.) that this involves
int xd = pClipChunk->xm - px;
int yd = pClipChunk->ym - py;
@ -2163,7 +2228,9 @@ bool LevelRenderer::updateDirtyChunks()
else
{
// Nothing to do - clear flags that there are things to process, unless it's been a while since we found any dirty chunks in which case force a check next time through
if( ( System::currentTimeMillis() - lastDirtyChunkFound ) > FORCE_DIRTY_CHUNK_CHECK_PERIOD_MS )
// Scale recheck period with render distance to reduce wasted full-scans at high distances
int recheckPeriod = (xChunks >= 60) ? 1000 : (xChunks >= 40) ? 500 : FORCE_DIRTY_CHUNK_CHECK_PERIOD_MS;
if( ( System::currentTimeMillis() - lastDirtyChunkFound ) > recheckPeriod )
{
dirtyChunkPresent = true;
}
@ -2564,32 +2631,81 @@ void LevelRenderer::cull(Culler *culler, float a)
fdraw[i * 4 + 3] = static_cast<float>(fd->m_Frustum[i][3] + (fx * -fc->xOff) + (fy * -fc->yOff) + (fz * -fc->zOff));
}
ClipChunk *pClipChunk = chunks[playerIndex].data;
int vis = 0;
int total = 0;
int numWrong = 0;
for (unsigned int i = 0; i < chunks[playerIndex].length; i++)
// Reset visible chunk lists for this frame
visibleCount_layer0 = 0;
visibleCount_layer1 = 0;
// Column-level frustum culling: test one AABB per XZ column before testing individual Y chunks.
// At dist 64 this reduces ~278K clip() calls to ~17K column tests + per-chunk tests only for visible columns.
for (int x = 0; x < xChunks; x++)
{
unsigned char flags = pClipChunk->globalIdx == -1 ? 0 : globalChunkFlags[ pClipChunk->globalIdx ];
for (int z = 0; z < zChunks; z++)
{
// Build column AABB from bottom and top chunks in this column
ClipChunk *bottomChunk = &chunks[playerIndex][(z * yChunks + 0) * xChunks + x];
ClipChunk *topChunk = &chunks[playerIndex][(z * yChunks + (yChunks - 1)) * xChunks + x];
float columnAABB[6] = {
bottomChunk->aabb[0], bottomChunk->aabb[1], bottomChunk->aabb[2], // minX, minY, minZ
bottomChunk->aabb[3], topChunk->aabb[4], bottomChunk->aabb[5] // maxX, maxY(top), maxZ
};
// Always perform frustum cull test
bool clipres = clip(pClipChunk->aabb, fdraw);
// Test entire column against frustum
if (!clip(columnAABB, fdraw))
{
// Entire column outside frustum — mark all Y chunks invisible
for (int y = 0; y < yChunks; y++)
{
ClipChunk *pClipChunk = &chunks[playerIndex][(z * yChunks + y) * xChunks + x];
pClipChunk->visible = false;
}
continue;
}
if ( (flags & CHUNK_FLAG_COMPILED ) && ( ( flags & CHUNK_FLAG_EMPTYBOTH ) != CHUNK_FLAG_EMPTYBOTH ) )
{
pClipChunk->visible = clipres;
if( pClipChunk->visible ) vis++;
total++;
// Column is (partially) in frustum — test individual chunks
for (int y = 0; y < yChunks; y++)
{
ClipChunk *pClipChunk = &chunks[playerIndex][(z * yChunks + y) * xChunks + x];
unsigned char flags = pClipChunk->globalIdx == -1 ? 0 : globalChunkFlags[ pClipChunk->globalIdx ];
// Skip frustum test for confirmed-empty compiled chunks - they have nothing to render
if ((flags & CHUNK_FLAG_COMPILED) && (flags & CHUNK_FLAG_EMPTYBOTH) == CHUNK_FLAG_EMPTYBOTH)
{
pClipChunk->visible = false;
continue;
}
bool clipres = clip(pClipChunk->aabb, fdraw);
if ( (flags & CHUNK_FLAG_COMPILED ) && ( ( flags & CHUNK_FLAG_EMPTYBOTH ) != CHUNK_FLAG_EMPTYBOTH ) )
{
pClipChunk->visible = clipres;
if( pClipChunk->visible ) vis++;
total++;
}
else if (clipres)
{
pClipChunk->visible = true;
}
else
{
pClipChunk->visible = false;
}
// Build compact visible chunk lists for renderChunks()
if (pClipChunk->visible && pClipChunk->globalIdx != -1 && visibleLists_layer0 != nullptr)
{
int list = pClipChunk->globalIdx * 2 + chunkLists;
if (!((flags & CHUNK_FLAG_EMPTY0) == CHUNK_FLAG_EMPTY0))
visibleLists_layer0[visibleCount_layer0++] = list;
if (!((flags & CHUNK_FLAG_EMPTY1) == CHUNK_FLAG_EMPTY1))
visibleLists_layer1[visibleCount_layer1++] = list + 1;
}
}
}
else if (clipres)
{
pClipChunk->visible = true;
}
else
{
pClipChunk->visible = false;
}
pClipChunk++;
}
}

View file

@ -83,6 +83,7 @@ private:
void resortChunks(int xc, int yc, int zc);
public:
int render(shared_ptr<LivingEntity> player, int layer, double alpha, bool updateChunks);
void renderChunksDirect(int layer, double alpha);
private:
int renderChunks(int from, int to, int layer, double alpha);
public:
@ -270,6 +271,12 @@ public:
XLockFreeStack<int> dirtyChunksLockFreeStack;
// Visible chunk lists built by cull(), consumed by renderChunks()
int *visibleLists_layer0;
int *visibleLists_layer1;
int visibleCount_layer0;
int visibleCount_layer1;
bool dirtyChunkPresent;
int64_t lastDirtyChunkFound;
static const int FORCE_DIRTY_CHUNK_CHECK_PERIOD_MS = 125; // decreased from 250 to 125 - updated by detectiveren

View file

@ -77,6 +77,11 @@
#include "Windows64/stb_image_write.h"
#endif
#ifdef _WINDOWS64
#define STB_IMAGE_WRITE_IMPLEMENTATION
#include "Windows64/stb_image_write.h"
#endif
#ifdef __ORBIS__
#include "Orbis/Network/PsPlusUpsellWrapper_Orbis.h"
#endif
@ -4338,8 +4343,8 @@ void Minecraft::setLevel(MultiPlayerLevel *level, int message /*=-1*/, shared_pt
this->progressRenderer->progressStage(-1);
}
// 4J-PB - since we now play music in the menu, just let it keep playing
//soundEngine->playStreaming(L"", 0, 0, 0, 0, 0);
// Stop menu music and transition to game music for the new level
soundEngine->playStreaming(L"", 0, 0, 0, 1, 1);
// 4J - stop update thread from processing this level, which blocks until it is safe to move on - will be re-enabled if we set the level to be non-nullptr
gameRenderer->DisableUpdateThread();

View file

@ -34,6 +34,9 @@
#ifdef _WINDOWS64
#include "Windows64/Network/WinsockNetLayer.h"
#endif
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
#include "..\Minecraft.Server\ServerLogger.h"
#endif
#include <sstream>
#ifdef SPLIT_SAVES
#include "../Minecraft.World/ConsoleSaveFileSplit.h"
@ -561,6 +564,7 @@ MinecraftServer::MinecraftServer()
m_serverPausedEvent = new C4JThread::Event;
m_saveOnExit = false;
m_deleteWorldOnExit = false;
m_suspending = false;
m_ugcPlayersVersion = 0;
@ -734,10 +738,11 @@ bool MinecraftServer::initServer(int64_t seed, NetworkGameInitData *initData, DW
if( findSeed )
{
int worldSizeChunks = (initData && initData->xzSize > 0) ? (int)initData->xzSize : 54;
#ifdef __PSVITA__
seed = BiomeSource::findSeed(pLevelType, &running);
seed = BiomeSource::findSeed(pLevelType, &running, worldSizeChunks);
#else
seed = BiomeSource::findSeed(pLevelType);
seed = BiomeSource::findSeed(pLevelType, worldSizeChunks);
#endif
}
@ -887,6 +892,15 @@ bool MinecraftServer::loadLevel(LevelStorageSource *storageSource, const wstring
// }
ProgressRenderer *mcprogress = Minecraft::GetInstance()->progressRenderer;
// 4J Added - store save folder name for potential hardcore world deletion
{
char szSaveFolder[MAX_SAVEFILENAME_LENGTH] = {};
StorageManager.GetSaveUniqueFilename(szSaveFolder);
wchar_t wSaveFolder[MAX_SAVEFILENAME_LENGTH] = {};
mbstowcs(wSaveFolder, szSaveFolder, MAX_SAVEFILENAME_LENGTH - 1);
m_saveFolderName = wSaveFolder;
}
// 4J TODO - free levels here if there are already some?
levels = ServerLevelArray(3);
@ -997,6 +1011,12 @@ bool MinecraftServer::loadLevel(LevelStorageSource *storageSource, const wstring
#endif
levels[i]->getLevelData()->setGameType(gameType);
#ifdef MINECRAFT_SERVER_BUILD
// Dedicated server: server.properties hardcore flag is authoritative
levels[i]->getLevelData()->setHardcore(isHardcore());
#endif
// Offline/client-hosted: keep the world's saved hardcore flag from NBT
if(app.getLevelGenerationOptions() != nullptr)
{
LevelGenerationOptions *mapOptions = app.getLevelGenerationOptions();
@ -1642,7 +1662,7 @@ bool MinecraftServer::isNetherEnabled()
bool MinecraftServer::isHardcore()
{
return false;
return app.GetGameHostOption(eGameHostOption_Hardcore) > 0;
}
int MinecraftServer::getOperatorUserPermissionLevel()
@ -1795,7 +1815,6 @@ void MinecraftServer::run(int64_t seed, void *lpParameter)
chunkPacketManagement_PostTick();
}
lastTime = getCurrentTimeMillis();
// int64_t afterall = System::currentTimeMillis();
// PIXReportCounter(L"Server time all",(float)(afterall-beforeall));
// PIXReportCounter(L"Server ticks",(float)tickcount);
@ -1864,11 +1883,23 @@ void MinecraftServer::run(int64_t seed, void *lpParameter)
QueryPerformanceCounter(&qwTime);
#endif
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
LARGE_INTEGER asTicksPerSec, asT0, asT1;
QueryPerformanceFrequency(&asTicksPerSec);
double asSecsPerTick = 1.0 / (double)asTicksPerSec.QuadPart;
QueryPerformanceCounter(&asT0);
LARGE_INTEGER asAfterPlayers, asAfterLevels, asAfterRules, asAfterFlush;
#endif
if (players != nullptr)
{
players->saveAll(nullptr);
}
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
QueryPerformanceCounter(&asAfterPlayers);
#endif
for (unsigned int j = 0; j < levels.length; j++)
{
if( s_bServerHalted ) break;
@ -1884,6 +1915,11 @@ void MinecraftServer::run(int64_t seed, void *lpParameter)
PIXEndNamedEvent();
#endif
}
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
QueryPerformanceCounter(&asAfterLevels);
#endif
if (!s_bServerHalted)
{
#if defined(_XBOX_ONE) || defined(__ORBIS__)
@ -1895,7 +1931,24 @@ void MinecraftServer::run(int64_t seed, void *lpParameter)
PIXBeginNamedEvent(0, "Save to disc");
#endif
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
QueryPerformanceCounter(&asAfterRules);
#endif
levels[0]->saveToDisc(Minecraft::GetInstance()->progressRenderer, true);
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
QueryPerformanceCounter(&asAfterFlush);
ServerRuntime::LogInfof("world-io",
"autosave breakdown: players=%.0fms levels=%.0fms rules=%.0fms flush=%.0fms total=%.0fms",
(asAfterPlayers.QuadPart - asT0.QuadPart) * asSecsPerTick * 1000.0,
(asAfterLevels.QuadPart - asAfterPlayers.QuadPart) * asSecsPerTick * 1000.0,
(asAfterRules.QuadPart - asAfterLevels.QuadPart) * asSecsPerTick * 1000.0,
(asAfterFlush.QuadPart - asAfterRules.QuadPart) * asSecsPerTick * 1000.0,
(asAfterFlush.QuadPart - asT0.QuadPart) * asSecsPerTick * 1000.0);
#endif
#if defined(_XBOX_ONE) || defined(__ORBIS__)
PIXEndNamedEvent();
#endif

View file

@ -265,6 +265,8 @@ private:
private:
// 4J Added
bool m_saveOnExit;
bool m_deleteWorldOnExit; // 4J Added - for hardcore mode world deletion
wstring m_saveFolderName; // 4J Added - stored for hardcore world deletion
bool m_suspending;
public:
@ -278,6 +280,9 @@ public:
void chunkPacketManagement_PostTick();
void setSaveOnExit(bool save) { m_saveOnExit = save; s_bSaveOnExitAnswered = true; }
void setDeleteWorldOnExit(bool del) { m_deleteWorldOnExit = del; }
bool getDeleteWorldOnExit() const { return m_deleteWorldOnExit; }
const wstring& getSaveFolderName() const { return m_saveFolderName; }
void Suspend();
bool IsSuspending();

View file

@ -139,18 +139,24 @@ bool MultiPlayerChunkCache::reallyHasChunk(int x, int z)
return hasData[idx];
}
void MultiPlayerChunkCache::drop(int x, int z)
void MultiPlayerChunkCache::drop(const int x, const int z)
{
// 4J Stu - We do want to drop any entities in the chunks, especially for the case when a player is dead as they will
// not get the RemoveEntity packet if an entity is removed.
LevelChunk *chunk = getChunk(x, z);
if (!chunk->isEmpty())
const int ix = x + XZOFFSET;
const int iz = z + XZOFFSET;
if ((ix < 0) || (ix >= XZSIZE)) return;
if ((iz < 0) || (iz >= XZSIZE)) return;
const int idx = ix * XZSIZE + iz;
LevelChunk* chunk = cache[idx];
if (chunk != nullptr && !chunk->isEmpty())
{
// Added parameter here specifies that we don't want to delete tile entities, as they won't get recreated unless they've got update packets
// The tile entities are in general only created on the client by virtue of the chunk rebuild
// Drop entities in the chunks, especially for the case when a player is dead
// as they will not get the RemoveEntity packet if an entity is removed.
// Don't delete tile entities, as they won't get recreated unless they've got
// update packets. Tile entities are created on the client by the chunk rebuild.
chunk->unload(false);
// 4J - We just want to clear out the entities in the chunk, but everything else should be valid
// Keep chunk in cache with structural data intact.
chunk->loaded = true;
}
}

View file

@ -27,6 +27,7 @@ public:
void adjustPlayer(shared_ptr<Player> player);
bool isCutScene();
void setLocalMode(GameType *mode);
GameType* getLocalPlayerMode() const { return localPlayerMode; }
virtual void initPlayer(shared_ptr<Player> player);
virtual bool canHurtPlayer();
virtual bool destroyBlock(int x, int y, int z, int face);

View file

@ -17,7 +17,8 @@
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
#include "../Minecraft.Server/ServerLogManager.h"
#include "../Minecraft.Server/Access/Access.h"
#include "../Minecraft.World/Socket.h"
#include "..\Minecraft.Server/Security/SecurityConfig.h"
#include "..\Minecraft.World\Socket.h"
#endif
// #ifdef __PS3__
// #include "PS3/Network/NetworkPlayerSony.h"
@ -150,6 +151,20 @@ void PendingConnection::sendPreLoginResponse()
}
}
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
// Security: strip real XUIDs from pre-login response to prevent unauthenticated enumeration.
// The client receives the correct player count but cannot identify who is connected.
// Real XUID data is sent post-login via PlayerInfoPacket broadcasts.
if (ServerRuntime::Security::GetSettings().hidePlayerListPreLogin)
{
for (DWORD i = 0; i < ugcXuidCount; ++i)
{
ugcXuids[i] = INVALID_XUID;
}
ugcFriendsOnlyBits = 0;
}
#endif
#if 0
if (false)// server->onlineMode) // 4J - removed
{
@ -203,6 +218,56 @@ void PendingConnection::handleLogin(shared_ptr<LoginPacket> packet)
duplicateXuid = true;
}
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
// Cross-reference: if someone claims the same XUID as an existing player from a different IP,
// log and reject as a potential spoofing attempt.
// Note: this runs on the main tick thread (via PendingConnection::tick -> Connection::tick ->
// handleLogin), same thread that mutates the player list, so no lock is needed.
if (!duplicateXuid && loginXuid != INVALID_XUID)
{
std::string newIp;
unsigned char newSmallId = GetPendingConnectionSmallId(connection);
bool hasNewIp = ServerRuntime::ServerLogManager::TryGetConnectionRemoteIp(newSmallId, &newIp);
for (auto &existingPlayer : server->getPlayers()->players)
{
if (existingPlayer == nullptr) continue;
PlayerUID existingXuid = existingPlayer->connection->m_offlineXUID;
if (existingXuid == INVALID_XUID) existingXuid = existingPlayer->connection->m_onlineXUID;
if (existingXuid == loginXuid)
{
if (hasNewIp)
{
std::string existingIp;
INetworkPlayer *np = existingPlayer->connection->getNetworkPlayer();
if (np != nullptr)
{
unsigned char existingSmallId = np->GetSmallId();
if (ServerRuntime::ServerLogManager::TryGetConnectionRemoteIp(existingSmallId, &existingIp))
{
if (existingIp != newIp)
{
app.DebugPrintf("SECURITY: XUID spoofing suspected - XUID 0x%016llx claimed from IP %s while already connected from IP %s\n",
(unsigned long long)loginXuid, newIp.c_str(), existingIp.c_str());
ServerRuntime::ServerLogManager::OnXuidSpoofDetected(newSmallId, name, newIp.c_str(), existingIp.c_str());
duplicateXuid = true;
}
}
}
}
else
{
// Cannot verify IP -- treat same-XUID connection as suspicious
app.DebugPrintf("SECURITY: XUID 0x%016llx claimed but could not verify source IP\n",
(unsigned long long)loginXuid);
duplicateXuid = true;
}
break;
}
}
}
#endif
bool bannedXuid = false;
if (loginXuid != INVALID_XUID)
{
@ -243,7 +308,11 @@ void PendingConnection::handleLogin(shared_ptr<LoginPacket> packet)
else if (!whitelistSatisfied)
{
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
// Cache name->XUID so `whitelist add <name>` can resolve the XUID
ServerRuntime::ServerLogManager::CachePlayerXuid(name, loginXuid);
ServerRuntime::ServerLogManager::OnRejectedPlayerLogin(GetPendingConnectionSmallId(connection), name, ServerRuntime::ServerLogManager::eLoginRejectReason_NotWhitelisted);
app.DebugPrintf("WHITELIST: Rejected %ls (XUID: 0x%016llx) - use 'whitelist add %ls' to allow\n",
name.c_str(), (unsigned long long)loginXuid, name.c_str());
#endif
disconnect(DisconnectPacket::eDisconnect_Banned);
}
@ -330,11 +399,17 @@ void PendingConnection::handleAcceptedLogin(shared_ptr<LoginPacket> packet)
PlayerUID playerXuid = packet->m_offlineXuid;
if(playerXuid == INVALID_XUID) playerXuid = packet->m_onlineXuid;
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
// Cache name->XUID for console commands (whitelist add, revoketoken, etc.)
ServerRuntime::ServerLogManager::CachePlayerXuid(name, playerXuid);
#endif
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);
ServerRuntime::ServerLogManager::OnAcceptedPlayerLogin(GetPendingConnectionSmallId(connection), name,
packet->m_offlineXuid, packet->m_onlineXuid, packet->m_isGuest);
#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

View file

@ -487,37 +487,52 @@ void PlayerChunkMap::getChunkAndRemovePlayer(int x, int z, shared_ptr<ServerPlay
}
// 4J - added - actually create & add player to a playerchunk, if there is one queued for this player.
// Processes up to CHUNKS_PER_PLAYER_PER_TICK requests per call to speed up initial chunk loading.
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
static const int CHUNKS_PER_PLAYER_PER_TICK = 16;
#else
static const int CHUNKS_PER_PLAYER_PER_TICK = 1;
#endif
void PlayerChunkMap::tickAddRequests(shared_ptr<ServerPlayer> player)
{
if( addRequests.size() )
{
// Find the nearest chunk request to the player
int px = static_cast<int>(player->x);
int pz = static_cast<int>(player->z);
int minDistSq = -1;
auto itNearest = addRequests.end();
for (auto it = addRequests.begin(); it != addRequests.end(); it++)
{
if( it->player == player )
for (int processed = 0; processed < CHUNKS_PER_PLAYER_PER_TICK; processed++)
{
// Find the nearest chunk request to the player
int minDistSq = -1;
auto itNearest = addRequests.end();
for (auto it = addRequests.begin(); it != addRequests.end(); it++)
{
int xm = ( it->x * 16 ) + 8;
int zm = ( it->z * 16 ) + 8;
int distSq = (xm - px) * (xm - px) +
(zm - pz) * (zm - pz);
if( ( minDistSq == -1 ) || ( distSq < minDistSq ) )
if( it->player == player )
{
minDistSq = distSq;
itNearest = it;
int xm = ( it->x * 16 ) + 8;
int zm = ( it->z * 16 ) + 8;
int distSq = (xm - px) * (xm - px) +
(zm - pz) * (zm - pz);
if( ( minDistSq == -1 ) || ( distSq < minDistSq ) )
{
minDistSq = distSq;
itNearest = it;
}
}
}
}
// If we found one at all, then do this one
if( itNearest != addRequests.end() )
{
getChunk(itNearest->x, itNearest->z, true)->add(itNearest->player);
addRequests.erase(itNearest);
// If we found one, process it and continue; otherwise done
if( itNearest != addRequests.end() )
{
getChunk(itNearest->x, itNearest->z, true)->add(itNearest->player);
addRequests.erase(itNearest);
}
else
{
break;
}
}
}
}
@ -792,6 +807,14 @@ void PlayerChunkMap::setRadius(int newRadius)
int xc = static_cast<int>(player->x) >> 4;
int zc = static_cast<int>(player->z) >> 4;
for (auto it = addRequests.begin(); it != addRequests.end(); )
{
if (it->player == player)
it = addRequests.erase(it);
else
++it;
}
for (int x = xc - newRadius; x <= xc + newRadius; x++)
for (int z = zc - newRadius; z <= zc + newRadius; z++)
{
@ -801,6 +824,18 @@ void PlayerChunkMap::setRadius(int newRadius)
getChunkAndAddPlayer(x, z, player);
}
}
// Remove chunks that are outside the new radius
for (int x = xc - radius; x <= xc + radius; x++)
{
for (int z = zc - radius; z <= zc + radius; z++)
{
if (x < xc - newRadius || x > xc + newRadius || z < zc - newRadius || z > zc + newRadius)
{
getChunkAndRemovePlayer(x, z, player);
}
}
}
}
}

View file

@ -16,27 +16,28 @@
#include "../Minecraft.World/net.minecraft.world.level.tile.entity.h"
#include "../Minecraft.World/net.minecraft.world.level.saveddata.h"
#include "../Minecraft.World/net.minecraft.world.entity.animal.h"
#include "../Minecraft.World/net.minecraft.network.h"
#include "../Minecraft.World/net.minecraft.world.food.h"
#include "../Minecraft.World/AABB.h"
#include "../Minecraft.World/Pos.h"
#include "../Minecraft.World/SharedConstants.h"
#include "../Minecraft.World/ChatPacket.h"
#include "../Minecraft.World/StringHelpers.h"
#include "../Minecraft.World/Socket.h"
#include "../Minecraft.World/Achievements.h"
#include "../Minecraft.World/net.minecraft.h"
#include "EntityTracker.h"
#include "../Minecraft.World/LevelData.h"
#include "ServerConnection.h"
#include "../Minecraft.World/GenericStats.h"
#include "../Minecraft.World/JavaMath.h"
#include "..\Minecraft.World\ListTag.h"
// 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"
#include "../Minecraft.Server/Access/Access.h"
#include "../Minecraft.Server/Security/IdentityTokenManager.h"
#include "../Minecraft.Server/Security/SecurityConfig.h"
#include "../Minecraft.Server/Security/ConnectionCipher.h"
extern bool g_Win64DedicatedServer;
#endif
namespace
@ -85,6 +86,9 @@ PlayerConnection::PlayerConnection(MinecraftServer *server, Connection *connecti
m_onlineXUID = INVALID_XUID;
m_bHasClientTickedOnce = false;
m_logSmallId = 0;
m_identityVerified = false;
m_identityChallengeTick = -1;
m_securityGateOpen = true; // default open; closed when cipher is required
// 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)
@ -143,6 +147,14 @@ void PlayerConnection::tick()
{
dropSpamTickCount--;
}
// Ensure server-side player tick runs even when no move packet was received this tick.
// Without this, environmental damage (drowning, fire, lava) is never applied to clients
// that don't send frequent move packets.
if (!didTick && player != nullptr)
{
player->doTick(false);
}
}
void PlayerConnection::disconnect(DisconnectPacket::eDisconnectReason reason)
@ -483,9 +495,9 @@ void PlayerConnection::handlePlayerAction(shared_ptr<PlayerActionPacket> packet)
}
else if (packet->action == PlayerActionPacket::STOP_DESTROY_BLOCK)
{
player->gameMode->stopDestroyBlock(x, y, z);
bool destroyed = player->gameMode->stopDestroyBlock(x, y, z);
server->getPlayers()->prioritiseTileChanges(x, y, z, level->dimension->id); // 4J added - make sure that the update packets for this get prioritised over other general world updates
if (level->getTile(x, y, z) != 0) player->connection->send(std::make_shared<TileUpdatePacket>(x, y, z, level));
if (!destroyed && level->getTile(x, y, z) != 0) player->connection->send(std::make_shared<TileUpdatePacket>(x, y, z, level));
}
else if (packet->action == PlayerActionPacket::ABORT_DESTROY_BLOCK)
{
@ -612,6 +624,22 @@ void PlayerConnection::onDisconnect(DisconnectPacket::eDisconnectReason reason,
LeaveCriticalSection(&done_cs);
}
void PlayerConnection::openSecurityGate()
{
if (m_securityGateOpen)
return;
m_securityGateOpen = true;
// Flush all buffered packets now that the cipher is active
for (auto &buffered : m_securityBuffer)
{
send(buffered);
}
m_securityBuffer.clear();
m_securityBuffer.shrink_to_fit();
}
void PlayerConnection::onUnhandledPacket(shared_ptr<Packet> packet)
{
// logger.warning(getClass() + " wasn't prepared to deal with a " + packet.getClass());
@ -622,6 +650,39 @@ void PlayerConnection::send(shared_ptr<Packet> packet)
{
if( connection->getSocket() != nullptr )
{
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
// Security gate: when require-secure-client is enabled, buffer ALL outgoing
// packets until the cipher handshake completes. Only the cipher handshake
// CustomPayloadPacket (MC|CKey) is sent immediately. Once the cipher activates,
// openSecurityGate() flushes the buffer. This prevents unsecured/old clients
// from receiving any game data (PlayerInfoPackets, XUIDs, etc.) before being kicked.
if (!m_securityGateOpen)
{
// Allow cipher handshake packets through immediately
if (packet->getId() == 250)
{
auto cpp = dynamic_pointer_cast<CustomPayloadPacket>(packet);
if (cpp != nullptr &&
(cpp->identifier == CustomPayloadPacket::CIPHER_KEY_CHANNEL ||
cpp->identifier == CustomPayloadPacket::CIPHER_ACK_CHANNEL ||
cpp->identifier == CustomPayloadPacket::CIPHER_ON_CHANNEL))
{
// Fall through to send
}
else
{
m_securityBuffer.push_back(packet);
return;
}
}
else
{
m_securityBuffer.push_back(packet);
return;
}
}
#endif
if( !server->getPlayers()->canReceiveAllPackets( player ) )
{
// Check if we are allowed to send this packet type
@ -817,9 +878,7 @@ void PlayerConnection::handleInteract(shared_ptr<InteractPacket> packet)
{
if ((target->GetType() == eTYPE_ITEMENTITY) || (target->GetType() == eTYPE_EXPERIENCEORB) || (target->GetType() == eTYPE_ARROW) || target == player)
{
//disconnect("Attempting to attack an invalid entity");
//server.warn("Player " + player.getName() + " tried to attack an invalid entity");
return;
return;
}
player->attack(target);
}
@ -1064,10 +1123,19 @@ void PlayerConnection::handleServerSettingsChanged(shared_ptr<ServerSettingsChan
{
if(packet->action==ServerSettingsChangedPacket::HOST_IN_GAME_SETTINGS)
{
// Need to check that this player has permission to change each individual setting?
INetworkPlayer *networkPlayer = getNetworkPlayer();
if( (networkPlayer != nullptr && networkPlayer->IsHost()) || player->isModerator())
bool isHost = (networkPlayer != nullptr && networkPlayer->IsHost());
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
// On dedicated servers, only the host can change server settings.
// Moderators (OPs) should not be able to alter game rules.
if (!isHost)
{
app.DebugPrintf("SECURITY: Non-host player %ls attempted to change server settings\n",
player->getName().c_str());
return;
}
#endif
if( isHost || player->isModerator())
{
app.SetGameHostOption(eGameHostOption_FireSpreads, app.GetGameHostOption(packet->data,eGameHostOption_FireSpreads));
app.SetGameHostOption(eGameHostOption_TNT, app.GetGameHostOption(packet->data,eGameHostOption_TNT));
@ -1090,14 +1158,81 @@ void PlayerConnection::handleServerSettingsChanged(shared_ptr<ServerSettingsChan
void PlayerConnection::handleKickPlayer(shared_ptr<KickPlayerPacket> packet)
{
INetworkPlayer *networkPlayer = getNetworkPlayer();
if( (networkPlayer != nullptr && networkPlayer->IsHost()) || player->isModerator())
bool isHost = (networkPlayer != nullptr && networkPlayer->IsHost());
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
// Live ops.json check for non-host players
if (!isHost)
{
PlayerUID kickerXuid = m_offlineXUID;
if (kickerXuid == INVALID_XUID) kickerXuid = m_onlineXUID;
if (!ServerRuntime::Access::IsPlayerOp(kickerXuid))
{
app.DebugPrintf("SECURITY: Non-OP player %ls attempted to kick\n", player->getName().c_str());
{
INetworkPlayer *npLog = getNetworkPlayer();
if (npLog != nullptr)
ServerRuntime::ServerLogManager::OnUnauthorizedCommand(npLog->GetSmallId(), player->getName(), "kick");
}
return;
}
}
#endif
if( isHost || player->isModerator())
{
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
// On dedicated servers, non-host moderators cannot kick other moderators or the host.
if (!isHost)
{
for (auto &checkingPlayer : server->getPlayers()->players)
{
if (checkingPlayer != nullptr &&
checkingPlayer->connection->getNetworkPlayer() != nullptr &&
checkingPlayer->connection->getNetworkPlayer()->GetSmallId() == packet->m_networkSmallId)
{
if (checkingPlayer->isModerator() ||
checkingPlayer->connection->getNetworkPlayer()->IsHost())
{
app.DebugPrintf("SECURITY: Moderator %ls tried to kick host/moderator %ls\n",
player->getName().c_str(), checkingPlayer->getName().c_str());
return;
}
break;
}
}
}
app.DebugPrintf("CMD: Player %ls kicked player with smallId=%d\n",
player->getName().c_str(), packet->m_networkSmallId);
#endif
server->getPlayers()->kickPlayerByShortId(packet->m_networkSmallId);
}
}
void PlayerConnection::handleGameCommand(shared_ptr<GameCommandPacket> packet)
{
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
INetworkPlayer *networkPlayer = getNetworkPlayer();
bool isHost = (networkPlayer != nullptr && networkPlayer->IsHost());
if (!isHost)
{
// Live ops.json check - in-memory isModerator() can be stale if ops.json was edited mid-session
PlayerUID cmdXuid = m_offlineXUID;
if (cmdXuid == INVALID_XUID) cmdXuid = m_onlineXUID;
if (!ServerRuntime::Access::IsPlayerOp(cmdXuid))
{
app.DebugPrintf("SECURITY: Non-OP player %ls attempted server command id=%d\n",
player->getName().c_str(), static_cast<int>(packet->command));
{
INetworkPlayer *npLog = getNetworkPlayer();
if (npLog != nullptr)
ServerRuntime::ServerLogManager::OnUnauthorizedCommand(npLog->GetSmallId(), player->getName(), "game-command");
}
return;
}
}
app.DebugPrintf("CMD: Player %ls (OP=%d, Host=%d) executed command id=%d\n",
player->getName().c_str(), player->isModerator() ? 1 : 0, isHost ? 1 : 0,
static_cast<int>(packet->command));
#endif
MinecraftServer::getInstance()->getCommandDispatcher()->performCommand(player, packet->command, packet->data);
}
@ -1110,22 +1245,12 @@ void PlayerConnection::handleClientCommand(shared_ptr<ClientCommandPacket> packe
{
player = server->getPlayers()->respawn(player, player->m_enteredEndExitPortal?0:player->dimension, true);
}
//else if (player.getLevel().getLevelData().isHardcore())
//{
// if (server.isSingleplayer() && player.name.equals(server.getSingleplayerName()))
// {
// player.connection.disconnect("You have died. Game over, man, it's game over!");
// server.selfDestruct();
// }
// else
// {
// BanEntry ban = new BanEntry(player.name);
// ban.setReason("Death in Hardcore");
// server.getPlayers().getBans().add(ban);
// player.connection.disconnect("You have died. Game over, man, it's game over!");
// }
//}
else if (player->level->getLevelData()->isHardcore())
{
// Hardcore mode — server rejects respawn. Ban and disconnect are already
// handled in ServerPlayer::die() via banPlayerForHardcoreDeath().
return;
}
else
{
if (player->getHealth() > 0) return;
@ -1377,10 +1502,21 @@ void PlayerConnection::handleKeepAlive(shared_ptr<KeepAlivePacket> packet)
void PlayerConnection::handlePlayerInfo(shared_ptr<PlayerInfoPacket> packet)
{
// Need to check that this player has permission to change each individual setting?
INetworkPlayer *networkPlayer = getNetworkPlayer();
if( (networkPlayer != nullptr && networkPlayer->IsHost()) || player->isModerator() )
bool isHost = (networkPlayer != nullptr && networkPlayer->IsHost());
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
// Live ops.json check for non-host players
if (!isHost)
{
PlayerUID infoXuid = m_offlineXUID;
if (infoXuid == INVALID_XUID) infoXuid = m_onlineXUID;
if (!ServerRuntime::Access::IsPlayerOp(infoXuid))
{
return;
}
}
#endif
if( isHost || player->isModerator() )
{
shared_ptr<ServerPlayer> serverPlayer;
// Find the player being edited
@ -1458,7 +1594,24 @@ void PlayerConnection::handlePlayerInfo(shared_ptr<PlayerInfoPacket> packet)
serverPlayer->setPlayerGamePrivilege(Player::ePlayerGamePrivilege_CanToggleClassicHunger,Player::getPlayerGamePrivilege(packet->m_playerPrivileges,Player::ePlayerGamePrivilege_CanToggleClassicHunger) );
serverPlayer->setPlayerGamePrivilege(Player::ePlayerGamePrivilege_CanTeleport,Player::getPlayerGamePrivilege(packet->m_playerPrivileges,Player::ePlayerGamePrivilege_CanTeleport) );
}
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
// On dedicated servers, OP can only be granted/revoked if the target is in ops.json.
// This prevents runtime OP escalation via crafted PlayerInfoPackets.
bool wantsOp = Player::getPlayerGamePrivilege(packet->m_playerPrivileges, Player::ePlayerGamePrivilege_Op) != 0;
PlayerUID targetXuid = serverPlayer->connection->m_offlineXUID;
if (targetXuid == INVALID_XUID) targetXuid = serverPlayer->connection->m_onlineXUID;
if (wantsOp && !ServerRuntime::Access::IsPlayerOp(targetXuid))
{
app.DebugPrintf("SECURITY: Host tried to OP player %ls who is not in ops.json\n",
serverPlayer->getName().c_str());
}
else
{
serverPlayer->setPlayerGamePrivilege(Player::ePlayerGamePrivilege_Op, wantsOp ? 1u : 0u);
}
#else
serverPlayer->setPlayerGamePrivilege(Player::ePlayerGamePrivilege_Op,Player::getPlayerGamePrivilege(packet->m_playerPrivileges,Player::ePlayerGamePrivilege_Op) );
#endif
}
}
@ -1496,6 +1649,46 @@ void PlayerConnection::handlePlayerAbilities(shared_ptr<PlayerAbilitiesPacket> p
void PlayerConnection::handleCustomPayload(shared_ptr<CustomPayloadPacket> customPayloadPacket)
{
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
// Identity token response from client
if (CustomPayloadPacket::IDENTITY_TOKEN_RESPONSE.compare(customPayloadPacket->identifier) == 0)
{
PlayerUID xuid = m_offlineXUID;
if (xuid == INVALID_XUID) xuid = m_onlineXUID;
bool tokenValid = false;
if (customPayloadPacket->length == ServerRuntime::Security::IdentityTokenManager::TOKEN_SIZE &&
customPayloadPacket->data.length == ServerRuntime::Security::IdentityTokenManager::TOKEN_SIZE &&
customPayloadPacket->data.data != nullptr)
{
tokenValid = ServerRuntime::Security::GetIdentityTokenManager().VerifyToken(xuid, customPayloadPacket->data.data);
}
if (tokenValid)
{
m_identityVerified = true;
app.DebugPrintf("SECURITY: Identity token verified for player %ls\n", player->getName().c_str());
INetworkPlayer *npLog = getNetworkPlayer();
if (npLog != nullptr)
ServerRuntime::ServerLogManager::OnIdentityTokenVerified(npLog->GetSmallId());
}
else
{
app.DebugPrintf("SECURITY: Identity token MISMATCH for player %ls - will disconnect\n", player->getName().c_str());
app.DebugPrintf("SECURITY: If this player lost their token, use: revoketoken %ls\n", player->getName().c_str());
INetworkPlayer *npLog = getNetworkPlayer();
if (npLog != nullptr)
ServerRuntime::ServerLogManager::OnIdentityTokenMismatch(npLog->GetSmallId(), player->getName());
// Defer disconnect to avoid re-entrancy issues during packet dispatch
setWasKicked();
closeOnTick();
}
return;
}
#endif
#if 0
if (CustomPayloadPacket.CUSTOM_BOOK_PACKET.equals(customPayloadPacket.identifier))
if (CustomPayloadPacket::CUSTOM_BOOK_PACKET.compare(customPayloadPacket->identifier) == 0)
{
ByteArrayInputStream bais(customPayloadPacket->data);
@ -1535,7 +1728,9 @@ void PlayerConnection::handleCustomPayload(shared_ptr<CustomPayloadPacket> custo
player->inventory->setItem(player->inventory->selected, sentItem);
}
}
else if (CustomPayloadPacket::TRADER_SELECTION_PACKET.compare(customPayloadPacket->identifier) == 0)
else
#endif
if (CustomPayloadPacket::TRADER_SELECTION_PACKET.compare(customPayloadPacket->identifier) == 0)
{
ByteArrayInputStream bais(customPayloadPacket->data);
DataInputStream input(&bais);

View file

@ -1,6 +1,7 @@
#include "ConsoleInputSource.h"
#include "../Minecraft.World/PacketListener.h"
#include "../Minecraft.World/JavaIntHash.h"
#include <atomic>
class MinecraftServer;
class Connection;
@ -130,15 +131,30 @@ public:
void setShowOnMaps(bool bVal);
void setWasKicked() { m_bWasKicked = true; }
bool getWasKicked() { return m_bWasKicked; }
void setWasKicked() { m_bWasKicked.store(true); }
bool getWasKicked() { return m_bWasKicked.load(); }
// 4J Added
bool hasClientTickedOnce() { return m_bHasClientTickedOnce; }
// Identity token verification state (accessed from both recv and main threads)
std::atomic<bool> m_identityVerified;
std::atomic<int> m_identityChallengeTick;
// Security gate: buffer packets until cipher handshake completes
bool m_securityGateOpen;
vector<shared_ptr<Packet>> m_securityBuffer;
bool isIdentityVerified() const { return m_identityVerified; }
int getIdentityChallengeTick() const { return m_identityChallengeTick; }
void setIdentityChallengeTick(int tick) { m_identityChallengeTick = tick; }
void setIdentityVerified(bool v) { m_identityVerified = v; }
bool isSecurityGateOpen() const { return m_securityGateOpen; }
void openSecurityGate();
private:
bool m_bCloseOnTick;
vector<wstring> m_texturesRequested;
bool m_bWasKicked;
std::atomic<bool> m_bWasKicked{false};
};

View file

@ -39,7 +39,17 @@
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
#include "../Minecraft.Server/Access/Access.h"
#include "../Minecraft.Server/Common\StringUtils.h"
#include "../Minecraft.Server/ServerLogger.h"
#include "../Minecraft.Server/ServerLogManager.h"
#include "../Minecraft.Server/ServerProperties.h"
#include "../Minecraft.Server/Security/SecurityConfig.h"
#include "../Minecraft.Server/Security/ConnectionCipher.h"
#include "../Minecraft.Server/Security/CipherHandshakeEnforcer.h"
#include "../Minecraft.Server/Security/IdentityTokenManager.h"
extern bool g_Win64DedicatedServer;
static unsigned int s_playerListTickCount = 0;
static const int kIdentityResponseGraceTicks = 200; // 10 seconds at 20 TPS
#endif
// 4J - this class is fairly substantially altered as there didn't seem any point in porting code for banning, whitelisting, ops etc.
@ -67,6 +77,7 @@ PlayerList::PlayerList(MinecraftServer *server)
int rawMax = server->settings->getInt(L"max-players", 8);
maxPlayers = static_cast<unsigned int>(Mth::clamp(rawMax, 1, MINECRAFT_NET_MAX_PLAYERS));
doWhiteList = false;
InitializeCriticalSection(&m_banCS);
InitializeCriticalSection(&m_kickPlayersCS);
InitializeCriticalSection(&m_closePlayersCS);
}
@ -80,6 +91,7 @@ PlayerList::~PlayerList()
player->gameMode = nullptr;
}
DeleteCriticalSection(&m_banCS);
DeleteCriticalSection(&m_kickPlayersCS);
DeleteCriticalSection(&m_closePlayersCS);
}
@ -271,12 +283,19 @@ bool PlayerList::placeNewPlayer(Connection *connection, shared_ptr<ServerPlayer>
static_cast<BYTE>(playerIndex), level->useNewSeaLevel(),
player->getAllPlayerGamePrivileges(),
level->getLevelData()->getXZSize(),
level->getLevelData()->getHellScale()));
level->getLevelData()->getHellScale(),
level->getLevelData()->isHardcore()));
playerConnection->send(std::make_shared<SetSpawnPositionPacket>(spawnPos->x, spawnPos->y, spawnPos->z));
playerConnection->send(std::make_shared<PlayerAbilitiesPacket>(&player->abilities));
playerConnection->send(std::make_shared<SetCarriedItemPacket>(player->inventory->selected));
delete spawnPos;
// Identify this server as a fork so the client can enable extended
// features (e.g. render-distance-independent player list). Upstream
// clients will silently ignore the unknown channel.
playerConnection->send(std::make_shared<CustomPayloadPacket>(
CustomPayloadPacket::FORK_HELLO_CHANNEL, byteArray()));
updateEntireScoreboard(reinterpret_cast<ServerScoreboard *>(level->getScoreboard()), player);
sendLevelInfo(player, level);
@ -331,6 +350,49 @@ bool PlayerList::placeNewPlayer(Connection *connection, shared_ptr<ServerPlayer>
}
}
}
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
// Initiate stream cipher handshake if enabled.
// Send MC|CKey with the generated key. Old clients will ignore the unknown channel.
if (g_Win64DedicatedServer && ServerRuntime::Security::GetSettings().enableStreamCipher)
{
BYTE smallId = 0;
Socket *cipherSock = connection->getSocket();
INetworkPlayer *cipherNp = cipherSock ? cipherSock->getPlayer() : nullptr;
if (cipherNp != nullptr && !cipherNp->IsLocal())
{
smallId = cipherNp->GetSmallId();
uint8_t key[ServerRuntime::Security::StreamCipher::KEY_SIZE];
if (ServerRuntime::Security::GetCipherRegistry().PrepareKey(smallId, key))
{
byteArray keyData(ServerRuntime::Security::StreamCipher::KEY_SIZE);
memcpy(keyData.data, key, ServerRuntime::Security::StreamCipher::KEY_SIZE);
playerConnection->send(std::make_shared<CustomPayloadPacket>(
CustomPayloadPacket::CIPHER_KEY_CHANNEL, keyData));
SecureZeroMemory(key, sizeof(key));
app.DebugPrintf("Server: Sent MC|CKey to player %ls (smallId=%d)\n",
player->getName().c_str(), smallId);
// Register with enforcer for timeout tracking
if (ServerRuntime::Security::GetSettings().requireSecureClient)
{
ServerRuntime::Security::GetHandshakeEnforcer().OnCipherKeySent(smallId, s_playerListTickCount);
}
}
// Close the security gate AFTER sending the essential login sequence
// and MC|CKey. The login setup packets (LoginPacket, spawn position,
// abilities, chunks, teleport) must arrive in plaintext before the
// cipher handshake completes. Only subsequent tick data is buffered
// until the handshake finishes and openSecurityGate() flushes.
if (ServerRuntime::Security::GetSettings().requireSecureClient)
{
playerConnection->m_securityGateOpen = false;
}
}
}
#endif
return true;
}
@ -563,6 +625,16 @@ void PlayerList::move(shared_ptr<ServerPlayer> player)
void PlayerList::remove(shared_ptr<ServerPlayer> player)
{
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
if (g_Win64DedicatedServer && player->connection != nullptr)
{
INetworkPlayer *np = player->connection->getNetworkPlayer();
if (np != nullptr)
{
ServerRuntime::Security::GetHandshakeEnforcer().OnDisconnected(np->GetSmallId());
}
}
#endif
save(player);
//4J Stu - We don't want to save the map data for guests, so when we are sure that the player is gone delete the map
if(player->isGuest()) playerIo->deleteMapFilesForPlayer(player);
@ -588,7 +660,17 @@ if (player->riding != nullptr)
{
players.erase(it);
}
//broadcastAll(shared_ptr<PlayerInfoPacket>( new PlayerInfoPacket(player->name, false, 9999) ) );
// Notify fork clients that this player has left the server so they can
// clean up IQNet/Tab list entries. Uses a custom payload channel so the
// wire format of existing packets is unchanged (upstream clients simply
// ignore the unknown channel).
{
const wstring& name = player->getName();
byteArray payload(static_cast<int>(name.size() * sizeof(wchar_t)));
memcpy(payload.data, name.c_str(), payload.length);
broadcastAll(std::make_shared<CustomPayloadPacket>(
CustomPayloadPacket::FORK_PLAYER_LEAVE_CHANNEL, payload));
}
removePlayerFromReceiving(player);
player->connection = nullptr; // Must remove reference to connection, or else there is a circular dependency
@ -754,6 +836,13 @@ shared_ptr<ServerPlayer> PlayerList::respawn(shared_ptr<ServerPlayer> serverPlay
// necessary)
updatePlayerGameMode(player, serverPlayer, level);
// 4J Added: Hardcore mode — force Adventure mode on respawn
if (server->getLevel(0)->getLevelData()->isHardcore())
{
player->gameMode->setGameModeForPlayer(GameType::ADVENTURE);
player->connection->send(std::make_shared<GameEventPacket>(GameEventPacket::CHANGE_GAME_MODE, GameType::ADVENTURE->getId()));
}
if(serverPlayer->wonGame && targetDimension == oldDimension && serverPlayer->getHealth() > 0)
{
// If the player is still alive and respawning to the same dimension, they are just being added back from someone else viewing the Win screen
@ -791,7 +880,7 @@ shared_ptr<ServerPlayer> PlayerList::respawn(shared_ptr<ServerPlayer> serverPlay
player->connection->send( std::make_shared<RespawnPacket>( static_cast<char>(player->dimension), player->level->getSeed(), player->level->getMaxBuildHeight(),
player->gameMode->getGameModeForPlayer(), level->difficulty, level->getLevelData()->getGenerator(),
player->level->useNewSeaLevel(), player->entityId, level->getLevelData()->getXZSize(), level->getLevelData()->getHellScale() ) );
player->level->useNewSeaLevel(), player->entityId, level->getLevelData()->getXZSize(), level->getLevelData()->getHellScale(), level->getLevelData()->isHardcore() ) );
player->connection->teleport(player->x, player->y, player->z, player->yRot, player->xRot);
player->connection->send( std::make_shared<SetExperiencePacket>( player->experienceProgress, player->totalExperience, player->experienceLevel) );
@ -908,7 +997,7 @@ void PlayerList::toggleDimension(shared_ptr<ServerPlayer> player, int targetDime
player->connection->send(std::make_shared<RespawnPacket>(static_cast<char>(player->dimension), newLevel->getSeed(), newLevel->getMaxBuildHeight(),
player->gameMode->getGameModeForPlayer(), newLevel->difficulty, newLevel->getLevelData()->getGenerator(),
newLevel->useNewSeaLevel(), player->entityId, newLevel->getLevelData()->getXZSize(), newLevel->getLevelData()->getHellScale()));
newLevel->useNewSeaLevel(), player->entityId, newLevel->getLevelData()->getXZSize(), newLevel->getLevelData()->getHellScale(), newLevel->getLevelData()->isHardcore()));
oldLevel->removeEntityImmediately(player);
player->removed = false;
@ -1003,15 +1092,16 @@ void PlayerList::repositionAcrossDimension(shared_ptr<Entity> entity, int lastDi
addPlayerToReceiving(player);
}
if (lastDimension != 1)
xt = static_cast<double>(Mth::clamp(static_cast<int>(xt), -Level::MAX_LEVEL_SIZE + 128, Level::MAX_LEVEL_SIZE - 128));
zt = static_cast<double>(Mth::clamp(static_cast<int>(zt), -Level::MAX_LEVEL_SIZE + 128, Level::MAX_LEVEL_SIZE - 128));
if (entity->isAlive())
{
xt = static_cast<double>(Mth::clamp(static_cast<int>(xt), -Level::MAX_LEVEL_SIZE + 128, Level::MAX_LEVEL_SIZE - 128));
zt = static_cast<double>(Mth::clamp(static_cast<int>(zt), -Level::MAX_LEVEL_SIZE + 128, Level::MAX_LEVEL_SIZE - 128));
if (entity->isAlive())
newLevel->addEntity(entity);
entity->moveTo(xt, entity->y, zt, entity->yRot, entity->xRot);
newLevel->tick(entity, false);
// Portal forcing only for non-End exits (End exits go to spawn, not a portal)
if (lastDimension != 1)
{
newLevel->addEntity(entity);
entity->moveTo(xt, entity->y, zt, entity->yRot, entity->xRot);
newLevel->tick(entity, false);
newLevel->cache->autoCreate = true;
newLevel->getPortalForcer()->force(entity, xOriginal, yOriginal, zOriginal, yRotOriginal);
newLevel->cache->autoCreate = false;
@ -1023,6 +1113,131 @@ void PlayerList::repositionAcrossDimension(shared_ptr<Entity> entity, int lastDi
void PlayerList::tick()
{
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
++s_playerListTickCount;
// Cipher handshake enforcement: kick clients that haven't completed the handshake
if (g_Win64DedicatedServer &&
ServerRuntime::Security::GetSettings().enableStreamCipher &&
ServerRuntime::Security::GetSettings().requireSecureClient)
{
std::vector<unsigned char> expired;
std::vector<unsigned char> completed;
ServerRuntime::Security::GetHandshakeEnforcer().CheckTimeouts(s_playerListTickCount, expired, completed);
for (unsigned char smallId : expired)
{
app.DebugPrintf("SECURITY: Kicking unsecured client (smallId=%d) - cipher handshake timed out\n", smallId);
ServerRuntime::ServerLogManager::OnUnsecuredClientKicked(smallId);
EnterCriticalSection(&m_closePlayersCS);
m_smallIdsToClose.push_back(smallId);
LeaveCriticalSection(&m_closePlayersCS);
}
// Report cipher completion and open security gate for all completed handshakes
for (unsigned char smallId : completed)
{
// Open the security gate -- flush buffered game packets now that cipher is active
for (auto &p : players)
{
if (p == nullptr || p->connection == nullptr) continue;
INetworkPlayer *np = p->connection->getNetworkPlayer();
if (np != nullptr && np->GetSmallId() == smallId)
{
if (!p->connection->isSecurityGateOpen())
{
p->connection->openSecurityGate();
}
break;
}
}
if (ServerRuntime::Security::GetSettings().requireChallengeToken)
{
ServerRuntime::ServerLogManager::OnCipherHandshakeCompleted(smallId);
}
else
{
ServerRuntime::ServerLogManager::OnCipherCompletedNoTokenRequired(smallId);
}
}
// For newly-completed cipher handshakes, initiate identity token exchange
if (ServerRuntime::Security::GetSettings().requireChallengeToken)
{
for (unsigned char smallId : completed)
{
// Find the player by smallId
for (auto &p : players)
{
if (p == nullptr || p->connection == nullptr) continue;
INetworkPlayer *np = p->connection->getNetworkPlayer();
if (np == nullptr || np->GetSmallId() != smallId) continue;
PlayerUID xuid = p->connection->m_offlineXUID;
if (xuid == INVALID_XUID) xuid = p->connection->m_onlineXUID;
if (p->connection->getIdentityChallengeTick() >= 0)
{
// Already challenged, skip
}
else if (ServerRuntime::Security::GetIdentityTokenManager().HasToken(xuid))
{
// Returning player - challenge them
p->connection->send(std::make_shared<CustomPayloadPacket>(
CustomPayloadPacket::IDENTITY_TOKEN_CHALLENGE, byteArray()));
p->connection->setIdentityChallengeTick(s_playerListTickCount);
app.DebugPrintf("Server: Sent identity challenge to %ls (smallId=%d)\n",
p->getName().c_str(), smallId);
}
else
{
// New player - issue a token over the encrypted channel
uint8_t token[ServerRuntime::Security::IdentityTokenManager::TOKEN_SIZE];
if (ServerRuntime::Security::GetIdentityTokenManager().IssueToken(xuid, token))
{
byteArray tokenData(ServerRuntime::Security::IdentityTokenManager::TOKEN_SIZE);
memcpy(tokenData.data, token, ServerRuntime::Security::IdentityTokenManager::TOKEN_SIZE);
p->connection->send(std::make_shared<CustomPayloadPacket>(
CustomPayloadPacket::IDENTITY_TOKEN_ISSUE, tokenData));
SecureZeroMemory(token, sizeof(token));
p->connection->setIdentityVerified(true);
app.DebugPrintf("Server: Issued identity token to %ls (smallId=%d)\n",
p->getName().c_str(), smallId);
ServerRuntime::ServerLogManager::OnIdentityTokenIssued(smallId);
}
}
break;
}
}
// Enforce identity token response timeout
for (auto &p : players)
{
if (p == nullptr || p->connection == nullptr) continue;
int challengeTick = p->connection->getIdentityChallengeTick();
if (challengeTick >= 0 && !p->connection->isIdentityVerified() &&
(s_playerListTickCount - challengeTick) > kIdentityResponseGraceTicks)
{
app.DebugPrintf("SECURITY: Kicking %ls - identity token response timed out\n",
p->getName().c_str());
INetworkPlayer *npLog = p->connection->getNetworkPlayer();
if (npLog != nullptr)
ServerRuntime::ServerLogManager::OnIdentityTokenTimeout(npLog->GetSmallId(), p->getName());
p->connection->setIdentityChallengeTick(-1); // prevent re-queuing
INetworkPlayer *np = p->connection->getNetworkPlayer();
if (np != nullptr)
{
EnterCriticalSection(&m_closePlayersCS);
m_smallIdsToClose.push_back(np->GetSmallId());
LeaveCriticalSection(&m_closePlayersCS);
}
}
}
}
}
#endif
// 4J - brought changes to how often this is sent forward from 1.2.3
if (++sendAllPlayerInfoIn > SEND_PLAYER_INFO_INTERVAL)
{
@ -1722,6 +1937,7 @@ bool PlayerList::isXuidBanned(PlayerUID xuid)
bool banned = false;
EnterCriticalSection(&m_banCS);
for(PlayerUID it : m_bannedXuids)
{
if( ProfileManager.AreXUIDSEqual( xuid, it ) )
@ -1730,6 +1946,7 @@ bool PlayerList::isXuidBanned(PlayerUID xuid)
break;
}
}
LeaveCriticalSection(&m_banCS);
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
if (!banned && g_Win64DedicatedServer)
@ -1741,8 +1958,118 @@ bool PlayerList::isXuidBanned(PlayerUID xuid)
return banned;
}
void PlayerList::banXuid(PlayerUID xuid)
{
// 4J Added - for hardcore mode ban-on-death
// Ban a player's XUID. Used when a player dies in a hardcore multiplayer world.
if(xuid == INVALID_XUID) return;
EnterCriticalSection(&m_banCS);
// Check if already banned
bool alreadyBanned = false;
for(PlayerUID it : m_bannedXuids)
{
if( ProfileManager.AreXUIDSEqual( xuid, it ) )
{
alreadyBanned = true;
break;
}
}
if(!alreadyBanned)
{
m_bannedXuids.push_back(xuid);
app.DebugPrintf("PlayerList::banXuid - Player XUID banned for hardcore death\n");
}
LeaveCriticalSection(&m_banCS);
}
void PlayerList::banPlayerForHardcoreDeath(ServerPlayer *player)
{
if (player == nullptr) return;
// Always apply the in-memory XUID ban (works for both client-hosted and dedicated)
banXuid(player->getOnlineXuid());
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
if (g_Win64DedicatedServer)
{
const std::string playerName = ServerRuntime::StringUtils::WideToUtf8(player->getName());
ServerRuntime::Access::BanMetadata metadata = ServerRuntime::Access::BanManager::BuildDefaultMetadata("Hardcore Death");
metadata.reason = "Died in hardcore mode";
// Ban online XUID
ServerRuntime::Access::AddPlayerBan(player->getOnlineXuid(), playerName, metadata);
// Also ban offline XUID if it differs (follows CliCommandBan pattern)
PlayerUID offlineXuid = player->getXuid();
if (offlineXuid != INVALID_XUID && offlineXuid != player->getOnlineXuid())
{
ServerRuntime::Access::AddPlayerBan(offlineXuid, playerName, metadata);
}
// Ban the player's IP address (uses same access path as CliCommandBanIp)
auto serverConfig = ServerRuntime::LoadServerPropertiesConfig();
if (serverConfig.hardcoreBanIp)
{
if (player->connection != nullptr && player->connection->connection != nullptr && player->connection->connection->getSocket() != nullptr)
{
const unsigned char smallId = player->connection->connection->getSocket()->getSmallId();
std::string ip;
if (smallId != 0 && ServerRuntime::ServerLogManager::TryGetConnectionRemoteIp(smallId, &ip))
{
ServerRuntime::Access::AddIpBan(ip, metadata);
ServerRuntime::LogInfof("Hardcore", "Player %s banned (XUID + IP %s) for dying in hardcore mode.", playerName.c_str(), ip.c_str());
}
else
{
ServerRuntime::LogInfof("Hardcore", "Player %s banned (XUID only, IP not available) for dying in hardcore mode.", playerName.c_str());
}
}
else
{
ServerRuntime::LogInfof("Hardcore", "Player %s banned (XUID only, no connection) for dying in hardcore mode.", playerName.c_str());
}
}
else
{
ServerRuntime::LogInfof("Hardcore", "Player %s banned (XUID only, IP ban disabled) for dying in hardcore mode.", playerName.c_str());
}
// Send ban reason then defer the actual close to the next tick, because this
// method runs mid-tick inside ServerPlayer::die(). A synchronous disconnect
// can invalidate the player/connection while the tick is still executing.
if (player->connection != nullptr)
{
player->connection->send(std::make_shared<DisconnectPacket>(DisconnectPacket::eDisconnect_Banned));
player->connection->closeOnTick();
}
}
else
#endif
{
// Client-hosted: force-save so the host cannot circumvent death by quitting without saving.
// On dedicated server the autosave handles persistence, so skip the forced save to avoid
// the client getting stuck on a "host is saving" screen during disconnect.
app.SetXuiServerAction(ProfileManager.GetPrimaryPad(), eXuiServerAction_SaveGame);
}
}
// AP added for Vita so the range can be increased once the level starts
void PlayerList::setViewDistance(int newViewDistance)
void PlayerList::setViewDistance(const int newViewDistance)
{
viewDistance = newViewDistance;
for (size_t i = 0; i < server->levels.length; i++)
{
ServerLevel* level = server->levels[i];
if (level != nullptr)
{
level->getChunkMap()->setRadius(newViewDistance);
}
}
}

View file

@ -31,6 +31,7 @@ private:
// 4J Added
vector<PlayerUID> m_bannedXuids;
CRITICAL_SECTION m_banCS; // 4J Added - protects m_bannedXuids for concurrent access
deque<BYTE> m_smallIdsToKick;
CRITICAL_SECTION m_kickPlayersCS;
deque<BYTE> m_smallIdsToClose;
@ -135,6 +136,8 @@ public:
void closePlayerConnectionBySmallId(BYTE networkSmallId);
void queueSmallIdForRecycle(BYTE smallId);
bool isXuidBanned(PlayerUID xuid);
void banXuid(PlayerUID xuid); // 4J Added - for hardcore mode ban-on-death
void banPlayerForHardcoreDeath(ServerPlayer *player); // Persistent XUID + IP ban on hardcore death
// AP added for Vita so the range can be increased once the level starts
void setViewDistance(int newViewDistance);
};

View file

@ -234,11 +234,26 @@ void PlayerRenderer::render(shared_ptr<Entity> _mob, double x, double y, double
armorParts1->sneaking = armorParts2->sneaking = resModel->sneaking = mob->isSneaking();
double yp = y - mob->heightOffset;
if (mob->isSneaking() && !mob->instanceof(eTYPE_LOCALPLAYER))
if (mob->isSneaking())
{
yp -= 2 / 16.0f;
}
if (mob->getAnimOverrideBitmask() & (1 << HumanoidModel::eAnim_SmallModel))
{
if (mob->isRiding())
{
std::shared_ptr<Entity> ridingEntity = mob->riding;
if (ridingEntity != nullptr) // Safety check;
{
if (ridingEntity->instanceof(eTYPE_BOAT))
{
yp += 0.25f; // reverts the change in Boat.cpp for smaller models.
}
}
}
}
// Check if an idle animation is needed
if(mob->getAnimOverrideBitmask()&(1<<HumanoidModel::eAnim_HasIdle))
{

View file

@ -303,6 +303,12 @@ void SelectWorldScreen::WorldSelectionList::renderItem(int i, int x, int y, int
info = parent->conversionLang + L" " + info;
}
// 4J Added: Show [Hardcore] badge for hardcore worlds
if (levelSummary->isHardcore())
{
name = name + L" [Hardcore]";
}
parent->drawString(parent->font, name, x + 2, y + 1, 0xffffff);
parent->drawString(parent->font, id, x + 2, y + 12, 0x808080);
parent->drawString(parent->font, info, x + 2, y + 12 + 10, 0x808080);

View file

@ -80,54 +80,27 @@ vector<LevelChunk *> *ServerChunkCache::getLoadedChunkList()
return &m_loadedChunkList;
}
void ServerChunkCache::drop(int x, int z)
void ServerChunkCache::drop(const int x, const int z)
{
// 4J - we're not dropping things anymore now that we have a fixed sized cache
#ifdef _LARGE_WORLDS
const int ix = x + XZOFFSET;
const int iz = z + XZOFFSET;
if ((ix < 0) || (ix >= XZSIZE)) return;
if ((iz < 0) || (iz >= XZSIZE)) return;
const int idx = ix * XZSIZE + iz;
LevelChunk* chunk = cache[idx];
bool canDrop = false;
// if (level->dimension->mayRespawn())
// {
// Pos *spawnPos = level->getSharedSpawnPos();
// int xd = x * 16 + 8 - spawnPos->x;
// int zd = z * 16 + 8 - spawnPos->z;
// delete spawnPos;
// int r = 128;
// if (xd < -r || xd > r || zd < -r || zd > r)
// {
// canDrop = true;
//}
// }
// else
if (chunk != nullptr)
{
canDrop = true;
m_toDrop.push_back(chunk);
}
if(canDrop)
{
int ix = x + XZOFFSET;
int iz = z + XZOFFSET;
// Check we're in range of the stored level
if( ( ix < 0 ) || ( ix >= XZSIZE ) ) return;
if( ( iz < 0 ) || ( iz >= XZSIZE ) ) return;
int idx = ix * XZSIZE + iz;
LevelChunk *chunk = cache[idx];
if(chunk)
{
m_toDrop.push_back(chunk);
}
}
#endif
}
void ServerChunkCache::dropAll()
{
#ifdef _LARGE_WORLDS
for (LevelChunk *chunk : m_loadedChunkList)
{
drop(chunk->x, chunk->z);
}
#endif
}
}
// 4J - this is the original (and virtual) interface to create
@ -954,9 +927,14 @@ bool ServerChunkCache::tick()
int ix = chunk->x + XZOFFSET;
int iz = chunk->z + XZOFFSET;
int idx = ix * XZSIZE + iz;
delete m_unloadedCache[idx];
m_unloadedCache[idx] = chunk;
cache[idx] = nullptr;
}
else
{
continue;
}
}
m_toDrop.pop_front();
}

View file

@ -10,6 +10,10 @@
#include "../Minecraft.World/Socket.h"
#include "../Minecraft.World/net.minecraft.world.level.h"
#include "MultiPlayerLevel.h"
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
#include "..\Minecraft.Server\Security\SecurityConfig.h"
#include "..\Minecraft.Server\ServerLogManager.h"
#endif
ServerConnection::ServerConnection(MinecraftServer *server)
{
@ -40,6 +44,17 @@ void ServerConnection::addPlayerConnection(shared_ptr<PlayerConnection> uc)
void ServerConnection::handleConnection(shared_ptr<PendingConnection> uc)
{
EnterCriticalSection(&pending_cs);
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
int maxPending = ServerRuntime::Security::GetSettings().maxPendingConnections;
if (maxPending > 0 && static_cast<int>(pending.size()) >= maxPending)
{
LeaveCriticalSection(&pending_cs);
app.DebugPrintf("SECURITY: Rejecting connection, too many pending (%d/%d)\n",
static_cast<int>(pending.size()), maxPending);
uc->disconnect(DisconnectPacket::eDisconnect_ServerFull);
return;
}
#endif
pending.push_back(uc);
LeaveCriticalSection(&pending_cs);
}

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