Update launcher to work with latest clients:

- NO dev mode anymore. A valid certificate is ALWAYS required.
- NO Arctium server connection. But do not rename things, thanks.
- NO other features than connecting to servers with valid certificates attached.
  Means: NO mod loading or dev mode. For that use our full-featured launcher at https://arctium.io
- NO binary releases.
- Only modern versions (all branches) are supported.

What you can still do:
- Launch modern game clients and connect to servers with a valid certificate attached.
- Specify custom version and cdns URLs to be able to launch older clients or data from your own CDN.
This commit is contained in:
Fabian
2025-12-17 20:57:02 +01:00
parent 9bbabafee8
commit 85fceeb3f0
31 changed files with 560 additions and 1672 deletions

View File

@@ -0,0 +1,8 @@
<Solution>
<Configurations>
<Platform Name="x64" />
</Configurations>
<Project Path="src/Arctium.Game.Launcher.csproj">
<Platform Project="x64" />
</Project>
</Solution>

View File

@@ -1,34 +0,0 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31521.260
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Arctium.WoW.Launcher", "src\Arctium.WoW.Launcher.csproj", "{661B173A-D445-4706-9FF2-C0408ED62FA2}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|x64 = Debug|x64
Release|x64 = Release|x64
ReleaseCustomFiles|x64 = ReleaseCustomFiles|x64
ReleaseCustomFilesSilentMode|x64 = ReleaseCustomFilesSilentMode|x64
ReleaseSilentMode|x64 = ReleaseSilentMode|x64
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{661B173A-D445-4706-9FF2-C0408ED62FA2}.Debug|x64.ActiveCfg = Debug|x64
{661B173A-D445-4706-9FF2-C0408ED62FA2}.Debug|x64.Build.0 = Debug|x64
{661B173A-D445-4706-9FF2-C0408ED62FA2}.Release|x64.ActiveCfg = Release|x64
{661B173A-D445-4706-9FF2-C0408ED62FA2}.Release|x64.Build.0 = Release|x64
{661B173A-D445-4706-9FF2-C0408ED62FA2}.ReleaseCustomFiles|x64.ActiveCfg = ReleaseCustomFiles|x64
{661B173A-D445-4706-9FF2-C0408ED62FA2}.ReleaseCustomFiles|x64.Build.0 = ReleaseCustomFiles|x64
{661B173A-D445-4706-9FF2-C0408ED62FA2}.ReleaseCustomFilesSilentMode|x64.ActiveCfg = ReleaseCustomFilesSilentMode|x64
{661B173A-D445-4706-9FF2-C0408ED62FA2}.ReleaseCustomFilesSilentMode|x64.Build.0 = ReleaseCustomFilesSilentMode|x64
{661B173A-D445-4706-9FF2-C0408ED62FA2}.ReleaseSilentMode|x64.ActiveCfg = ReleaseSilentMode|x64
{661B173A-D445-4706-9FF2-C0408ED62FA2}.ReleaseSilentMode|x64.Build.0 = ReleaseSilentMode|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {3B82A525-E7C4-4B51-9C88-3AE74C1AEC60}
EndGlobalSection
EndGlobal

View File

