обновил лаунчер под новую версию веба
This commit is contained in:
@@ -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
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user