обновил лаунчер под новую версию веба

This commit is contained in:
2026-10-04 13:19:12 +04:00
parent dd189fe48f
commit d3d19bfaeb
20 changed files with 486 additions and 64 deletions
+1 -1
View File
@@ -526,7 +526,7 @@ class _HeroCopy extends StatelessWidget {
),
const SizedBox(height: 14),
const Text(
'Wrath of the Lich King · приватный сервер',
'World of Warcraft 1.12.1 · приватный сервер',
style: TextStyle(
fontFamily: 'Cormorant Garamond',
fontStyle: FontStyle.italic,
@@ -351,8 +351,6 @@ class HomeScreenBloc extends Bloc<HomeScreenEvent, HomeScreenState> {
_gameLaunchPending = true;
try {
final installationDir = outputPath.toFilePath();
await _gameInstallationService.removeSavedAccountName(installationDir);
await _gameInstallationService.clearCache(installationDir);
final authorization = await _launcherApiClient.issueGameTicket(
_session.accessToken,
);
@@ -381,7 +379,7 @@ class HomeScreenBloc extends Bloc<HomeScreenEvent, HomeScreenState> {
model: state.model.copyWith(
phase: HomeScreenPhase.readyToPlay,
isGameRunning: true,
statusText: 'Игра запущена. Папка Cache очищена перед стартом.',
statusText: 'MoonWell запущен.',
errorMessage: null,
),
),
@@ -418,7 +416,7 @@ class HomeScreenBloc extends Bloc<HomeScreenEvent, HomeScreenState> {
isGameRunning: false,
statusText: hasClientExecutable
? 'Игра закрыта. Клиент готов к запуску.'
: 'Игра закрыта, но Wow.exe больше не найден.',
: 'Игра закрыта, но клиент MoonWell больше не найден.',
errorMessage: null,
),
),
+6 -1
View File
@@ -6,7 +6,11 @@ final class Config {
'MOONWELL_APPCAST_URL',
);
static const launcherLogFileName = 'launcher.log';
static const gameExecutableName = 'Wow.exe';
static const gameExecutableName = 'MoonWell.exe';
static const gameClientBuild = 5875;
static const gameAuthAddress = String.fromEnvironment(
'MOONWELL_AUTH_ADDRESS',
);
static const cacheDirectoryName = 'Cache';
static const launcherMetadataDirectoryName = '.moonwell_launcher';
static const hashCacheFileName = 'hash_cache.json';
@@ -17,6 +21,7 @@ final class Config {
'logs',
'screenshots',
'wtf',
'benilla-config',
'.moonwell_launcher',
};
}
@@ -33,6 +33,10 @@ final class GameProcessHandle {
@lazySingleton
class GameInstallationService {
String get operatingSystem => Platform.operatingSystem;
String get configuredAuthAddress => Config.gameAuthAddress;
Future<ClientInstallationSnapshot> scanInstallation(
String installationDir, {
required ClientManifest manifest,
@@ -249,18 +253,39 @@ class GameInstallationService {
final executable = File(executablePath);
if (!await executable.exists()) {
throw LauncherSyncException(
'MoonWell client was not found: $executablePath',
);
}
final authAddress = await resolveAuthAddress(installationDir);
final rootPath = p.absolute(installationDir);
final directDataPath = p.join(rootPath, 'Data');
final nestedDataPath = p.join(rootPath, 'WoW', 'Data');
final dataPath = await Directory(directDataPath).exists()
? directDataPath
: nestedDataPath;
if (!await Directory(dataPath).exists()) {
throw const LauncherSyncException(
'Wow.exe was not found in the selected folder.',
'WoW 1.12.1 Data or WoW/Data was not found in the selected folder.',
);
}
if (!authorization.isUsable()) {
throw const LauncherSyncException(
'Game ticket has expired. Press Play to request a new ticket.',
);
}
return startGameProcess(
executablePath: executablePath,
arguments: const <String>[],
workingDirectory: installationDir,
workingDirectory: rootPath,
environment: <String, String>{
'MOONWELL_LAUNCH_ACCOUNT': authorization.account,
'MOONWELL_LAUNCH_TICKET': authorization.ticket,
'WOW_USER': authorization.account,
'WOW_PASS': authorization.ticket,
'WOW_HOST': authAddress,
'WOW_DATA': dataPath,
'BENILLA_HOME': p.join(rootPath, 'benilla-config'),
},
);
}
@@ -308,7 +333,56 @@ class GameInstallationService {
}
String getExecutablePath(String installationDir) {
return p.join(installationDir, Config.gameExecutableName);
final rootPath = p.absolute(installationDir);
return switch (operatingSystem) {
'windows' => p.join(rootPath, Config.gameExecutableName),
'macos' => p.join(
rootPath,
'MoonWell.app',
'Contents',
'MacOS',
'launch',
),
_ => p.join(rootPath, 'MoonWell'),
};
}
/// The auth endpoint is separate from a realm's world port (normally 8085).
Future<String> resolveAuthAddress(String installationDir) async {
final configured = configuredAuthAddress.trim();
if (configured.isNotEmpty) {
return _validateAuthAddress(configured);
}
final realmlist = File(p.join(installationDir, 'realmlist.wtf'));
if (await realmlist.exists()) {
final pattern = RegExp(
r'^\s*set\s+realmlist\s+(?:"([^"]+)"|([^\s]+))\s*(?://.*)?$',
caseSensitive: false,
multiLine: true,
);
final matches = pattern.allMatches(await realmlist.readAsString());
if (matches.isNotEmpty) {
final match = matches.last;
return _validateAuthAddress((match.group(1) ?? match.group(2)!).trim());
}
}
throw const LauncherSyncException(
'Set realmlist in realmlist.wtf or configure MOONWELL_AUTH_ADDRESS.',
);
}
String _validateAuthAddress(String address) {
final match = RegExp(
r'^([A-Za-z0-9][A-Za-z0-9._-]*)(?::([0-9]{1,5}))?$',
).firstMatch(address);
final port = int.tryParse(match?.group(2) ?? '3724');
if (match == null || port == null || port < 1 || port > 65535) {
throw const LauncherSyncException(
'The auth address must be a hostname or IPv4 address with an optional TCP port.',
);
}
return address;
}
String resolveClientPath(String installationDir, String relativePath) {
@@ -18,6 +18,8 @@ import 'package:moonwell_launcher/features/launcher/domain/entities/launcher_ses
@lazySingleton
class LauncherApiClient {
String get apiBaseUrl => Config.launcherApiBaseUrl;
LauncherApiClient({LauncherLogService? launcherLogService})
: _launcherLogService = launcherLogService ?? LauncherLogService(),
_dio = Dio(
@@ -169,7 +171,7 @@ class LauncherApiClient {
try {
final response = await _dio.postUri(
ticketUri,
data: const <String, Object?>{'client_build': 12340},
data: const <String, Object?>{'client_build': Config.gameClientBuild},
options: _authorizedOptions(accessToken),
);
final ticket = LauncherGameTicket.fromJson(_coerceMap(response.data));
@@ -425,7 +427,7 @@ class LauncherApiClient {
}
Uri _resolveUri(String path) {
final rawBaseUrl = Config.launcherApiBaseUrl.trim();
final rawBaseUrl = apiBaseUrl.trim();
if (rawBaseUrl.isEmpty) {
throw const LauncherConfigurationException(
'Set MOONWELL_API_BASE_URL before using the launcher API.',
@@ -1,5 +1,5 @@
final class LauncherGameTicket {
static final RegExp _accountPattern = RegExp(r'^[!-<>-~]{1,17}$');
static final RegExp _accountPattern = RegExp(r'^[A-Z0-9]{3,16}$');
static final RegExp _ticketPattern = RegExp(r'^[A-Z0-9]{16}$');
final String account;