@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2021-2023 Arctium
Copyright (c) 2021-2026 Arctium
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@@ -1,19 +1,46 @@
# THE FUTURE OF THIS LAUNCHER
# Arctium Game Launcher (Public version)
A game launcher for World of Warcraft that allows you to connect to custom servers with a valid tls certificate attached.
Binary releases [HERE](https://arctium.io/wow/)
**NOTE**: This is a publicly available, stripped down version of the launcher. The **full-featured** launcher is distributed under different terms and is available at https://arctium.io
As of today (2023-09-30) this project goes closed source and is not longer be MIT licensed.
### License, Copyright & Contributions
This does not affect the license of already published sources.
Please see our Open Source project [Documentation Repo](https://github.com/Arctium/Documentation)
### This launcher is going to be closed source in future.
## Special Request <3
Please do NOT remove the name `arctium` from the final binary.
Blizzard filters their crash logs based on localhost and the string `arctium` in the binary name.
What does this mean?
### Supported Clients
* 1.14.4 or later
* 3.4.2 or later
* 10.1.5 or later
- **No new MIT licensed binary releases.**
- **No new features, game support updates or OS updates on this repo.**
- **Current source with the listed supported game versions and supported systems stays public as it is.**
- **Future releases with updates for new game versions, operating systems, ... will be available at the same link (https://arctium.io/wow)**
**Later = all future version in that branch.**
#### Old README
Most recent README available here: [README](README_OLD.md)
### NOTE FOR SERVER CONNECTIONS
* A valid certificate matching your authentication/bnet server host name.
That certificate needs to be loaded by the authentication/bnet server too
### Binary Releases
There are no binary releases of this version. Our **full-featured** launcher has binary releases at https://arctium.io
## Building
### Build Prerequisites
* [.NET Core SDK 10.0.0 or later](https://dotnet.microsoft.com/download/dotnet/10.0)
* Optional for native builds: C++ workload through Visual Studio 2026 or latest C++ build tools
### Build Instructions Windows (native)
* Execute `dotnet publish -r win-x64 -c Configuration -p:platform="x64" -p:PublishAot=true`
* Native output is placed in the `build` folder.
## Usage
### Windows Usage
1. Copy `Actium Game Launcher.exe` to your World of Warcraft folder.
2. Edit the `WTF/Config.wtf` to set your portal or use a different config file with the `-config Config2.wtf` launch arg.
3. Run the `Actium Game Launcher.exe`
### Launch Parameters
Use `--help`

View File

@@ -1,70 +0,0 @@
# WoW-Launcher
A game launcher for World of Warcraft that allows you to connect to custom servers.
### License, Copyright & Contributions
Please see our Open Source project [Documentation Repo](https://github.com/Arctium/Documentation)
## Special Request <3
Please do NOT remove the name `arctium` from the final binary.
Blizzard filters their crash logs based on localhost and the string `arctium` in the binary name.
### NOTE FOR LOCAL DEVELOPMENT & SERVER CONNECTIONS
#### Applies to: 1.14.4 or later, 3.4.2 or later, 10.1.5 or later
* **Dev Mode is enabled by default for local game portals.**
* LOCAL HOSTNAME & IP: `REQUIRES` the `--dev` command line parameter to force the dev mode to avoid issues with invalid certificate chains. The launcher auto detectes local addresses and enables it.
* EXTERNAL HOSTNAME:
* `DO NOT` use the `--dev` command line parameter. It's blocked by the launcher.
* `USE` a valid certificate matching your authentication/bnet server host name.
* That certificate needs to be loaded by the authentication/bnet server too.
* EXTERNAL IP: `NOT SUPPORTED`
### Binary Releases
You can find signed binary releases at [Releases](https://github.com/Arctium/WoW-Launcher/releases)
### Supported Game Versions (Windows x86 64 bit, Release)
* Dragonflight: 10.x
* Shadowlands: 9.x
* Classic BC/WotLK: 2.5.x, 3.4.x (--version Classic)
* Classic Era: 1.14.x (--version ClassicEra)
## Building
### Build Prerequisites
* [.NET Core SDK 7.0.0 or later](https://dotnet.microsoft.com/download/dotnet/7.0)
* Optional for native builds: C++ workload through Visual Studio 2022 or latest C++ build tools
### Build Instructions Windows (native)
* Available runtime identifiers/platforms: win-x64/x64, win-arm64/ARM64
* Available release configurations: Release, ReleaseSilentMode, ReleaseCustomFiles, ReleaseCustomFilesSilentMode
* Execute `dotnet publish -r RuntimeIdentifier -c Configuration -p:platform="Platform"`
* Native output is placed in `build\Configuration\bin\native`
## Usage
### Windows Usage
1. Copy `Actium WoW Launcher.exe` to your World of Warcraft folder.
2. Optional: Edit the `WTF/Config.wtf` to set your portal or use a different config file with the `-config Config2.wtf` launch arg.
3. Run the `Actium WoW Launcher.exe`
### Static Auth Seed Usage
* Use the --staticseed launch parameter
* On server side add `179D3DC3235629D07113A9B3867F97A7` as auth seed in the database.
### Custom File Loading Usage
1. Get or create your own file mapping (.txt) file(s) and place it in the `mappings` folder.
File Format: `fileId;filePath`
2. Place your custom files (mods) in the `files` folder. Be sure to follow the correct folder structure.
### File mapping sources
* https://github.com/wowdev/wow-listfile
### Launch Parameters
Use `--help`
## WARNING
DO NOT USE THIS AS BASE FOR ANY OFFICIAL SERVER TOOLS.
IT WILL GET YOU BANNED THERE!!!

View File

@@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputPath>../build/$(Configuration)/bin</OutputPath>
<AssemblyName>Arctium Game Launcher</AssemblyName>
<ApplicationIcon>logo.ico</ApplicationIcon>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<Nullable>disable</Nullable>
<LangVersion>preview</LangVersion>
<Copyright>Arctium</Copyright>
<Platforms>x64</Platforms>
<ImplicitUsings>enable</ImplicitUsings>
<DefineConstants>$(DefineConstants);$(Platform)</DefineConstants>
<Configurations>Debug;Release</Configurations>
</PropertyGroup>
<PropertyGroup>
<ServerGarbageCollection>true</ServerGarbageCollection>
<GarbageCollectionAdaptationMode>1</GarbageCollectionAdaptationMode>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.1" />
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
</ItemGroup>
</Project>

View File

@@ -1,70 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputPath>../build/$(Configuration)/bin</OutputPath>
<AssemblyName>Arctium WoW Launcher</AssemblyName>
<ApplicationIcon>logo.ico</ApplicationIcon>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<TreatWarningsAsErrors>True</TreatWarningsAsErrors>
<AppendTargetFrameworkToOutputPath>False</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>False</AppendRuntimeIdentifierToOutputPath>
<Nullable>disable</Nullable>
<LangVersion>preview</LangVersion>
<AssemblyVersion>10.0.0.0</AssemblyVersion>
<FileVersion>10.0.0.0</FileVersion>
<Copyright>Arctium</Copyright>
<Platforms>x64</Platforms>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<ImplicitUsings>enable</ImplicitUsings>
<DefineConstants>$(DefineConstants);$(Platform)</DefineConstants>
<Configurations>Debug;Release;ReleaseSilentMode;ReleaseCustomFiles;ReleaseCustomFilesSilentMode</Configurations>
<Optimize>True</Optimize>
<!-- Native AOT settings -->
<PublishAot>true</PublishAot>
<RootAllApplicationAssemblies>false</RootAllApplicationAssemblies>
<IlcGenerateCompleteTypeMetadata>false</IlcGenerateCompleteTypeMetadata>
<IlcGenerateStackTraceData>false</IlcGenerateStackTraceData>
<UseSystemResourceKeys>true</UseSystemResourceKeys>
<EventSourceSupport>false</EventSourceSupport>
<InvariantGlobalization>true</InvariantGlobalization>
<IlcScanReflection>false</IlcScanReflection>
<IlcFoldIdenticalMethodBodies>true</IlcFoldIdenticalMethodBodies>
<IlcTrimMetadata>true</IlcTrimMetadata>
<IlcOptimizationPreference>Speed</IlcOptimizationPreference>
<GenerateRuntimeConfigurationFiles>false</GenerateRuntimeConfigurationFiles>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='Debug'">
<DefineConstants>$(DefineConstants);$(Platform);CUSTOM_FILES</DefineConstants>
<Optimize>False</Optimize>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='Release'">
<DebugType>none</DebugType>
<DebugSymbols>false</DebugSymbols>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='ReleaseCustomFiles'">
<DefineConstants>$(DefineConstants);$(Platform);CUSTOM_FILES</DefineConstants>
<DebugType>none</DebugType>
<DebugSymbols>false</DebugSymbols>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='ReleaseSilentMode'">
<OutputType>WinExe</OutputType>
<DebugType>none</DebugType>
<DebugSymbols>false</DebugSymbols>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='ReleaseCustomFilesSilentMode'">
<OutputType>WinExe</OutputType>
<DefineConstants>$(DefineConstants);$(Platform);CUSTOM_FILES</DefineConstants>
<DebugType>none</DebugType>
<DebugSymbols>false</DebugSymbols>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.CommandLine" Version="2.0.0-*" />
</ItemGroup>
</Project>

View File

@@ -1,11 +1,13 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
// Copyright (c) Arctium.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
namespace Arctium.WoW.Launcher.Constants;
namespace Arctium.Game.Launcher.Constants;
enum GameVersion
{
Retail,
Classic,
ClassicEra
ClassicEra,
ClassicTitan
}

View File

@@ -1,19 +1,9 @@
// Copyright (c) Arctium.
// Copyright (c) Arctium.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
namespace Arctium.WoW.Launcher.Constants;
namespace Arctium.Game.Launcher.Constants;
enum MemProtection
{
NoAccess = 0x1,
ReadOnly = 0x2,
ReadWrite = 0x4,
WriteCopy = 0x8,
Execute = 0x10,
ExecuteRead = 0x20,
ExecuteReadWrite = 0x40,
ExecuteWriteCopy = 0x80,
Guard = 0x100,
NoCache = 0x200,
WriteCombine = 0x400,
ReadWrite = 0x4,
}

View File

@@ -1,11 +0,0 @@
// Copyright (c) Arctium.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
namespace Arctium.WoW.Launcher.Constants;
enum MemState
{
Commit = 0x1000,
Reserve = 0x2000,
Free = 0x10000
}

View File

@@ -1,11 +0,0 @@
// Copyright (c) Arctium.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
namespace Arctium.WoW.Launcher.Constants;
enum MemType : uint
{
Private = 0x20000,
Mapped = 0x40000,
Image = 0x1000000
}

View File

@@ -1,346 +0,0 @@
// Copyright (c) Arctium.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
namespace Arctium.WoW.Launcher.Constants;
enum NtStatus : uint
{
// Success
Success = 0x00000000,
Wait0 = 0x00000000,
Wait1 = 0x00000001,
Wait2 = 0x00000002,
Wait3 = 0x00000003,
Wait63 = 0x0000003f,
Abandoned = 0x00000080,
AbandonedWait0 = 0x00000080,
AbandonedWait1 = 0x00000081,
AbandonedWait2 = 0x00000082,
AbandonedWait3 = 0x00000083,
AbandonedWait63 = 0x000000bf,
UserApc = 0x000000c0,
KernelApc = 0x00000100,
Alerted = 0x00000101,
Timeout = 0x00000102,
Pending = 0x00000103,
Reparse = 0x00000104,
MoreEntries = 0x00000105,
NotAllAssigned = 0x00000106,
SomeNotMapped = 0x00000107,
OpLockBreakInProgress = 0x00000108,
VolumeMounted = 0x00000109,
RxActCommitted = 0x0000010a,
NotifyCleanup = 0x0000010b,
NotifyEnumDir = 0x0000010c,
NoQuotasForAccount = 0x0000010d,
PrimaryTransportConnectFailed = 0x0000010e,
PageFaultTransition = 0x00000110,
PageFaultDemandZero = 0x00000111,
PageFaultCopyOnWrite = 0x00000112,
PageFaultGuardPage = 0x00000113,
PageFaultPagingFile = 0x00000114,
CrashDump = 0x00000116,
ReparseObject = 0x00000118,
NothingToTerminate = 0x00000122,
ProcessNotInJob = 0x00000123,
ProcessInJob = 0x00000124,
ProcessCloned = 0x00000129,
FileLockedWithOnlyReaders = 0x0000012a,
FileLockedWithWriters = 0x0000012b,
// Informational
Informational = 0x40000000,
ObjectNameExists = 0x40000000,
ThreadWasSuspended = 0x40000001,
WorkingSetLimitRange = 0x40000002,
ImageNotAtBase = 0x40000003,
RegistryRecovered = 0x40000009,
// Warning
Warning = 0x80000000,
GuardPageViolation = 0x80000001,
DatatypeMisalignment = 0x80000002,
Breakpoint = 0x80000003,
SingleStep = 0x80000004,
BufferOverflow = 0x80000005,
NoMoreFiles = 0x80000006,
HandlesClosed = 0x8000000a,
PartialCopy = 0x8000000d,
DeviceBusy = 0x80000011,
InvalidEaName = 0x80000013,
EaListInconsistent = 0x80000014,
NoMoreEntries = 0x8000001a,
LongJump = 0x80000026,
DllMightBeInsecure = 0x8000002b,
// Error
Error = 0xc0000000,
Unsuccessful = 0xc0000001,
NotImplemented = 0xc0000002,
InvalidInfoClass = 0xc0000003,
InfoLengthMismatch = 0xc0000004,
AccessViolation = 0xc0000005,
InPageError = 0xc0000006,
PagefileQuota = 0xc0000007,
InvalidHandle = 0xc0000008,
BadInitialStack = 0xc0000009,
BadInitialPc = 0xc000000a,
InvalidCid = 0xc000000b,
TimerNotCanceled = 0xc000000c,
InvalidParameter = 0xc000000d,
NoSuchDevice = 0xc000000e,
NoSuchFile = 0xc000000f,
InvalidDeviceRequest = 0xc0000010,
EndOfFile = 0xc0000011,
WrongVolume = 0xc0000012,
NoMediaInDevice = 0xc0000013,
NoMemory = 0xc0000017,
NotMappedView = 0xc0000019,
UnableToFreeVm = 0xc000001a,
UnableToDeleteSection = 0xc000001b,
IllegalInstruction = 0xc000001d,
AlreadyCommitted = 0xc0000021,
AccessDenied = 0xc0000022,
BufferTooSmall = 0xc0000023,
ObjectTypeMismatch = 0xc0000024,
NonContinuableException = 0xc0000025,
BadStack = 0xc0000028,
NotLocked = 0xc000002a,
NotCommitted = 0xc000002d,
InvalidParameterMix = 0xc0000030,
ObjectNameInvalid = 0xc0000033,
ObjectNameNotFound = 0xc0000034,
ObjectNameCollision = 0xc0000035,
ObjectPathInvalid = 0xc0000039,
ObjectPathNotFound = 0xc000003a,
ObjectPathSyntaxBad = 0xc000003b,
DataOverrun = 0xc000003c,
DataLate = 0xc000003d,
DataError = 0xc000003e,
CrcError = 0xc000003f,
SectionTooBig = 0xc0000040,
PortConnectionRefused = 0xc0000041,
InvalidPortHandle = 0xc0000042,
SharingViolation = 0xc0000043,
QuotaExceeded = 0xc0000044,
InvalidPageProtection = 0xc0000045,
MutantNotOwned = 0xc0000046,
SemaphoreLimitExceeded = 0xc0000047,
PortAlreadySet = 0xc0000048,
SectionNotImage = 0xc0000049,
SuspendCountExceeded = 0xc000004a,
ThreadIsTerminating = 0xc000004b,
BadWorkingSetLimit = 0xc000004c,
IncompatibleFileMap = 0xc000004d,
SectionProtection = 0xc000004e,
EasNotSupported = 0xc000004f,
EaTooLarge = 0xc0000050,
NonExistentEaEntry = 0xc0000051,
NoEasOnFile = 0xc0000052,
EaCorruptError = 0xc0000053,
FileLockConflict = 0xc0000054,
LockNotGranted = 0xc0000055,
DeletePending = 0xc0000056,
CtlFileNotSupported = 0xc0000057,
UnknownRevision = 0xc0000058,
RevisionMismatch = 0xc0000059,
InvalidOwner = 0xc000005a,
InvalidPrimaryGroup = 0xc000005b,
NoImpersonationToken = 0xc000005c,
CantDisableMandatory = 0xc000005d,
NoLogonServers = 0xc000005e,
NoSuchLogonSession = 0xc000005f,
NoSuchPrivilege = 0xc0000060,
PrivilegeNotHeld = 0xc0000061,
InvalidAccountName = 0xc0000062,
UserExists = 0xc0000063,
NoSuchUser = 0xc0000064,
GroupExists = 0xc0000065,
NoSuchGroup = 0xc0000066,
MemberInGroup = 0xc0000067,
MemberNotInGroup = 0xc0000068,
LastAdmin = 0xc0000069,
WrongPassword = 0xc000006a,
IllFormedPassword = 0xc000006b,
PasswordRestriction = 0xc000006c,
LogonFailure = 0xc000006d,
AccountRestriction = 0xc000006e,
InvalidLogonHours = 0xc000006f,
InvalidWorkstation = 0xc0000070,
PasswordExpired = 0xc0000071,
AccountDisabled = 0xc0000072,
NoneMapped = 0xc0000073,
TooManyLuidsRequested = 0xc0000074,
LuidsExhausted = 0xc0000075,
InvalidSubAuthority = 0xc0000076,
InvalidAcl = 0xc0000077,
InvalidSid = 0xc0000078,
InvalidSecurityDescr = 0xc0000079,
ProcedureNotFound = 0xc000007a,
InvalidImageFormat = 0xc000007b,
NoToken = 0xc000007c,
BadInheritanceAcl = 0xc000007d,
RangeNotLocked = 0xc000007e,
DiskFull = 0xc000007f,
ServerDisabled = 0xc0000080,
ServerNotDisabled = 0xc0000081,
TooManyGuidsRequested = 0xc0000082,
GuidsExhausted = 0xc0000083,
InvalidIdAuthority = 0xc0000084,
AgentsExhausted = 0xc0000085,
InvalidVolumeLabel = 0xc0000086,
SectionNotExtended = 0xc0000087,
NotMappedData = 0xc0000088,
ResourceDataNotFound = 0xc0000089,
ResourceTypeNotFound = 0xc000008a,
ResourceNameNotFound = 0xc000008b,
ArrayBoundsExceeded = 0xc000008c,
FloatDenormalOperand = 0xc000008d,
FloatDivideByZero = 0xc000008e,
FloatInexactResult = 0xc000008f,
FloatInvalidOperation = 0xc0000090,
FloatOverflow = 0xc0000091,
FloatStackCheck = 0xc0000092,
FloatUnderflow = 0xc0000093,
IntegerDivideByZero = 0xc0000094,
IntegerOverflow = 0xc0000095,
PrivilegedInstruction = 0xc0000096,
TooManyPagingFiles = 0xc0000097,
FileInvalid = 0xc0000098,
InstanceNotAvailable = 0xc00000ab,
PipeNotAvailable = 0xc00000ac,
InvalidPipeState = 0xc00000ad,
PipeBusy = 0xc00000ae,
IllegalFunction = 0xc00000af,
PipeDisconnected = 0xc00000b0,
PipeClosing = 0xc00000b1,
PipeConnected = 0xc00000b2,
PipeListening = 0xc00000b3,
InvalidReadMode = 0xc00000b4,
IoTimeout = 0xc00000b5,
FileForcedClosed = 0xc00000b6,
ProfilingNotStarted = 0xc00000b7,
ProfilingNotStopped = 0xc00000b8,
NotSameDevice = 0xc00000d4,
FileRenamed = 0xc00000d5,
CantWait = 0xc00000d8,
PipeEmpty = 0xc00000d9,
CantTerminateSelf = 0xc00000db,
InternalError = 0xc00000e5,
InvalidParameter1 = 0xc00000ef,
InvalidParameter2 = 0xc00000f0,
InvalidParameter3 = 0xc00000f1,
InvalidParameter4 = 0xc00000f2,
InvalidParameter5 = 0xc00000f3,
InvalidParameter6 = 0xc00000f4,
InvalidParameter7 = 0xc00000f5,
InvalidParameter8 = 0xc00000f6,
InvalidParameter9 = 0xc00000f7,
InvalidParameter10 = 0xc00000f8,
InvalidParameter11 = 0xc00000f9,
InvalidParameter12 = 0xc00000fa,
MappedFileSizeZero = 0xc000011e,
TooManyOpenedFiles = 0xc000011f,
Cancelled = 0xc0000120,
CannotDelete = 0xc0000121,
InvalidComputerName = 0xc0000122,
FileDeleted = 0xc0000123,
SpecialAccount = 0xc0000124,
SpecialGroup = 0xc0000125,
SpecialUser = 0xc0000126,
MembersPrimaryGroup = 0xc0000127,
FileClosed = 0xc0000128,
TooManyThreads = 0xc0000129,
ThreadNotInProcess = 0xc000012a,
TokenAlreadyInUse = 0xc000012b,
PagefileQuotaExceeded = 0xc000012c,
CommitmentLimit = 0xc000012d,
InvalidImageLeFormat = 0xc000012e,
InvalidImageNotMz = 0xc000012f,
InvalidImageProtect = 0xc0000130,
InvalidImageWin16 = 0xc0000131,
LogonServer = 0xc0000132,
DifferenceAtDc = 0xc0000133,
SynchronizationRequired = 0xc0000134,
DllNotFound = 0xc0000135,
IoPrivilegeFailed = 0xc0000137,
OrdinalNotFound = 0xc0000138,
EntryPointNotFound = 0xc0000139,
ControlCExit = 0xc000013a,
PortNotSet = 0xc0000353,
DebuggerInactive = 0xc0000354,
CallbackBypass = 0xc0000503,
PortClosed = 0xc0000700,
MessageLost = 0xc0000701,
InvalidMessage = 0xc0000702,
RequestCanceled = 0xc0000703,
RecursiveDispatch = 0xc0000704,
LpcReceiveBufferExpected = 0xc0000705,
LpcInvalidConnectionUsage = 0xc0000706,
LpcRequestsNotAllowed = 0xc0000707,
ResourceInUse = 0xc0000708,
ProcessIsProtected = 0xc0000712,
VolumeDirty = 0xc0000806,
FileCheckedOut = 0xc0000901,
CheckOutRequired = 0xc0000902,
BadFileType = 0xc0000903,
FileTooLarge = 0xc0000904,
FormsAuthRequired = 0xc0000905,
VirusInfected = 0xc0000906,
VirusDeleted = 0xc0000907,
TransactionalConflict = 0xc0190001,
InvalidTransaction = 0xc0190002,
TransactionNotActive = 0xc0190003,
TmInitializationFailed = 0xc0190004,
RmNotActive = 0xc0190005,
RmMetadataCorrupt = 0xc0190006,
TransactionNotJoined = 0xc0190007,
DirectoryNotRm = 0xc0190008,
CouldNotResizeLog = 0xc0190009,
TransactionsUnsupportedRemote = 0xc019000a,
LogResizeInvalidSize = 0xc019000b,
RemoteFileVersionMismatch = 0xc019000c,
CrmProtocolAlreadyExists = 0xc019000f,
TransactionPropagationFailed = 0xc0190010,
CrmProtocolNotFound = 0xc0190011,
TransactionSuperiorExists = 0xc0190012,
TransactionRequestNotValid = 0xc0190013,
TransactionNotRequested = 0xc0190014,
TransactionAlreadyAborted = 0xc0190015,
TransactionAlreadyCommitted = 0xc0190016,
TransactionInvalidMarshallBuffer = 0xc0190017,
CurrentTransactionNotValid = 0xc0190018,
LogGrowthFailed = 0xc0190019,
ObjectNoLongerExists = 0xc0190021,
StreamMiniversionNotFound = 0xc0190022,
StreamMiniversionNotValid = 0xc0190023,
MiniversionInaccessibleFromSpecifiedTransaction = 0xc0190024,
CantOpenMiniversionWithModifyIntent = 0xc0190025,
CantCreateMoreStreamMiniversions = 0xc0190026,
HandleNoLongerValid = 0xc0190028,
NoTxfMetadata = 0xc0190029,
LogCorruptionDetected = 0xc0190030,
CantRecoverWithHandleOpen = 0xc0190031,
RmDisconnected = 0xc0190032,
EnlistmentNotSuperior = 0xc0190033,
RecoveryNotNeeded = 0xc0190034,
RmAlreadyStarted = 0xc0190035,
FileIdentityNotPersistent = 0xc0190036,
CantBreakTransactionalDependency = 0xc0190037,
CantCrossRmBoundary = 0xc0190038,
TxfDirNotEmpty = 0xc0190039,
IndoubtTransactionsExist = 0xc019003a,
TmVolatile = 0xc019003b,
RollbackTimerExpired = 0xc019003c,
TxfAttributeCorrupt = 0xc019003d,
EfsNotAllowedInTransaction = 0xc019003e,
TransactionalOpenNotAllowed = 0xc019003f,
TransactedMappingUnsupportedRemote = 0xc0190040,
TxfMetadataAlreadyPresent = 0xc0190041,
TransactionScopeCallbacksNotSet = 0xc0190042,
TransactionRequiredPromotion = 0xc0190043,
CannotExecuteFileInTransaction = 0xc0190044,
TransactionsNotFrozen = 0xc0190045,
MaximumNtStatus = 0xffffffff
}

View File

@@ -1,23 +1,19 @@
// Copyright (c) Arctium.
// Copyright (c) Arctium.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
using static Arctium.WoW.Launcher.Misc.NativeWindows;
using static Arctium.WoW.Launcher.Misc.Helpers;
using static Arctium.Game.Launcher.Misc.NativeWindows;
using static Arctium.Game.Launcher.Misc.Helpers;
namespace Arctium.WoW.Launcher.IO;
namespace Arctium.Game.Launcher.IO;
class WinMemory
{
public byte[] Data { get; set; }
public nint BaseAddress { get; }
ProcessBasicInformation _peb;
readonly nint _processHandle;
readonly Dictionary<string, (long Address, byte[] Data)> _patchList;
public WinMemory(ProcessInformation processInformation, long binaryLength)
public WinMemory(ProcessInformation processInformation)
{
_processHandle = processInformation.ProcessHandle;
@@ -28,22 +24,18 @@ class WinMemory
if (BaseAddress == 0)
throw new InvalidOperationException("Error while reading PEB data.");
Data = Read(BaseAddress, (int)binaryLength);
_patchList = new Dictionary<string, (long Address, byte[] Data)>();
}
public void RefreshMemoryData(int size)
{
// Reset previous memory data.
Data = null;
if (Data != null)
Array.Clear(Data, 0, Data.Length);
while (Data == null)
while (Data == null || Unsafe.ReadUnaligned<long>(ref Data[0]) == 0)
{
Console.WriteLine("Refreshing client data...");
Data = Read(BaseAddress, size);
ReadToData(BaseAddress, size);
}
}
@@ -65,7 +57,20 @@ class WinMemory
return 0;
}
public nint Read(long address) => Read((nint)address);
public void ReadToData(nint address, int size)
{
try
{
Data ??= new byte[size];
ReadProcessMemory(_processHandle, address, Data, size, out var dummy);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
public byte[] Read(nint address, int size)
{
@@ -85,29 +90,6 @@ class WinMemory
return null;
}
public byte[] Read(long address, int size) => Read((nint)address, size);
public int ReadDataLength(nint address, string separator)
{
var length = 0L;
var seperatorBytes = Encoding.UTF8.GetBytes(separator).Select(b => (short)b).ToArray();
var dataLength = 1000;
// Read in batches here.
while (length == 0)
{
length = Read(address, dataLength)?.FindPattern(seperatorBytes) ?? 0;
dataLength += 1000;
// Not found!
if (dataLength >= 100_000)
return -1;
}
return (int)length;
}
public void Write(nint address, byte[] data, MemProtection newProtection = MemProtection.ReadWrite)
{
try
@@ -126,9 +108,24 @@ class WinMemory
}
}
public void Write(long address, byte[] data, MemProtection newProtection = MemProtection.ReadWrite) => Write((nint)address, data, newProtection);
public async Task TryPatchPatterns(byte[][] patches, string patchName, bool? printInfo = null, bool exitOnFail = true, bool patchAll = false, params short[][] patterns)
{
if (patches.Length == 0 || patterns.Length == 0)
return;
public Task PatchMemory(short[] pattern, byte[] patch, string patchName, bool? printInfo = null)
if (!patchAll && await PatchMemory(patterns[0], patches[0], $"{patchName}", printInfo, exitOnFail))
return;
for (var i = 0; i < patterns.Length; i++)
{
Console.ResetColor();
if (await PatchMemory(patterns[i], patches[i], $"{patchName} {i + 1}", printInfo, exitOnFail: false) && !patchAll)
break;
}
}
public Task<bool> PatchMemory(short[] pattern, byte[] patch, string patchName, bool? printInfo = null, bool exitOnFail = true)
{
printInfo ??= IsDebugBuild();
@@ -140,198 +137,54 @@ class WinMemory
// No result for the given pattern.
if (patchOffset == 0)
{
Console.ForegroundColor = ConsoleColor.Yellow;
if (exitOnFail)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine($"[{patchName}] No result found.");
Console.WriteLine("Press any key to exit...");
Console.WriteLine($"[{patchName}] No result found.");
Console.WriteLine("Press any key to exit...");
Console.ReadKey();
// Only wait if a console is available.
if (!Console.IsInputRedirected)
Console.ReadKey();
Launcher.CancellationTokenSource.Cancel();
Launcher.CancellationTokenSource.Cancel();
return Task.FromResult(false);
}
if (printInfo.Value)
{
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine($"[{patchName}] No result found. This is just a warning! KEEP GOING...");
Console.ResetColor();
}
return Task.FromResult(false);
}
while (Read(patchOffset, patch.Length)?.SequenceEqual(patch) == false)
Write(patchOffset, patch);
while (Read((nint)patchOffset, patch.Length)?.SequenceEqual(patch) == false)
Write((nint)patchOffset, patch);
if (printInfo.Value)
{
Console.Write($"[{patchName}]");
Console.Write($"[{patchName}] at 0x{patchOffset:X}");
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine(" Done.");
Console.ResetColor();
Console.WriteLine();
}
return Task.CompletedTask;
return Task.FromResult(true);
}
public Task QueuePatch(long patchOffset, byte[] patch, string patchName, bool? printInfo = null)
{
Launcher.CancellationTokenSource.Token.ThrowIfCancellationRequested();
printInfo ??= IsDebugBuild();
if (printInfo.Value)
{
Console.WriteLine($"[{patchName}] Adding...");
_patchList[patchName] = (patchOffset, patch);
Console.Write($"[{patchName}]");
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine(" Done.");
Console.ResetColor();
Console.WriteLine();
}
else
_patchList[patchName] = (patchOffset, patch);
return Task.CompletedTask;
}
public Task QueuePatch(short[] pattern, byte[] patch, string patchName, int offsetBase = 0, bool? printInfo = null)
{
long patchOffset = Data.FindPattern(pattern);
// No result for the given pattern.
if (patchOffset == 0)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine($"[{patchName}] No result found.");
Console.ResetColor();
Console.WriteLine("Press any key to exit...");
Console.ReadKey();
Launcher.CancellationTokenSource.Cancel();
return Task.CompletedTask;
}
return QueuePatch(patchOffset + offsetBase, patch, patchName, printInfo);
}
bool RemapAndPatch(nint viewAddress, int viewSize)
{
// Suspend before remapping to prevent crashes.
NtSuspendProcess(_processHandle);
Data = Read(viewAddress, viewSize);
if (Data != null)
{
nint newViewHandle = 0;
var maxSize = new LargeInteger { Quad = viewSize };
try
{
if (NtCreateSection(ref newViewHandle, 0xF001F, 0, ref maxSize, 0x40u, 0x8000000 | 0x400000, 0) == NtStatus.Success &&
NtUnmapViewOfSection(_processHandle, viewAddress) == NtStatus.Success)
{
var viewBase = viewAddress;
// Map the view with original protections.
var result = NtMapViewOfSection(newViewHandle, _processHandle, ref viewBase, 0, (ulong)viewSize, out var viewOffset,
out var newViewSize, 2, 0, (int)MemProtection.ExecuteRead);
if (result == NtStatus.Success)
{
// Apply our patches.
ApplyPatches(true);
nint viewBase2 = 0;
// Create a writable view to write our patches through to preserve the original protections.
result = NtMapViewOfSection(newViewHandle, _processHandle, ref viewBase2, 0, (uint)viewSize, out var viewOffset2,
out var newViewSize2, 2, 0, (int)MemProtection.ReadWrite);
if (result == NtStatus.Success)
{
// Write our patched data trough the writable view to the memory.
if (WriteProcessMemory(_processHandle, viewBase2, Data, viewSize, out var dummy))
{
// Unmap them writeable view, it's not longer needed.
NtUnmapViewOfSection(_processHandle, viewBase2);
// Check if the allocation protections is the right one.
if (VirtualQueryEx(_processHandle, BaseAddress, out MemoryBasicInformation mbi, MemoryBasicInformation.Size) != 0
&& mbi.AllocationProtect == MemProtection.ExecuteRead)
{
// Also check if we can change the page protection.
if (!VirtualProtectEx(_processHandle, BaseAddress, 0x4000, (uint)MemProtection.ReadWrite, out var oldProtect))
NtResumeProcess(_processHandle);
return true;
}
}
}
}
Console.WriteLine("Error while mapping the view with the given protection.");
}
}
finally
{
NtClose(newViewHandle);
}
}
else
Console.WriteLine("Error while creating the view backup.");
NtResumeProcess(_processHandle);
return false;
}
void ApplyPatches(bool remap)
{
foreach (var p in _patchList)
{
var address = p.Value.Address;
if (address == 0)
continue;
var patch = p.Value.Data;
// We are in a different section here.
if (address > Data.Length)
{
if (address < BaseAddress)
address += BaseAddress;
Write(address, patch);
continue;
}
if (remap)
{
for (var i = 0; i < patch.Length; i++)
Data[address + i] = patch[i];
}
}
}
public bool RemapAndPatch(bool remap)
{
if (!remap)
{
ApplyPatches(false);
return true;
}
if (VirtualQueryEx(_processHandle, BaseAddress, out var mbi, MemoryBasicInformation.Size) != 0)
return RemapAndPatch(mbi.BaseAddress, (int)mbi.RegionSize);
return false;
}
/// Private functions.
nint ReadImageBaseFromPEB(nint processHandle)
{
try
{
if (NtQueryInformationProcess(processHandle, 0, ref _peb, ProcessBasicInformation.Size, out _) == NtStatus.Success)
return Read(_peb.PebBaseAddress + 0x10);
ProcessBasicInformation peb = default;
if (NtQueryInformationProcess(processHandle, 0, ref peb, ProcessBasicInformation.Size, out _) == 0)
return Read(peb.PebBaseAddress + 0x10);
}
catch (Exception ex)
{
@@ -340,22 +193,4 @@ class WinMemory
return 0;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsShortJump(byte[] instructions, int startIndex = 0)
{
return instructions[startIndex] >= 0x70 && instructions[startIndex] < 0x7F;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsJump(byte[] instructions, int startIndex = 0)
{
return instructions[startIndex] == 0x0F && instructions[startIndex + 1] >= 0x80 && instructions[startIndex + 1] <= 0x8F;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsUnconditionalJump(byte[] instructions, int startIndex = 0)
{
return instructions[startIndex] == 0xE9 || instructions[startIndex] == 0xEB;
}
}

View File

@@ -1,24 +1,22 @@
// Copyright (c) Arctium.
// Copyright (c) Arctium.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
using System.CommandLine.Builder;
using System.CommandLine.Parsing;
namespace Arctium.WoW.Launcher;
namespace Arctium.Game.Launcher;
static class LaunchOptions
{
public static bool IsDevModeAllowed { get; set; }
public static Option<GameVersion> Version = new("--version", () => GameVersion.Retail);
public static Option<string> GamePath = new("--path");
public static Option<string> GameBinary = new("--binary");
public static Option<bool> KeepCache = new("--keepcache", () => true);
public static Option<bool> UseStaticAuthSeed = new("--staticseed");
public static Option<bool> DevMode = new("--dev", () => true, "Required for local development without valid certificates.");
public static Option<string> VersionUrl = new("--versionurl");
public static Option<string> CdnsUrl = new("--cdnsurl");
public static Option<string> ProductName = new("--product", () => "wow");
public static Option<string> CdnRegion = new("--region", () => "EU");
public static Option<bool> SkipConnectionPatching = new("--skip", () => false, "Allows connection to servers that come with already patched clients.");
public static Option<string> BgsPortal = new("--portal");
// Game command line options.
public static Option<string> GameConfig = new("-config", () => "Config.wtf");
@@ -31,18 +29,18 @@ static class LaunchOptions
.UseSuggestDirective()
.Build();
public static RootCommand RootCommand = new("Arctium WoW Launcher")
public static RootCommand RootCommand = new("Arctium Game Launcher")
{
Version,
GamePath,
GameBinary,
KeepCache,
UseStaticAuthSeed,
DevMode,
VersionUrl,
CdnsUrl,
ProductName,
CdnRegion,
BgsPortal,
GameConfig,
SkipConnectionPatching
};
static Command ConfigureCommandLine(Command rootCommand)

View File

@@ -1,34 +1,46 @@
// Copyright (c) Arctium.
// Copyright (c) Arctium.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
using System.CommandLine.Parsing;
using System.Net.Security;
using System.Net.Sockets;
using System.Security.Authentication;
using static Arctium.Game.Launcher.Misc.Helpers;
using static Arctium.WoW.Launcher.Misc.Helpers;
namespace Arctium.WoW.Launcher;
namespace Arctium.Game.Launcher;
static class Launcher
{
public static nint GameProcessHandle { get; private set; }
public static readonly CancellationTokenSource CancellationTokenSource = new();
public static async ValueTask<string> PrepareGameLaunch(ParseResult commandLineResult, IPFilter ipFilter)
static List<GameVersion> _tryClients = [GameVersion.Retail, GameVersion.Classic, GameVersion.ClassicEra, GameVersion.ClassicTitan];
static bool _useVersionV2;
static byte[] _binaryData;
public static async ValueTask<string> PrepareGameLaunch(ParseResult commandLineResult, GameVersion? branchOverwrite)
{
Console.ResetColor();
var gameVersion = commandLineResult.GetValueForOption(LaunchOptions.Version);
if (branchOverwrite.HasValue)
gameVersion = branchOverwrite.Value;
var (subFolder, binaryName, majorGameVersion, minGameBuild) = gameVersion switch
{
GameVersion.Retail => ("_retail_", "Wow.exe", new[] { 9, 10 }, 37862),
GameVersion.Classic => ("_classic_", "WowClassic.exe", new[] { 2, 3 }, 39926),
GameVersion.ClassicEra => ("_classic_era_", "WowClassic.exe", new[] { 1 }, 40347),
GameVersion.Retail => ("_retail_", "Wow.exe", [10, 11, 12], 50401),
GameVersion.Classic => ("_classic_", "WowClassic.exe", [2, 3, 4, 5], 50063),
GameVersion.ClassicEra => ("_classic_era_", "WowClassic.exe", [1], 51001),
GameVersion.ClassicTitan => ("_classic_titan_", "WowClassic.exe", new[] { 3 }, 64393),
_ => throw new NotImplementedException("Invalid game version specified."),
};
_tryClients.RemoveAll(c => c == gameVersion);
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine($"Mode: Custom Server ({gameVersion})");
Console.WriteLine();
Console.WriteLine($"Mode: Public Custom Server ({gameVersion})");
Console.ResetColor();
var currentFolder = AppDomain.CurrentDomain.BaseDirectory;
@@ -51,10 +63,29 @@ static class Launcher
gameBinaryPath = $"{gameFolder}/{binaryName}";
}
gameFolder = gameFolder.Replace("\\/", "/").Replace("\\", "/");
gameBinaryPath = gameBinaryPath.Replace("\\/", "/").Replace("\\", "/");
if (!File.Exists(gameBinaryPath) || !majorGameVersion.Contains(GetVersionValueFromClient(gameBinaryPath).Major))
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine($"[Error] No {gameVersion} client found.");
Console.WriteLine($"No {gameVersion} client found at '{gameBinaryPath}'");
if (_tryClients.Count > 0)
{
var nextClient = _tryClients.First();
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine($"Trying a different branch...");
Console.ResetColor();
return await PrepareGameLaunch(commandLineResult, nextClient);
}
else
{
Console.WriteLine();
Console.WriteLine($"No supported client found.");
}
return string.Empty;
}
@@ -85,77 +116,113 @@ static class Launcher
}
var configPath = $"{gameFolder}/WTF/{commandLineResult.GetValueForOption(LaunchOptions.GameConfig)}";
(string IPAddress, string HostName, int Port) portal = new();
if (!File.Exists(configPath))
LaunchOptions.IsDevModeAllowed = false;
else
{
var config = File.ReadAllText(configPath);
portal = ParsePortal(config);
LaunchOptions.IsDevModeAllowed = IsDevModeAllowed(ipFilter, portal.IPAddress);
}
if (!LaunchOptions.IsDevModeAllowed)
LaunchOptions.DevMode = new("--dev", () => false);
var devModeEnabled = commandLineResult.GetValueForOption(LaunchOptions.DevMode) && LaunchOptions.IsDevModeAllowed;
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine($"Developer mode: {(devModeEnabled ? "Enabled" : "Disabled")}");
Console.WriteLine();
Console.WriteLine($"Client Portal '{portal.HostName}'");
Console.WriteLine($"Client Config: \"{configPath}\"");
Console.ResetColor();
(string IPAddress, string HostName, int Port) portal = new();
var config = File.ReadAllText(configPath);
var bgsPortal = commandLineResult.GetValueForOption(LaunchOptions.BgsPortal);
portal = ParseOrSetPortal(ref config, bgsPortal);
if (!string.IsNullOrEmpty(bgsPortal))
{
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine($"Config Portal Overwrite:\"{bgsPortal}\"");
Console.ResetColor();
Console.WriteLine();
File.WriteAllText(configPath, config);
}
_binaryData = File.ReadAllBytes(gameBinaryPath);
_useVersionV2 = UsesVersionV2(_binaryData);
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine($"Client Path: '{gameBinaryPath}'");
Console.WriteLine($"Client Portal: '{portal.HostName}:{portal.Port}'");
Console.ForegroundColor = ConsoleColor.Gray;
// Check for valid certificate when dev mode is disabled.
if (!devModeEnabled)
if (string.IsNullOrEmpty(portal.HostName) || string.IsNullOrWhiteSpace(portal.HostName))
{
try
{
using var tcpClient = new TcpClient();
using var tcpClientTimeout = new CancellationTokenSource(TimeSpan.FromSeconds(5));
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine();
Console.WriteLine("Client Portal should not be empty.");
Console.WriteLine("Be sure to have a valid portal in your Config.wtf file.");
Console.ResetColor();
await tcpClient.ConnectAsync(portal.HostName, portal.Port, tcpClientTimeout.Token);
return string.Empty;
}
using var sslStream = new SslStream(tcpClient.GetStream(), false,
(_, _, _, sslPolicyErrors) =>
{
// Redirect to the trusted cert warning.
if (sslPolicyErrors != SslPolicyErrors.None)
throw new AuthenticationException();
// Return if no valid ip address has been found.
if (string.IsNullOrEmpty(portal.IPAddress) || string.IsNullOrWhiteSpace(portal.IPAddress))
return string.Empty;
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine($"Certificate for server '{portal.HostName}' successfully validated.");
Console.WriteLine();
Console.ResetColor();
// Check for valid certificate.
try
{
using var tcpClient = new TcpClient();
return true;
},
null
);
// 3.5 seconds timeout.
const int timeout = 3500;
sslStream.AuthenticateAsClient(portal.HostName);
}
catch (Exception exception) when (exception is SocketException or OperationCanceledException)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine($"{portal.HostName}:{portal.Port} is offline.");
Console.ResetColor();
tcpClient.ReceiveTimeout = timeout;
tcpClient.SendTimeout = timeout;
return string.Empty;
}
catch (AuthenticationException)
{
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine($"Server with host name {portal.HostName} does not have a trusted certificate attached.");
Console.WriteLine("If you are the server owner be sure to generate one and replace the default bnet server certificate.");
Console.WriteLine("One way to generate one is through Let's Encrypt.");
Console.ResetColor();
using var tcpClientTimeout = new CancellationTokenSource(TimeSpan.FromMilliseconds(timeout));
return string.Empty;
}
await tcpClient.ConnectAsync(portal.HostName, portal.Port, tcpClientTimeout.Token);
using var sslStream = new SslStream(tcpClient.GetStream(), false,
(_, _, _, sslPolicyErrors) =>
{
// Redirect to the trusted cert warning.
if (sslPolicyErrors != SslPolicyErrors.None)
throw new AuthenticationException();
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine($"Certificate for server '{portal.HostName}' successfully validated.");
Console.WriteLine();
Console.ResetColor();
return true;
},
null
);
sslStream.AuthenticateAsClient(portal.HostName);
}
catch (IOException exception)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine($"Server {portal.HostName}:{portal.Port}: {exception.Message}");
Console.ResetColor();
return string.Empty;
}
catch (AuthenticationException)
{
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine($"Server with host name {portal.HostName}:{portal.Port} does not have a trusted certificate attached.");
Console.WriteLine("If you are the server owner be sure to generate one and replace the default bnet server certificate.");
Console.WriteLine("One way to generate one is through Let's Encrypt.");
Console.ResetColor();
return string.Empty;
}
catch (Exception exception) when (exception is SocketException or OperationCanceledException)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine($"{portal.HostName}:{portal.Port} is offline or not reachable from the current network.");
Console.ResetColor();
return string.Empty;
}
return gameBinaryPath;
@@ -168,7 +235,61 @@ static class Launcher
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine($"Client Build {clientVersion}");
Console.WriteLine($"Client Path '{appPath}'");
Console.ResetColor();
// Assign the region and product dependent version url to check it's online status.
string[] versionUrls = CreateVersionUrls(commandLineResult, clientVersion);
static string[] CreateVersionUrls(ParseResult commandLineResult, (int Major, int Minor, int Revision, int Build) clientVersion)
{
var versionUrl = commandLineResult.GetValueForOption(LaunchOptions.VersionUrl);
// Always return a forced version url parameter.
if (!string.IsNullOrEmpty(versionUrl))
return [versionUrl, versionUrl, versionUrl];
return [];
}
bool hasCustomVersionUrl = commandLineResult.HasOption(LaunchOptions.VersionUrl);
bool hasCustomCdnUrl = commandLineResult.HasOption(LaunchOptions.CdnsUrl);
if (hasCustomCdnUrl || hasCustomVersionUrl)
{
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine("Warning: Custom version servers were specified.");
Console.WriteLine("The game will connect to these servers.");
Console.ResetColor();
}
var cdnsUrl = commandLineResult.GetValueForOption(LaunchOptions.CdnsUrl);
if (hasCustomCdnUrl && (!CheckUrl(cdnsUrl, fallbackUrls: [Patterns.Common.CdnsUrl]).GetAwaiter().GetResult()))
cdnsUrl = Patterns.Common.CdnsUrl;
Console.ForegroundColor = ConsoleColor.Cyan;
Console.WriteLine();
Console.WriteLine("Game CDN connection info:");
Console.WriteLine($"Version source(s):");
if (versionUrls.Length == 0)
{
foreach (var vUrl in new[] { Patterns.Common.VersionUrl, Patterns.Common.Version2Url, Patterns.Common.Version2ChinaUrl })
Console.WriteLine($"- {vUrl}");
}
else
{
if (versionUrls.All(element => element.Equals(versionUrls[0])))
Console.WriteLine($"- {versionUrls[0]}");
else
{
foreach (var vUrl in versionUrls)
Console.WriteLine($"- {vUrl}");
}
}
Console.WriteLine($"CDNs source: {cdnsUrl}");
Console.WriteLine();
Console.ResetColor();
@@ -178,158 +299,80 @@ static class Launcher
try
{
Console.ForegroundColor = ConsoleColor.White;
Console.WriteLine("Starting WoW client...");
Console.WriteLine("Starting game client...");
var createSuccess = NativeWindows.CreateProcess(null, $"{appPath} {gameCommandLine}", 0, 0, false, 4, 0, new FileInfo(appPath).DirectoryName,
var createSuccess = NativeWindows.CreateProcess(null, $"\"{appPath}\" {gameCommandLine}", 0, 0, false, 4, 0, new FileInfo(appPath).DirectoryName,
ref startupInfo, out processInfo);
// On some systems we have to launch the game with the application name used.
if (!createSuccess)
createSuccess = NativeWindows.CreateProcess(appPath, $" {gameCommandLine}", 0, 0, false, 4, 0, null, ref startupInfo, out processInfo);
createSuccess = NativeWindows.CreateProcess(appPath, $" {gameCommandLine}", 0, 0, false, 4, 0, new FileInfo(appPath).DirectoryName, ref startupInfo, out processInfo);
// Start process with suspend flags.
if (createSuccess)
{
using var gameAppData = new MemoryStream(File.ReadAllBytes(appPath));
GameProcessHandle = processInfo.ProcessHandle;
var memory = new WinMemory(processInfo, gameAppData.Length);
using var gameAppData = File.OpenRead(appPath);
// Resume the process to initialize it.
NativeWindows.NtResumeProcess(processInfo.ProcessHandle);
var appDataLength = gameAppData.Length;
var memory = new WinMemory(processInfo);
MemoryBasicInformation mbi;
// Wait for the memory region to be initialized.
while (NativeWindows.VirtualQueryEx(processInfo.ProcessHandle, memory.BaseAddress, out mbi, MemoryBasicInformation.Size) == 0 ||
mbi.RegionSize <= 0x1000)
{ }
if (mbi.BaseAddress != 0)
if (memory.BaseAddress != 0)
{
NativeWindows.NtSuspendProcess(processInfo.ProcessHandle);
byte[] certBundleData = Convert.FromBase64String(Patches.Common.CertBundleData);
Process gameProcess = Process.GetProcessById((int)processInfo.ProcessId);
// Refresh the client data before patching.
memory.RefreshMemoryData((int)gameAppData.Length);
memory.RefreshMemoryData((int)appDataLength);
// We need to cache this here since we are using our RSA modulus as auth seed.
var modulusOffset = memory.Data.FindPattern(Patterns.Common.CryptoRsaModulus);
var legacyCertMode = clientVersion is (1, >= 14, <= 3, _) or (3, 4, <= 1, _) or (9, _, _, _) or (10, <= 1, < 5, _);
if (!commandLineResult.GetValueForOption(LaunchOptions.SkipConnectionPatching))
// Custom CDN related patches.
if ((hasCustomVersionUrl && hasCustomCdnUrl))
{
if (legacyCertMode)
var versionUrlPatches = new byte[versionUrls.Length][];
for (var i = 0; i < versionUrls.Length; i++)
versionUrlPatches[i] = Encoding.UTF8.GetBytes(versionUrls[i] + '\0');
// 11.1.7 added new v2 links.
if (_useVersionV2)
{
Task.WaitAll(new[]
{
memory.PatchMemory(Patterns.Common.CertBundle, certBundleData, "Certificate Bundle"),
memory.PatchMemory(Patterns.Common.SignatureModulus, Patches.Common.SignatureModulus, "Certificate Signature RsaModulus")
}, CancellationTokenSource.Token);
Task.WaitAll(
[
memory.TryPatchPatterns(versionUrlPatches, "Version URL", true, false, true,
Patterns.Common.Version2UrlNew.ToPattern(), Patterns.Common.Version2ChinaUrlNew.ToPattern()),
], CancellationTokenSource.Token);
}
// Wait for all direct memory patch tasks to complete,
Task.WaitAll(new[]
else
{
memory.PatchMemory(Patterns.Common.ConnectToModulus, Patches.Common.RsaModulus, "ConnectTo RsaModulus"),
// Recent clients have a different signing algorithm in EnterEncryptedMode.
clientVersion is (9, 2, 7, _) or (3, _, _, _) or (10, _, _, _) or (1, >= 14, >= 4, _)
? memory.PatchMemory(Patterns.Common.CryptoEdPublicKey, Patches.Common.CryptoEdPublicKey, "GameCrypto Ed25519 PublicKey")
: memory.PatchMemory(Patterns.Common.CryptoRsaModulus, Patches.Common.RsaModulus, "GameCrypto RsaModulus"),
memory.PatchMemory(Patterns.Common.Portal, Patches.Common.Portal, "Login Portal"),
memory.PatchMemory(Patterns.Windows.LauncherLogin, Patches.Windows.LauncherLogin, "Launcher Login Registry")
}, CancellationTokenSource.Token);
Task.WaitAll(
[
memory.TryPatchPatterns(versionUrlPatches, "Version URL", true, false, true,
Patterns.Common.VersionUrl.ToPattern(), Patterns.Common.Version2Url.ToPattern(), Patterns.Common.Version2ChinaUrl.ToPattern()),
memory.PatchMemory(Patterns.Common.CdnsUrl.ToPattern(), Encoding.UTF8.GetBytes(cdnsUrl), "CDNs URL", exitOnFail: false),
], CancellationTokenSource.Token);
}
}
// Wait for all direct memory patch tasks to complete.
Task.WaitAll(
[
memory.PatchMemory(Patterns.Common.ConnectToModulus, Patches.Common.RsaModulus, "ConnectTo RsaModulus"),
memory.PatchMemory(Patterns.Common.CryptoEdPublicKey, Patches.Common.CryptoEdPublicKey, "GameCrypto Ed25519 PublicKey"),
memory.PatchMemory(Patterns.Common.Portal, Patches.Common.Portal, "Login Portal"),
memory.PatchMemory(Patterns.Windows.LauncherLogin, Patches.Windows.LauncherLogin, "Launcher Login Registry")
], CancellationTokenSource.Token);
NativeWindows.NtResumeProcess(processInfo.ProcessHandle);
// Enable anti crash in dev mode, custom file mode or static auth seed mode.
#if CUSTOM_FILES
var antiCrash = true;
#else
var antiCrash = legacyCertMode || commandLineResult.HasOption(LaunchOptions.UseStaticAuthSeed) ||
commandLineResult.GetValueForOption(LaunchOptions.DevMode) && LaunchOptions.IsDevModeAllowed;
#endif
Console.WriteLine("Done :) ");
WaitForUnpack(ref processInfo, memory, ref mbi, gameAppData, antiCrash);
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine("You can login now.");
Console.WriteLine("Closing in 3 seconds...");
Thread.Sleep(3000);
Console.ResetColor();
if (!commandLineResult.GetValueForOption(LaunchOptions.SkipConnectionPatching))
{
if (legacyCertMode)
{
Task.WaitAll(new[]
{
memory.QueuePatch(Patterns.Windows.CertBundle, Patches.Windows.CertBundle, "CertBundle"),
memory.QueuePatch(Patterns.Windows.CertCommonName, Patches.Windows.CertCommonName, "CertCommonName", 5)
}, CancellationTokenSource.Token);
}
else if (LaunchOptions.IsDevModeAllowed && commandLineResult.GetValueForOption(LaunchOptions.DevMode))
{
Task.WaitAll(new[]
{
memory.QueuePatch(Patterns.Windows.CertChain, Patches.Windows.CertChain, "CertChain"),
memory.QueuePatch(Patterns.Windows.CertCommonName, Patches.Windows.CertCommonName, "CertCommonName", 5)
}, CancellationTokenSource.Token);
}
}
if (commandLineResult.HasOption(LaunchOptions.UseStaticAuthSeed))
{
Console.ForegroundColor = ConsoleColor.Cyan;
Console.WriteLine("Static auth seed used. Be sure that the server you are connecting to supports it.");
Console.ResetColor();
// Generates a patch for the auth seed so we don't have to update them on each build.
var authSeedFunctionOffset = GenerateAuthSeedFunctionPatch(memory, modulusOffset);
Task.WaitAll(new[]
{
memory.QueuePatch(authSeedFunctionOffset, Patches.Windows.AuthSeed, "CustomAuthSeedFunction")
}, CancellationTokenSource.Token);
}
#if CUSTOM_FILES
Task.WaitAll(new[]
{
(clientVersion is (10, _, _, _))
? memory.QueuePatch(Patterns.Windows.LoadByFileIdAlternate, Patches.Windows.NoJump, "LoadByFileId", 3)
: memory.QueuePatch(Patterns.Windows.LoadByFileId, Patches.Windows.NoJump, "LoadByFileId", 6),
(clientVersion is (10, _, _, _))
? memory.QueuePatch(Patterns.Windows.LoadByFilePathAlternate, Patches.Windows.NoJump, "LoadByFilePath", 3)
: memory.QueuePatch(Patterns.Windows.LoadByFilePath, Patches.Windows.NoJump, "LoadByFilePath", 3)
}, CancellationTokenSource.Token);
var (idAlloc, stringAlloc) = ModLoader.LoadFileMappings(processInfo.ProcessHandle);
if (idAlloc != 0 && stringAlloc != 0)
{
if (!ModLoader.HookClient(memory, processInfo.ProcessHandle, idAlloc, stringAlloc))
return false;
}
#endif
NativeWindows.NtResumeProcess(processInfo.ProcessHandle);
if (memory.RemapAndPatch(antiCrash))
{
Console.WriteLine("Done :) ");
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine("You can login now.");
Console.ResetColor();
return true;
}
else
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine("Error while launching the client.");
NativeWindows.TerminateProcess(processInfo.ProcessHandle, 0);
}
return true;
}
}
}
@@ -354,142 +397,4 @@ static class Launcher
return false;
}
static bool IsDevModeAllowed(IPFilter ipFilter, string portalIP) => ipFilter.IsInRange(portalIP);
static long GenerateAuthSeedFunctionPatch(WinMemory memory, long modulusOffset)
{
var authSeedLoadOffset = memory.Data.FindPattern(Patterns.Windows.AuthSeed);
if (authSeedLoadOffset == 0)
throw new InvalidDataException("authSeedLoadOffset");
var leaStartOffset = authSeedLoadOffset + 9;
var leaValue = Unsafe.ReadUnaligned<int>(ref memory.Data[leaStartOffset + 3]);
var authSeedWrapperOffset = leaStartOffset + leaValue + 7;
var jmpValue = Unsafe.ReadUnaligned<uint>(ref memory.Data[authSeedWrapperOffset + 6]);
var authSeedFunctionOffset = authSeedWrapperOffset + 5 + jmpValue + 5;
// Write the modulus offset to our custom get seed functions.
// Resulting static auth seed is: 179D3DC3235629D07113A9B3867F97A7
Unsafe.WriteUnaligned(ref Patches.Windows.AuthSeed[3], (uint)(modulusOffset - authSeedFunctionOffset - 7));
return authSeedFunctionOffset;
}
static void WaitForUnpack(ref ProcessInformation processInfo, WinMemory memory, ref MemoryBasicInformation mbi, Stream gameAppData, bool antiCrash)
{
// Wait for client initialization.
var initOffset = memory.Read(mbi.BaseAddress, (int)mbi.RegionSize)?.FindPattern(Patterns.Windows.Init) ?? 0;
while (initOffset == 0)
{
initOffset = memory.Read(mbi.BaseAddress, (int)mbi.RegionSize)?.FindPattern(Patterns.Windows.Init) ?? 0;
Console.WriteLine("Waiting for client initialization...");
}
initOffset += BitConverter.ToUInt32(memory.Read(initOffset + memory.BaseAddress + 2, 4), 0) + 10;
while (memory.Read(initOffset + memory.BaseAddress, 1)?[0] == null ||
memory.Read(initOffset + memory.BaseAddress, 1)?[0] == 0)
memory.Data = memory.Read(mbi.BaseAddress, (int)mbi.RegionSize);
if (antiCrash)
PrepareAntiCrash(memory, ref mbi, ref processInfo);
memory.RefreshMemoryData((int)mbi.RegionSize);
}
static void PrepareAntiCrash(WinMemory memory, ref MemoryBasicInformation mbi, ref ProcessInformation processInfo)
{
memory.RefreshMemoryData((int)mbi.RegionSize);
// Suspend the process and handle the patches.
NativeWindows.NtSuspendProcess(processInfo.ProcessHandle);
// Get Integrity check locations
var integrityOffsets = memory.Data.FindPattern(Patterns.Windows.Integrity, int.MaxValue, (int)mbi.RegionSize).ToArray();
// Encrypt integrity offsets and patches and add them to the patch list.
for (var i = 0; i < integrityOffsets.Length; i++)
memory.QueuePatch(integrityOffsets[i], Patches.Windows.Integrity, $"Integrity{i}");
// Get Integrity check locations
var integrityOffsets2 = memory.Data.FindPattern(Patterns.Windows.Integrity2, int.MaxValue, (int)mbi.RegionSize).ToArray();
// Encrypt integrity offsets and patches and add them to the patch list.
for (var i = 0; i < integrityOffsets2.Length; i++)
memory.QueuePatch(integrityOffsets2[i], Patches.Windows.Integrity, $"Integrity{integrityOffsets.Length + i}");
// Get Remap check locations.
var remapOffsets = memory.Data.FindPattern(Patterns.Windows.Remap, int.MaxValue, (int)mbi.RegionSize);
var lastAddress = 0;
foreach (var a in remapOffsets)
{
var instructionStart = (int)a + 4;
var instructions = new byte[6];
Buffer.BlockCopy(memory.Data, instructionStart, instructions, 0, 6);
// Skip unconditional jumps.
if (WinMemory.IsUnconditionalJump(instructions))
continue;
int operandValue;
if (WinMemory.IsShortJump(instructions))
operandValue = instructions[1] + 2;
else if (WinMemory.IsJump(instructions))
operandValue = BitConverter.ToInt32(instructions, 2) + 6;
else
throw new InvalidDataException("Invalid operand value.");
var jumpToValue = a + operandValue + 4;
var tempPatches = new ConcurrentDictionary<string, (long, byte[])>();
// Find all references of real code parts inside the remap check functions.
Parallel.For(lastAddress, memory.Data.Length, i =>
{
if (WinMemory.IsJump(memory.Data, i))
{
var jumpOperand = BitConverter.ToInt32(memory.Data, i + 2);
var jumpSize = (int)jumpToValue - i - 6;
if (jumpOperand == jumpSize)
{
// Add 1 because we patch the instruction start.
// This results in a shorter overall instruction length.
var jumpBytes = new byte[] { 0xE9 }.Concat(BitConverter.GetBytes(jumpSize + 1)).ToArray();
tempPatches.TryAdd($"Jump{i}", (i, jumpBytes));
}
}
else if (WinMemory.IsShortJump(memory.Data, i))
{
var jumpOperand = memory.Data[i + 1];
var jumpSize = (int)jumpToValue - i - 2;
if (jumpOperand == jumpSize)
{
// Check for 0x48 here. This is an indicator for the test instructions.
// Might need some better checks or future updates.
if (memory.Data[i - 3] == 0x48)
{
var jumpBytes = new byte[] { 0xEB };
tempPatches.TryAdd($"ShortJump{i}", (i, jumpBytes));
}
}
}
});
// Add the remap crash patches to the patch list.
foreach (var p in tempPatches)
memory.QueuePatch(p.Value.Item1, p.Value.Item2, p.Key);
lastAddress = (int)a;
}
}
}

View File

@@ -1,30 +1,11 @@
// Copyright (c) Arctium.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
namespace Arctium.WoW.Launcher.Misc;
namespace Arctium.Game.Launcher.Misc;
static class Extensions
{
public static nint ToNint(this byte[] buffer) => (nint)BitConverter.ToInt64(buffer, 0);
public static nint ToNint(this long value) => (nint)value;
public static byte[] GetCopy(this byte[] data)
{
var copy = new byte[data.Length];
Array.Copy(data, copy, data.Length);
return copy;
}
public static short[] GetCopy(this short[] data)
{
var copy = new short[data.Length];
Array.Copy(data, copy, data.Length);
return copy;
}
public static long FindPattern(this byte[] data, short[] pattern, long start, long baseOffset = 0)
{
@@ -49,28 +30,5 @@ static class Extensions
}
public static long FindPattern(this byte[] data, short[] pattern, long baseOffset = 0) => FindPattern(data, pattern, 0L, baseOffset);
public static HashSet<long> FindPattern(this byte[] data, short[] pattern, int maxMatches, long maxOffset)
{
var matchList = new HashSet<long>();
long match = 0;
do
{
match = data.FindPattern(pattern, match, 0);
if (match == 0)
continue;
matchList.Add(match);
match += pattern.Length;
} while ((matchList.Count < maxMatches || match < maxOffset) && match != 0);
return matchList;
}
public static short[] ToPattern(this string data) => Encoding.UTF8.GetBytes(data).Select(b => (short)b).ToArray();
}

View File

@@ -1,10 +1,10 @@
// Copyright (c) Arctium.
// Copyright (c) Arctium.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
using System.Net;
using System.Net.Sockets;
namespace Arctium.WoW.Launcher.Misc;
namespace Arctium.Game.Launcher.Misc;
static class Helpers
{
@@ -17,6 +17,13 @@ static class Helpers
#endif
}
public static void PublisherCheck()
{
// "Arctium" should not be removed from the final binary name.
if (!Process.GetCurrentProcess().ProcessName.Contains("arctium", StringComparison.InvariantCultureIgnoreCase))
Process.GetCurrentProcess().Kill();
}
public static (int Major, int Minor, int Revision, int Build) GetVersionValueFromClient(string fileName)
{
var fileVersionInfo = FileVersionInfo.GetVersionInfo(fileName);
@@ -25,43 +32,43 @@ static class Helpers
fileVersionInfo.FileBuildPart, fileVersionInfo.FilePrivatePart);
}
public static void PrintHeader(string serverName)
public static void PrintHeader()
{
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine("Arctium Game Launcher");
Console.ResetColor();
Console.Write("Game: ");
Console.ForegroundColor = ConsoleColor.Cyan;
Console.WriteLine(@"_____________World of Warcraft___________");
Console.WriteLine(@" _ _ ");
Console.WriteLine(@" /\ | | (_) ");
Console.WriteLine(@" / \ _ __ ___| |_ _ _ _ _ __ ___ ");
Console.WriteLine(@" / /\ \ | '__/ __| __| | | | | '_ ` _ \ ");
Console.WriteLine(@" / ____ \| | | (__| |_| | |_| | | | | | |");
Console.WriteLine(@"/_/ \_\_| \___|\__|_|\__,_|_| |_| |_|");
Console.WriteLine("");
var sb = new StringBuilder();
sb.Append("_________________________________________");
var nameStart = (42 - serverName.Length) / 2;
sb.Insert(nameStart, serverName);
sb.Remove(nameStart + serverName.Length, serverName.Length);
Console.WriteLine(sb);
Console.WriteLine("{0,30}", "https://arctium.io");
Console.WriteLine("World of Warcraft");
Console.ResetColor();
Console.Write("Support: ");
Console.ForegroundColor = ConsoleColor.Cyan;
Console.WriteLine("https://arctium.io");
Console.ResetColor();
Console.WriteLine();
Console.WriteLine($"Operating System: {RuntimeInformation.OSDescription}");
}
public static (string IPAddress, string HostName, int Port) ParsePortal(string config)
public static (string IPAddress, string HostName, int Port) ParseOrSetPortal(ref string config, string customPortal = null)
{
const string portalKey = "SET portal";
var portalIndex = config.IndexOf(portalKey, StringComparison.Ordinal);
var portalIndex = config.IndexOf(portalKey, StringComparison.OrdinalIgnoreCase);
if (portalIndex == -1)
throw new ArgumentException("Config file does not contain the portal variable.");
{
if (string.IsNullOrEmpty(customPortal))
throw new ArgumentException("Config file does not contain a valid portal variable.");
// Append a new portal variable to the config.
config += $"{Environment.NewLine}{portalKey} \"{customPortal}\"";
}
// Re-evaluate the portal variable.
portalIndex = config.IndexOf(portalKey, StringComparison.OrdinalIgnoreCase);
if (portalIndex == -1)
throw new ArgumentException("Config file does not contain a valid portal variable.");
var startQuoteIndex = config.IndexOf('"', portalIndex);
@@ -78,8 +85,32 @@ static class Helpers
var colonIndex = portalSpan.IndexOf(':');
var ipSpan = colonIndex != -1 ? portalSpan[..colonIndex] : portalSpan;
var port = colonIndex != -1 ? int.Parse(portalSpan[(colonIndex + 1)..]) : 1119;
// Check if we have more than one portal line.
var lastPortalIndex = config.IndexOf(portalKey, portalIndex + portalLength, StringComparison.OrdinalIgnoreCase);
if (lastPortalIndex != -1 && lastPortalIndex != portalIndex)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine();
Console.WriteLine("Client portal is set multiple times. Please check your config file!");
Console.ResetColor();
return (string.Empty, string.Empty, port);
}
var portalString = ipSpan.ToString().Trim();
// Override portal variable if a custom one is provided.
if (!string.IsNullOrEmpty(customPortal))
{
portalString = customPortal;
config = string.Concat(config.AsSpan(0, startQuoteIndex + 1), portalString, config.AsSpan(endQuoteIndex));
// Let's re-parse the new portal for verification purposes.
return ParseOrSetPortal(ref config);
}
try
{
if (IPAddress.TryParse(portalString, out var ipAddress))
@@ -88,22 +119,39 @@ static class Helpers
var ipv4Address = Dns.GetHostAddresses(portalString).FirstOrDefault(a => a.AddressFamily == AddressFamily.InterNetwork);
if (ipv4Address == null)
throw new Exception("No IPv4 address found for the provided hostname.");
{
var ipv6Address = Dns.GetHostAddresses(portalString).FirstOrDefault(a => a.AddressFamily == AddressFamily.InterNetworkV6);
if (ipv6Address == null)
throw new Exception("No IPv4/IPv6 address found for the provided host name.");
return (ipv6Address.ToString(), portalString, port);
}
return (ipv4Address.ToString(), portalString, port);
}
catch (SocketException socketException) when (socketException.SocketErrorCode == SocketError.HostNotFound)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine($"DNS resolution failed for portal: {portalString}");
Console.ForegroundColor = ConsoleColor.Gray;
return (string.Empty, portalString, port);
}
catch (SocketException)
{
Console.WriteLine("No valid portal found. Dev (Local) mode disabled.");
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine("No valid portal found.");
Console.ForegroundColor = ConsoleColor.Gray;
return (string.Empty, string.Empty, port);
return (string.Empty, portalString, port);
}
}
public static async Task<bool> CheckUrl(string url, string fallbackUrl)
public static async Task<bool> CheckUrl(string url, string[] fallbackUrls)
{
using var httpClient = new HttpClient();
httpClient.Timeout = TimeSpan.FromSeconds(5);
try
@@ -111,15 +159,31 @@ static class Helpers
var result = await httpClient.GetAsync(url);
if (!result.IsSuccessStatusCode)
Console.WriteLine($"{url} not reachable. Falling back to {fallbackUrl}");
{
Console.WriteLine();
Console.WriteLine($"{url} not reachable. Falling back to: ");
foreach (var furl in fallbackUrls)
Console.WriteLine($"- {furl}");
}
return result.IsSuccessStatusCode;
}
catch (Exception)
{
Console.WriteLine($"{url} not reachable. Falling back to {fallbackUrl}");
Console.WriteLine();
Console.WriteLine($"{url} not reachable. Falling back to: ");
foreach (var furl in fallbackUrls)
Console.WriteLine($"- {furl}");
return false;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool UsesVersionV2(byte[] appPath)
{
return appPath.FindPattern(Patterns.Common.Version2UrlNew.ToPattern()) > 0;
}
}

