Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions lib/src/core/dancing_line_game.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ class DancingLineGame extends Game {
@override
String? get steamAppId => '4395300';

@override
List<String> windowsExecutableNames() => const [
'Dancing Line.exe',
'DancingLine.exe',
];

@override
List<String> getSteamInstallFolderNames() => const [
'Dancing Line',
Expand Down
32 changes: 32 additions & 0 deletions lib/src/core/game.dart
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,38 @@ abstract class Game {
// Folder names Steam may use under steamapps/common.
List<String> getSteamInstallFolderNames() => [name];

/// Candidate Windows executable file names, used to detect a Proton/Wine
/// install on a non-Windows host. Subclasses override when the Windows build
/// uses a name that differs from `<name>.exe`.
List<String> windowsExecutableNames() => ['$name.exe'];

/// True when this install is the Windows build running through Proton/Wine —
/// i.e. a Windows .exe is present on a non-Windows host.
bool isProtonOrWineInstall(String gamePath) {
if (Platform.isWindows) return false;
for (final exe in windowsExecutableNames()) {
if (File(p.join(gamePath, exe)).existsSync()) return true;
}
return false;
}

/// The exact Steam Launch Options string MelonLoader needs for this install,
/// or null when none is required (Windows, where the bootstrap injects
/// without a wrapper). Used to auto-write Steam's localconfig.vdf after
/// install and to populate the manual guide.
String? steamLaunchOptionsValue(String gamePath) {
if (Platform.isWindows) return null;
if (isProtonOrWineInstall(gamePath)) {
return r'WINEDLLOVERRIDES="winhttp=n,b" %command%';
}
if (Platform.isMacOS) {
// Steam on macOS does not resolve relative paths; use the absolute script.
return '"${p.join(gamePath, 'setup_helper.sh')}" %command%';
}
// Linux native
return './setup_helper.sh %command%';
}

String? findSteamInstallPath() {
for (final candidate in getSteamInstallCandidatePaths()) {
if (isValidGamePath(candidate)) {
Expand Down
68 changes: 68 additions & 0 deletions lib/src/core/installer_state.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:path/path.dart' as p;
import 'game.dart';
import 'steam_config.dart';
import 'adofai_game.dart';
import 'dancing_line_game.dart';
import 'rhythm_doctor_game.dart';
Expand Down Expand Up @@ -44,6 +45,11 @@ class InstallerState extends ChangeNotifier {
bool _isProcessing = false;
String? _statusMessage;

// Whether the last install auto-wrote Steam's Launch Options for this game,
// and whether Steam needs a full restart for that write to take effect.
bool _steamLaunchOptionsApplied = false;
bool _steamLaunchOptionsNeedRestart = false;

// Mod Update Cache & Status
final Map<String, ModItem> _onlineModsCache = {};
List<String> _modsWithUpdates = [];
Expand Down Expand Up @@ -73,6 +79,9 @@ class InstallerState extends ChangeNotifier {
bool get isUmmDetected => _isUmmDetected;
String get loaderVersion => _loaderVersion;
bool get isLoaderOutdated => _isLoaderOutdated;

bool get steamLaunchOptionsApplied => _steamLaunchOptionsApplied;
bool get steamLaunchOptionsNeedRestart => _steamLaunchOptionsNeedRestart;

String get locale => _locale;

Expand Down Expand Up @@ -325,6 +334,8 @@ class InstallerState extends ChangeNotifier {
_isProcessing = true;
_progress = 0.0;
_statusMessage = t('status_loader_downloading');
_steamLaunchOptionsApplied = false;
_steamLaunchOptionsNeedRestart = false;
notifyListeners();

var installTimedOut = false;
Expand Down Expand Up @@ -371,6 +382,10 @@ class InstallerState extends ChangeNotifier {
);
await DebugLog.info('Core MelonLoader install completed');

// Point Steam's Launch Options at the setup_helper.sh wrapper so the
// loader actually injects on launch (no-op on Windows / unknown appid).
await _applySteamLaunchOptions();

if (_isUmmDetected && installUmmCompat) {
_statusMessage = t('status_loader_checking_ummcompat');
notifyListeners();
Expand Down Expand Up @@ -412,6 +427,58 @@ class InstallerState extends ChangeNotifier {
}
}

// Steam Launch Options 자동 설정 (localconfig.vdf 직접 편집)
Future<void> _applySteamLaunchOptions() async {
final appId = game.steamAppId;
final value = game.steamLaunchOptionsValue(_gamePath);
if (appId == null || value == null) {
// Windows (no wrapper needed) or unknown app id — nothing to write.
return;
}
try {
final result = await SteamConfig.setLaunchOptions(
appId: appId,
value: value,
);
_steamLaunchOptionsApplied = result.applied;
_steamLaunchOptionsNeedRestart = result.applied && result.steamRunning;
await DebugLog.info(
'Steam launch options: applied=${result.applied} '
'updated=${result.updatedCount} configFound=${result.configFound} '
'steamRunning=${result.steamRunning} appId=$appId',
);
} catch (e, stackTrace) {
_steamLaunchOptionsApplied = false;
await DebugLog.error(
'Failed to set Steam launch options',
error: e,
stackTrace: stackTrace,
);
}
}

// Steam Launch Options 정리 (우리가 설정한 래퍼만 제거)
Future<void> _clearSteamLaunchOptions() async {
final appId = game.steamAppId;
if (appId == null) return;
try {
final result = await SteamConfig.clearLaunchOptions(appId: appId);
await DebugLog.info(
'Cleared Steam launch options: updated=${result.updatedCount} '
'appId=$appId',
);
} catch (e, stackTrace) {
await DebugLog.error(
'Failed to clear Steam launch options',
error: e,
stackTrace: stackTrace,
);
} finally {
_steamLaunchOptionsApplied = false;
_steamLaunchOptionsNeedRestart = false;
}
}

// UMM 호환 모드(ummcompat) 설치
Future<void> installUmmCompat() async {
if (_isProcessing || !_isValidPath) return;
Expand Down Expand Up @@ -500,6 +567,7 @@ class InstallerState extends ChangeNotifier {

try {
await game.uninstallLoader(_gamePath);
await _clearSteamLaunchOptions();
_statusMessage = t('status_loader_uninstall_success');
} catch (e) {
_statusMessage = t('status_loader_uninstall_failed', args: {'error': describeAppError(e)});
Expand Down
9 changes: 9 additions & 0 deletions lib/src/core/localization.dart
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,9 @@ class Localization {
'installed_copied_clipboard': '클립보드에 복사되었습니다.',
'installed_err_file_picker': '파일 선택 중 오류가 발생했습니다: {error}',
'installed_launch_guide_title': '💡 비윈도우 OS 추가 안내 사항',
'installed_launch_auto_set': '✓ 스팀 실행 옵션이 자동으로 설정되었습니다.',
'installed_launch_auto_set_restart':
'변경 사항을 적용하려면 스팀을 완전히 종료한 후 다시 실행하세요.',
'installed_btn_copy_native_launch': '네이티브 시작 옵션 복사',
'installed_btn_copy_proton_launch': 'Proton 시작 옵션 복사',
'settings_btn_check_updates': '업데이트 확인',
Expand Down Expand Up @@ -375,6 +378,10 @@ class Localization {
'installed_copied_clipboard': 'Copied to clipboard.',
'installed_err_file_picker': 'An error occurred while picking file: {error}',
'installed_launch_guide_title': '💡 Additional Guide for Non-Windows OS',
'installed_launch_auto_set':
'✓ Steam launch options were configured automatically.',
'installed_launch_auto_set_restart':
'Fully quit and reopen Steam for the change to take effect.',
'installed_btn_copy_native_launch': 'Copy Native Launch Options',
'installed_btn_copy_proton_launch': 'Copy Proton Launch Options',
'settings_btn_check_updates': 'Check for Updates',
Expand Down Expand Up @@ -656,6 +663,8 @@ class Localization {
'installed_copied_clipboard': '已复制到剪贴板。',
'installed_err_file_picker': '选择文件时出错: {error}',
'installed_launch_guide_title': '💡 非 Windows 系统额外说明',
'installed_launch_auto_set': '✓ 已自动配置 Steam 启动选项。',
'installed_launch_auto_set_restart': '请完全退出并重新打开 Steam 以使更改生效。',
'installed_btn_copy_native_launch': '复制原生启动选项',
'installed_btn_copy_proton_launch': '复制 Proton 启动选项',
'settings_btn_check_updates': '检查更新',
Expand Down
6 changes: 6 additions & 0 deletions lib/src/core/rhythm_doctor_game.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ class RhythmDoctorGame extends Game {
@override
String? get steamAppId => '774181';

@override
List<String> windowsExecutableNames() => const [
'Rhythm Doctor.exe',
'RhythmDoctor.exe',
];

@override
List<String> getSteamInstallFolderNames() => const [
'Rhythm Doctor',
Expand Down
Loading