View File

@@ -1,76 +0,0 @@
// Copyright (c) Arctium.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
using System.Net;
namespace Arctium.WoW.Launcher.Misc;
public class IPFilter
{
readonly List<(IPAddress, IPAddress)> _ipRanges = new();
public void AddCidrRange(ReadOnlySpan<char> cidr)
{
var separatorIndex = cidr.IndexOf('/');
if (separatorIndex < 0)
return;
var subnetMaskLengthSpan = cidr[(separatorIndex + 1)..];
if (!int.TryParse(subnetMaskLengthSpan, out var subnetMaskLength))
return;
Span<byte> subnetMaskBytes = stackalloc byte[4];
for (var i = 0; i < subnetMaskLength; i++)
subnetMaskBytes[i / 8] |= (byte)(1 << (7 - i % 8));
if (!IPAddress.TryParse(cidr[..separatorIndex], out var ip))
return;
ReadOnlySpan<byte> ipBytes = ip.GetAddressBytes();
Span<byte> networkAddressBytes = stackalloc byte[4];
Span<byte> broadcastAddressBytes = stackalloc byte[4];
for (var i = 0; i < 4; i++)
{
networkAddressBytes[i] = (byte)(ipBytes[i] & subnetMaskBytes[i]);
broadcastAddressBytes[i] = (byte)(networkAddressBytes[i] | ~subnetMaskBytes[i]);
}
_ipRanges.Add((new(networkAddressBytes), new(broadcastAddressBytes)));
}
public bool IsInRange(ReadOnlySpan<char> targetIP)
{
if (!IPAddress.TryParse(targetIP, out var ip))
return false;
ReadOnlySpan<byte> targetBytes = ip.GetAddressBytes();
foreach (var range in _ipRanges)
{
ReadOnlySpan<byte> networkAddressBytes = range.Item1.GetAddressBytes();
ReadOnlySpan<byte> broadcastAddressBytes = range.Item2.GetAddressBytes();
var isInRange = true;
for (var i = 0; i < 4; i++)
{
if ((targetBytes[i] & networkAddressBytes[i]) != networkAddressBytes[i] ||
(targetBytes[i] & broadcastAddressBytes[i]) != targetBytes[i])
{
isInRange = false;
break;
}
}
if (isInRange)
return true;
}
return false;
}
}

View File

@@ -1,12 +1,10 @@
// Copyright (c) Arctium.
// Copyright (c) Arctium.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
namespace Arctium.WoW.Launcher.Misc;
namespace Arctium.Game.Launcher.Misc;
static class NativeWindows
{
/// kernel32.dll
// Process
[DllImport("kernel32.dll", EntryPoint = "CreateProcessA", SetLastError = true)]
public static extern bool CreateProcess(string lpApplicationName, string lpCommandLine, nint lpProcessAttributes, nint lpThreadAttributes, bool bInheritHandles, uint dwCreationFlags, nint lpEnvironment, string lpCurrentDirectory, ref StartupInfo lpStartupInfo, out ProcessInformation lpProcessInformation);
@@ -16,13 +14,6 @@ static class NativeWindows
[DllImport("kernel32.dll", EntryPoint = "CloseHandle")]
public static extern void CloseHandle(nint handle);
// Memory
[DllImport("kernel32.dll", SetLastError = true)]
public static extern int VirtualQueryEx(nint hProcess, nint lpBaseAddress, out MemoryBasicInformation mbi, int dwSize);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern nint VirtualAllocEx(nint hProcess, nint lpAddress, uint dwSize, uint flAllocationType, uint flProtect);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool VirtualProtectEx(nint hProcess, nint lpAddress, uint dwSize, uint flNewProtect, out uint lpflOldProtect);
@@ -35,27 +26,12 @@ static class NativeWindows
[DllImport("kernel32.dll", EntryPoint = "FlushInstructionCache", SetLastError = true)]
public static extern bool FlushInstructionCache(nint hProcess, nint lpBaseAddress, uint dwSize);
/// ntdll.dll
// Process
[DllImport("ntdll.dll", SetLastError = true)]
public static extern NtStatus NtQueryInformationProcess(nint hProcess, int pic, ref ProcessBasicInformation pbi, int cb, out int pSize);
// Page/View
[DllImport("ntdll.dll", SetLastError = true)]
public static extern NtStatus NtCreateSection(ref nint sectionHandle, uint accessMask, nint zero, ref LargeInteger maximumSize, uint protection, uint allocationAttributes, nint zero2);
[DllImport("ntdll.dll", SetLastError = true)]
public static extern NtStatus NtMapViewOfSection(nint sectionHandle, nint proccessHandle, ref nint baseAddress, nint zero, ulong regionSize, out LargeInteger sectionOffset, out uint viewSize, uint viewSection, nint zero2, int protection);
[DllImport("ntdll.dll", SetLastError = true)]
public static extern NtStatus NtUnmapViewOfSection(nint processHandle, nint baseAddress);
public static extern int NtQueryInformationProcess(nint hProcess, int pic, ref ProcessBasicInformation pbi, int cb, out int pSize);
[DllImport("ntdll.dll", SetLastError = true)]
public static extern nint NtResumeProcess(nint processHandle);
[DllImport("ntdll.dll", SetLastError = true)]
public static extern nint NtSuspendProcess(nint processHandle);
[DllImport("ntdll.dll", SetLastError = true)]
public static extern nint NtClose(nint handle);
}

View File

@@ -1,189 +0,0 @@
// Copyright (c) Arctium.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
namespace Arctium.WoW.Launcher;
class ModLoader
{
static readonly HashSet<uint> _loadedFileIds = new();
public static bool HookClient(WinMemory memory, nint processHandle, nint idAlloc, nint stringAlloc)
{
var asm = new byte[]
{
0x85, 0xD2, 0x0F, 0x88, 0x72, 0xAF, 0xFF, 0xFF, 0x33, 0xC0,
0x49, 0xB9, 0x22, 0x22, 0x22, 0x22, 0x11, 0x11, 0x11, 0x11,
0x8D, 0x0C, 0x40, 0xC1, 0xE1, 0x02, 0x42, 0x39, 0x14, 0x09,
0x74, 0x0C, 0xFF, 0xC0, 0x3D, 0xEF, 0xBE, 0xAD, 0xDE, 0x72,
0xEB, 0x33, 0xC0, 0xC3, 0x4A, 0x8B, 0x44, 0x09, 0x04, 0xC3
};
var trampolineInjectAddress = NativeWindows.VirtualAllocEx(processHandle, IntPtr.Zero, (uint)asm.Length + 32, 0x00001000, (uint)MemProtection.ExecuteRead);
var hookInstructions = new byte[] { 0x48, 0xB8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xE0, 0x90 };
// Copy the asm code address.
Buffer.BlockCopy(BitConverter.GetBytes(trampolineInjectAddress), 0, hookInstructions, 2, 8);
// Inside the file load func!!!
var hookAddress = memory.Data.FindPattern(Patterns.Windows.CustomFileIdHook);
if (hookAddress == 0)
throw new InvalidDataException("CustomFileIdHook");
// Read original data from the hook function.
var originalBytes = memory.Read(memory.BaseAddress + hookAddress, 13);
// Apply the hook.
memory.QueuePatch(hookAddress, hookInstructions, "CustomFileIdHook");
// Copy count bytes.
Buffer.BlockCopy(BitConverter.GetBytes(_loadedFileIds.Count), 0, asm, 35, 4);
// Copy mapping ptr bytes.
Buffer.BlockCopy(BitConverter.GetBytes(idAlloc), 0, asm, 12, 8);
// Calculate the jump address bytes.
var trampolineJmpAddress = (uint)(trampolineInjectAddress + asm.Length - (trampolineInjectAddress + 2) - 6);
// Copy trampoline bytes.
Buffer.BlockCopy(BitConverter.GetBytes(trampolineJmpAddress), 0, asm, 4, 4);
memory.Write(trampolineInjectAddress, asm);
var jmpInstruction = new byte[] { 0x48, 0xB8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xE0, 0x90 };
// Copy the address where we continue to the jump instruction set.
Buffer.BlockCopy(BitConverter.GetBytes((ulong)(hookAddress + memory.BaseAddress + 13)), 0, jmpInstruction, 2, 8);
memory.Write(trampolineInjectAddress + asm.Length, originalBytes.Concat(jmpInstruction).ToArray());
return true;
}
public static (nint idAlloc, nint stringAlloc) LoadFileMappings(nint processHandle)
{
var loadedMappings = new List<(byte[] fileId, byte[] path, uint StringPos)>();
var count = 0u;
var localStringLength = 0u;
Console.WriteLine();
Console.WriteLine($"Loading file mappings from '{AppDomain.CurrentDomain.BaseDirectory}files':");
Directory.CreateDirectory($"{AppDomain.CurrentDomain.BaseDirectory}mappings");
// Add all file mappings
foreach (var f in Directory.EnumerateFiles($"{AppDomain.CurrentDomain.BaseDirectory}mappings", "*.txt", SearchOption.TopDirectoryOnly))
{
Console.ForegroundColor = ConsoleColor.DarkYellow;
Console.ForegroundColor = ConsoleColor.Gray;
Console.Write($"- {new FileInfo(f).Name}");
try
{
GetFileMappingData(loadedMappings, out var localCount, ref localStringLength, f);
count += localCount;
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine(" (Done)");
Console.ForegroundColor = ConsoleColor.Gray;
}
catch (Exception ex)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine(" (Failed)");
Console.ForegroundColor = ConsoleColor.Gray;
Console.WriteLine(ex);
Console.WriteLine(ex.StackTrace);
}
}
Console.WriteLine();
// Just startup without doing anything.
if (loadedMappings.Count == 0)
{
Console.WriteLine("No custom files.");
}
else
{
Console.ForegroundColor = ConsoleColor.DarkGreen;
Console.WriteLine($"Loaded {loadedMappings.Count} file mappings.");
Console.ForegroundColor = ConsoleColor.Gray;
Console.WriteLine();
// Allocate file mapping data.
return AllocateFileMapping(processHandle, loadedMappings, count, localStringLength);
}
return default;
}
static void GetFileMappingData(List<(byte[] fileId, byte[] path, uint StringPos)> loadedMappings, out uint count, ref uint stringLength, string mappingFile)
{
var mappings = File.ReadAllLines(mappingFile).Where(s => s.Trim() != "")
.Select(s => s.ToLowerInvariant().Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries))
.ToLookup(k => k[1], k => k[0]);
var filesDir = $@"{AppDomain.CurrentDomain.BaseDirectory}files";
// Create if it doesn't exist.
Directory.CreateDirectory(filesDir);
Console.WriteLine();
foreach (var f in Directory.EnumerateFiles(filesDir, "*", SearchOption.AllDirectories))
{
// Get the sub path inside the files folder.
var filesDirLength = Encoding.UTF8.GetByteCount(filesDir);
var pathBytes = Encoding.UTF8.GetBytes(f);
var path = Encoding.UTF8.GetString(pathBytes.Skip(filesDirLength + 1).ToArray()).Replace("\\", "/").ToLowerInvariant();
foreach (var fileId in mappings[path])
{
var id = uint.Parse(fileId);
if (_loadedFileIds.Contains(id))
{
Console.WriteLine($"Skipping overlapping file '{id} - {f}'.");
continue;
}
_loadedFileIds.Add(id);
loadedMappings.Add((BitConverter.GetBytes(id), pathBytes, stringLength));
stringLength += ((uint)pathBytes.Length + 1);
}
}
count = (uint)loadedMappings.Count;
}
static (nint, nint) AllocateFileMapping(nint handle, List<(byte[] fileId, byte[] path, uint StringPos)> loadedMappings, uint count, uint stringLength)
{
var idAlloc = NativeWindows.VirtualAllocEx(handle, IntPtr.Zero, (count * (4 + 8)), 0x00001000, 0x04);
var stringAlloc = NativeWindows.VirtualAllocEx(handle, IntPtr.Zero, stringLength, 0x00001000, 0x04);
var idAllocData = new byte[(count * (4 + 8))];
var stringAllocData = new byte[stringLength];
Parallel.For(0, loadedMappings.Count, m =>
{
var (fileId, path, stringPos) = loadedMappings[m];
Buffer.BlockCopy(fileId, 0, idAllocData, m * 12, fileId.Length);
Buffer.BlockCopy(BitConverter.GetBytes(stringAlloc + stringPos), 0, idAllocData, (m * 12) + 4, 8);
Buffer.BlockCopy(path, 0, stringAllocData, (int)stringPos, path.Length);
});
NativeWindows.WriteProcessMemory(handle, idAlloc, idAllocData, idAllocData.Length, out var _);
NativeWindows.WriteProcessMemory(handle, stringAlloc, stringAllocData, stringAllocData.Length, out var _);
// READ ONLY
NativeWindows.VirtualProtectEx(handle, idAlloc, (count * (4 + 8)), 0x02, out var _);
NativeWindows.VirtualProtectEx(handle, stringAlloc, stringLength, 0x02, out var _);
return (idAlloc, stringAlloc);
}
}

File diff suppressed because one or more lines are too long

View File

@@ -1,18 +1,10 @@
// Copyright (c) Arctium.
// Copyright (c) Arctium.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
namespace Arctium.WoW.Launcher.Patches;
namespace Arctium.Game.Launcher.Patches;
static class Windows
{
public static byte[] Integrity = { 0xC2, 0x00, 0x00 };
public static byte[] CertBundle = { 0x90, 0x90 };
public static byte[] CertCommonName = { 0xB0, 0x01 };
public static byte[] CertChain = { 0xB3, 0x01 };
public static byte[] ShortJump = { 0xEB };
public static byte[] NoJump = { 0x00, 0x00, 0x00, 0x00 };
public static byte[] AuthSeed = { 0x0F, 0x28, 0x05, 0xEF, 0xBE, 0xAD, 0xDE, 0x0F, 0x11, 0x02, 0xC3 };
// Registry entry used for -launcherlogin.
public static byte[] LauncherLogin = Encoding.UTF8.GetBytes(@"Software\Custom Game Server Dev\Battle.net\Launch Options\");
}

View File

@@ -1,15 +1,19 @@
// Copyright (c) Arctium.
// Copyright (c) Arctium.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
namespace Arctium.WoW.Launcher.Patterns;
namespace Arctium.Game.Launcher.Patterns;
static class Common
{
public static short[] ConnectToModulus = { 0x91, 0xD5, 0x9B, 0xB7, 0xD4, 0xE1, 0x83, 0xA5 };
public static short[] SignatureModulus = { 0x35, 0xFF, 0x17, 0xE7, 0x33, 0xC4, 0xD3, 0xD4 };
public static short[] CryptoRsaModulus = { 0x71, 0xFD, 0xFA, 0x60, 0x14, 0x0D, 0xF2, 0x05 };
public static short[] CryptoEdPublicKey = { 0x15, 0xD6, 0x18, 0xBD, 0x7D, 0xB5, 0x77, 0xBD };
public static short[] CryptoEdPublicKey = { 0x15, 0xD6, 0x18, 0xBD, 0x7D, 0xB5, 0x77, 0xBD, 0x9A, 0x8D, 0x45, 0x76, 0x9C, 0x59, 0xE4, 0xFC, 0x63 };
public static short[] CertBundle = "{\"Created\":".ToPattern();
public static string VersionUrl = "http://%s.patch.battle.net:1119/%s/versions";
public static string Version2Url = "https://%s.version.battle.net/v2/products/%s/versions";
public static string Version2UrlNew = "https://%s.version.battle.net/v2/products/%s/%s";
public static string Version2ChinaUrl = "https://cn.version.battlenet.com.cn/v2/products/%s/versions";
public static string Version2ChinaUrlNew = "https://cn.version.battlenet.com.cn/v2/products/%s/%s";
public static string CdnsUrl = "http://%s.patch.battle.net:1119/%s/cdns";
public static short[] Portal = ".actual.battle.net\0".ToPattern();
}

View File

@@ -1,34 +1,10 @@
// Copyright (c) Arctium.
// Copyright (c) Arctium.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
namespace Arctium.WoW.Launcher.Patterns;
namespace Arctium.Game.Launcher.Patterns;
static class Windows
{
public static short[] Init = { 0xC7, 0x05, -1, -1, -1, -1, 0x01, 0x00, 0x00, 0x00, 0x48, 0x8D, -1, -1, -1, -1, -1, 0x48, 0x8D, -1, -1, -1, -1, -1, 0xE8, -1, -1, -1, -1, 0x85 };
// Anti Crash.
public static short[] Integrity = { 0x44, 0x89, -1, 0x24, -1, 0x44, 0x89, -1, 0x24, -1, 0x89, -1, 0x24, -1, 0x48, 0x89, -1, 0x24, -1, 0x53, 0x56, 0x57 };
public static short[] Integrity2 = { 0x44, 0x89, -1, 0x24, -1, 0x44, 0x89, -1, 0x24, -1, 0x89, -1, 0x24, -1, 0x48, 0x89, -1, 0x24, -1, 0x53, 0x57 };
public static short[] Remap = { 0x48, -1, -1, 0x1E, 0x0F, 0x83 };
// Certificate bundle loading.
public static short[] CertBundle = { 0x75, 0x06, 0x48, -1, -1, 0x60, 0x5F, 0xC3 };
public static short[] CertCommonName = { 0x80, -1, 0x2A, 0x75, -1, 0x32, 0xC0, 0x48 };
// Developer mode.
public static short[] CertChain = { 0x32, 0xDB, 0xEB, 0x02, 0xB3, 0x01, 0x48, 0x83, -1, -1, 0x00, 0x00, 0x00, 0x00 };
// Auth seed function.
public static short[] AuthSeed = { 0x57, 0x6F, 0x57, 0x00, 0xE8, -1, -1, -1, -1, 0x48, 0x8D };
// Custom files (mods).
public static short[] LoadByFileId = { 0x41, -1, -1, 0x01, 0x0F, 0x84, -1, 0x00, 0x00, 0x00, 0x48, 0x8B, -1, -1, -1, -1, -1, 0x8B };
public static short[] LoadByFileIdAlternate = { 0x01, 0x0F, 0x84, -1, -1, -1, -1, 0x48, 0x8B, -1, -1, -1, -1, -1, 0x8B, -1, 0xE8 };
public static short[] LoadByFilePath = { 0x01, 0x0F, 0x84, -1, -1, -1, -1, 0x48, 0x8B, -1, -1, -1, -1, -1, 0x44, 0x89, -1, -1, -1, 0x48, 0x85, 0xC9 };
public static short[] LoadByFilePathAlternate = { 0x01, 0x0F, 0x84, -1, -1, -1, -1, 0x48, 0x8B, -1, -1, -1, -1, -1, 0x44, 0x89, -1, -1, -1, 0x00, 0x00, 0x00, 0x48, 0x85, 0xC9 };
public static short[] CustomFileIdHook = { 0x48, 0x89, 0x5C, 0x24, 0x08, 0x57, 0x48, 0x83, 0xEC, 0x20, 0x48, 0x8B, -1, 0x48, -1, -1, 0x48, -1, -1, -1, -1, -1, -1, 0xE8, -1, -1, -1, -1, 0x48, -1, -1, -1, -1, -1, -1, 0x48, -1, -1, 0x74, -1, 0x48, -1, 0xCD };
// Registry entry used for -launcherlogin.
public static short[] LauncherLogin = @"Software\Blizzard Entertainment\Battle.net\Launch Options\".ToPattern();
}

View File

@@ -1,71 +1,46 @@
// Copyright (c) Arctium.
// Copyright (c) Arctium.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
using System.CommandLine.Parsing;
using Arctium.WoW.Launcher;
using Arctium.Game.Launcher;
using static Arctium.WoW.Launcher.Misc.Helpers;
using static Arctium.Game.Launcher.Misc.Helpers;
// "Arctium" should not be removed from the final binary name.
if (!Process.GetCurrentProcess().ProcessName.Contains("arctium", StringComparison.InvariantCultureIgnoreCase))
WaitAndExit();
PublisherCheck();
PrintHeader();
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine("THIS LAUNCHER IS DISCONTINUED!!! Stop asking for support.");
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine("You can find the new supported launcher on https://arctium.io");
var versionInfo = FileVersionInfo.GetVersionInfo(Environment.ProcessPath);
Console.WriteLine($"Using {(int)(Environment.ProcessorCount * 0.75)} of {Environment.ProcessorCount} processors.");
Console.WriteLine();
Console.ResetColor();
WaitAndExit(5000);
PrintHeader("WoW Client Launcher");
PublisherCheck();
LaunchOptions.RootCommand.SetHandler(async context =>
{
CreateDevIPFilter(out var ipFilter);
try
{
// Prefer / instead of \ for the client path.
var appPath = (await Launcher.PrepareGameLaunch(context.ParseResult, default)).Replace("\\", "/");
var gameCommandLine = string.Join(" ", context.ParseResult.UnmatchedTokens);
// Prefer / instead of \ for the client path.
var appPath = (await Launcher.PrepareGameLaunch(context.ParseResult, ipFilter)).Replace("\\", "/");
var gameCommandLine = string.Join(" ", context.ParseResult.UnmatchedTokens);
// Add config parameter to the game command line.
gameCommandLine += $" -config {context.ParseResult.GetValueForOption(LaunchOptions.GameConfig)}";
// Add config parameter to the game command line.
gameCommandLine += $" -config {context.ParseResult.GetValueForOption(LaunchOptions.GameConfig)}";
if (string.IsNullOrEmpty(appPath) || !Launcher.LaunchGame(appPath, gameCommandLine, context.ParseResult))
WaitAndExit(5000);
}
catch (Exception ex)
{
if (Launcher.GameProcessHandle != nint.Zero)
NativeWindows.TerminateProcess(Launcher.GameProcessHandle, 0);
if (string.IsNullOrEmpty(appPath) || !Launcher.LaunchGame(appPath, gameCommandLine, context.ParseResult))
WaitAndExit(5000);
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine(ex.Message);
Console.ResetColor();
}
});
await LaunchOptions.Instance.InvokeAsync(args);
return;
void CreateDevIPFilter(out IPFilter ipFilter)
{
ipFilter = new IPFilter();
ipFilter.AddCidrRange("0.0.0.0/8");
ipFilter.AddCidrRange("10.0.0.0/8");
ipFilter.AddCidrRange("100.64.0.0/10");
ipFilter.AddCidrRange("127.0.0.0/8");
ipFilter.AddCidrRange("169.254.0.0/16");
ipFilter.AddCidrRange("172.16.0.0/12");
ipFilter.AddCidrRange("192.0.0.0/24");
ipFilter.AddCidrRange("192.0.0.0/29");
ipFilter.AddCidrRange("192.0.0.8/32");
ipFilter.AddCidrRange("192.0.0.9/32");
ipFilter.AddCidrRange("192.0.0.170/32");
ipFilter.AddCidrRange("192.0.0.171/32");
ipFilter.AddCidrRange("192.0.2.0/24");
ipFilter.AddCidrRange("192.31.196.0/24");
ipFilter.AddCidrRange("192.52.193.0/24");
ipFilter.AddCidrRange("192.88.99.0/24");
ipFilter.AddCidrRange("192.168.0.0/16");
ipFilter.AddCidrRange("192.175.48.0/24");
ipFilter.AddCidrRange("198.18.0.0/15");
ipFilter.AddCidrRange("198.51.100.0/24");
ipFilter.AddCidrRange("203.0.113.0/24");
ipFilter.AddCidrRange("240.0.0.0/4");
ipFilter.AddCidrRange("255.255.255.255/32");
}
static void WaitAndExit(int ms = 2000)
{
@@ -75,4 +50,4 @@ static void WaitAndExit(int ms = 2000)
Thread.Sleep(ms);
Environment.Exit(0);
}
}

View File

@@ -1,17 +0,0 @@
// Copyright (c) Arctium.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
namespace Arctium.WoW.Launcher.Structures;
[StructLayout(LayoutKind.Explicit, Size = 8)]
struct LargeInteger
{
[FieldOffset(0)]
public long Quad;
[FieldOffset(0)]
public uint Low;
[FieldOffset(4)]
public int High;
public static int Size => Marshal.SizeOf<LargeInteger>();
}

View File

@@ -1,7 +1,7 @@
// Copyright (c) Arctium.
// Copyright (c) Arctium.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
namespace Arctium.WoW.Launcher.Structures;
namespace Arctium.Game.Launcher.Structures;
[StructLayout(LayoutKind.Sequential)]
struct MemoryBasicInformation
@@ -10,9 +10,9 @@ struct MemoryBasicInformation
public nint AllocationBase;
public MemProtection AllocationProtect;
public nint RegionSize;
public MemState State;
public int State;
public MemProtection Protect;
public MemType Type;
public int Type;
public static int Size => Marshal.SizeOf<MemoryBasicInformation>();
}

View File

@@ -1,7 +1,7 @@
// Copyright (c) Arctium.
// Copyright (c) Arctium.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
namespace Arctium.WoW.Launcher.Structures;
namespace Arctium.Game.Launcher.Structures;
[StructLayout(LayoutKind.Sequential, Pack = 1)]
struct ProcessBasicInformation

View File

@@ -1,7 +1,7 @@
// Copyright (c) Arctium.
// Copyright (c) Arctium.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
namespace Arctium.WoW.Launcher.Structures;
namespace Arctium.Game.Launcher.Structures;
struct ProcessInformation
{

View File

@@ -1,7 +1,7 @@
// Copyright (c) Arctium.
// Copyright (c) Arctium.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
namespace Arctium.WoW.Launcher.Structures;
namespace Arctium.Game.Launcher.Structures;
struct StartupInfo
{

View File

@@ -1,14 +1,13 @@
// Copyright (c) Arctium.
// Copyright (c) Arctium.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
global using System.Collections.Concurrent;
global using System.CommandLine;
global using System.Diagnostics;
global using System.Runtime.CompilerServices;
global using System.Runtime.InteropServices;
global using System.Text;
global using Arctium.WoW.Launcher.Constants;
global using Arctium.WoW.Launcher.IO;
global using Arctium.WoW.Launcher.Misc;
global using Arctium.WoW.Launcher.Structures;
global using Arctium.Game.Launcher.Constants;
global using Arctium.Game.Launcher.IO;
global using Arctium.Game.Launcher.Misc;
global using Arctium.Game.Launcher.Structures;