Skip to content
Draft
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
2 changes: 1 addition & 1 deletion app/src/main/app/shell/UnifiedActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9280,7 +9280,7 @@ class UnifiedActivity :
} catch (e: Exception) {
0L
}
val isInstallEnabled = totalInstallSize == 0L || availableBytes >= totalInstallSize
val isInstallEnabled = installed == true || totalInstallSize == 0L || availableBytes >= totalInstallSize
val installPathDisplay = customPath ?: SteamService.defaultAppInstallPath

val dlcItems =
Expand Down
Binary file modified app/src/main/assets/wnsteam/bionic/steam.exe
Binary file not shown.
160 changes: 150 additions & 10 deletions app/src/main/cpp/wn-steam-launcher/src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,24 @@ static void stage_steam_config(void) {
}
}

// Escape a free-text value for a VDF/ACF quoted field: double backslashes, then
// escape quotes and newlines. Mirrors the Kotlin escapeString() so the C++ and
// Kotlin manifest paths produce identical, well-formed output.
static std::string vdf_escape(const char* s) {
std::string out;
if (!s) return out;
for (const char* p = s; *p; ++p) {
switch (*p) {
case '\\': out += "\\\\"; break;
case '"': out += "\\\""; break;
case '\n': out += "\\n"; break;
case '\r': out += "\\r"; break;
default: out += *p; break;
}
}
return out;
}

static void stage_app_manifest(uint32_t appId, const char* gameExe) {
if (appId == 0 || !gameExe) return;
const char* marker = "\\steamapps\\common\\";
Expand Down Expand Up @@ -258,33 +276,155 @@ static void stage_app_manifest(uint32_t appId, const char* gameExe) {
snprintf(acf, sizeof(acf),
"C:\\Program Files (x86)\\Steam\\steamapps\\appmanifest_%u.acf",
appId);
const char* owner = getenv("WN_STEAM_STEAMID");
const char* depotsEnv = getenv("WN_STEAM_DEPOTS");
const char* sharedEnv = getenv("WN_STEAM_SHARED_DEPOTS");
const char* appName = getenv("WN_STEAM_APP_NAME");
const char* installScriptsEnv = getenv("WN_STEAM_INSTALL_SCRIPTS");
const char* language = getenv("WN_STEAM_LANGUAGE");
const char* buildIdStr = getenv("WN_STEAM_BUILD_ID");
const char* sizeOnDiskStr = getenv("WN_STEAM_SIZE_ON_DISK");
const char* bytesToDownloadStr = getenv("WN_STEAM_BYTES_TO_DOWNLOAD");
const char* bytesToStageStr = getenv("WN_STEAM_BYTES_TO_STAGE");
if (!appName || !*appName) appName = installdir;
if (!language || !*language) language = "english";
unsigned long long buildId = (buildIdStr && *buildIdStr) ? strtoull(buildIdStr, NULL, 10) : 0ULL;
unsigned long long sizeOnDisk = (sizeOnDiskStr && *sizeOnDiskStr) ? strtoull(sizeOnDiskStr, NULL, 10) : 0ULL;
unsigned long long bytesToDownload = (bytesToDownloadStr && *bytesToDownloadStr) ? strtoull(bytesToDownloadStr, NULL, 10) : 0ULL;
unsigned long long bytesToStage = (bytesToStageStr && *bytesToStageStr) ? strtoull(bytesToStageStr, NULL, 10) : 0ULL;
FILE* f = fopen(acf, "w");
if (!f) {
log_line("[wn-launcher] app manifest: fopen(%s) failed", acf);
return;
}
const char* owner = getenv("WN_STEAM_STEAMID");
std::string nameEsc = vdf_escape(appName);
std::string installdirEsc = vdf_escape(installdir);
std::string languageEsc = vdf_escape(language);
fprintf(f,
"\"AppState\"\n"
"{\n"
"\t\"appid\"\t\t\"%u\"\n"
"\t\"Universe\"\t\t\"1\"\n"
"\t\"universe\"\t\t\"1\"\n"
"\t\"LauncherPath\"\t\t\"C:\\\\Program Files (x86)\\\\Steam\\\\steam.exe\"\n"
"\t\"name\"\t\t\"%s\"\n"
"\t\"StateFlags\"\t\t\"4\"\n"
"\t\"installdir\"\t\t\"%s\"\n"
"\t\"LastUpdated\"\t\t\"0\"\n"
"\t\"SizeOnDisk\"\t\t\"0\"\n"
"\t\"buildid\"\t\t\"0\"\n"
"\t\"LastUpdated\"\t\t\"%llu\"\n"
"\t\"LastPlayed\"\t\t\"0\"\n"
"\t\"SizeOnDisk\"\t\t\"%llu\"\n"
"\t\"StagingSize\"\t\t\"0\"\n"
"\t\"buildid\"\t\t\"%llu\"\n"
"\t\"LastOwner\"\t\t\"%s\"\n"
"\t\"InstalledDepots\"\n"
"\t\"DownloadType\"\t\t\"1\"\n"
"\t\"UpdateResult\"\t\t\"0\"\n"
"\t\"BytesToDownload\"\t\t\"%llu\"\n"
"\t\"BytesDownloaded\"\t\t\"%llu\"\n"
"\t\"BytesToStage\"\t\t\"%llu\"\n"
"\t\"BytesStaged\"\t\t\"%llu\"\n"
"\t\"TargetBuildID\"\t\t\"%llu\"\n"
"\t\"AutoUpdateBehavior\"\t\t\"0\"\n"
"\t\"AllowOtherDownloadsWhileRunning\"\t\t\"0\"\n"
"\t\"ScheduledAutoUpdate\"\t\t\"0\"\n",
appId, nameEsc.c_str(), installdirEsc.c_str(),
(unsigned long long)time(NULL),
sizeOnDisk, buildId,
(owner && *owner) ? owner : "0",
bytesToDownload, bytesToDownload,
bytesToStage, bytesToStage, buildId);
// Write InstalledDepots with depot data from WN_STEAM_DEPOTS env var.
// Format: depotId:manifestGid:size[:dlcAppId],...
if (depotsEnv && *depotsEnv) {
fprintf(f, "\t\"InstalledDepots\"\n\t{\n");
std::vector<char> buf(strlen(depotsEnv) + 1);
memcpy(buf.data(), depotsEnv, buf.size());
char* token = strtok(buf.data(), ",");
while (token) {
// Parse depotId:manifestGid:size[:dlcAppId]
char* colon1 = strchr(token, ':');
if (!colon1) { token = strtok(NULL, ","); continue; }
*colon1 = '\0';
const char* depotIdStr = token;
char* manifestStart = colon1 + 1;
char* colon2 = strchr(manifestStart, ':');
if (!colon2) { token = strtok(NULL, ","); continue; }
*colon2 = '\0';
const char* manifestStr = manifestStart;
char* sizeStart = colon2 + 1;
char* colon3 = strchr(sizeStart, ':');
const char* sizeStr, *dlcAppIdStr;
if (colon3) {
*colon3 = '\0';
sizeStr = sizeStart;
dlcAppIdStr = colon3 + 1;
} else {
sizeStr = sizeStart;
dlcAppIdStr = NULL;
}
fprintf(f, "\t\t\"%s\"\n\t\t{\n"
"\t\t\t\"manifest\"\t\t\"%s\"\n"
"\t\t\t\"size\"\t\t\"%s\"\n",
depotIdStr, manifestStr, sizeStr);
if (dlcAppIdStr && *dlcAppIdStr) {
fprintf(f, "\t\t\t\"dlcappid\"\t\t\"%s\"\n", dlcAppIdStr);
}
fprintf(f, "\t\t}\n");
token = strtok(NULL, ",");
}
fprintf(f, "\t}\n");
} else {
fprintf(f, "\t\"InstalledDepots\"\n\t{\n\t}\n");
}
// Write InstallScripts from WN_STEAM_INSTALL_SCRIPTS env var.
// Format: depotId:scriptFilename,...
if (installScriptsEnv && *installScriptsEnv) {
fprintf(f, "\t\"InstallScripts\"\n\t{\n");
std::vector<char> isbuf(strlen(installScriptsEnv) + 1);
memcpy(isbuf.data(), installScriptsEnv, isbuf.size());
char* istoken = strtok(isbuf.data(), ",");
while (istoken) {
char* iscolon = strchr(istoken, ':');
if (!iscolon) { istoken = strtok(NULL, ","); continue; }
*iscolon = '\0';
std::string scriptEsc = vdf_escape(iscolon + 1);
fprintf(f, "\t\t\"%s\"\t\t\"%s\"\n", istoken, scriptEsc.c_str());
istoken = strtok(NULL, ",");
}
fprintf(f, "\t}\n");
}
// Write SharedDepots from WN_STEAM_SHARED_DEPOTS env var.
// Format: sourceDepotId:targetAppId,...
if (sharedEnv && *sharedEnv) {
fprintf(f, "\t\"SharedDepots\"\n\t{\n");
std::vector<char> sbuf(strlen(sharedEnv) + 1);
memcpy(sbuf.data(), sharedEnv, sbuf.size());
char* stoken = strtok(sbuf.data(), ",");
while (stoken) {
char* scolon = strchr(stoken, ':');
if (!scolon) { stoken = strtok(NULL, ","); continue; }
*scolon = '\0';
fprintf(f, "\t\t\"%s\"\t\t\"%s\"\n", stoken, scolon + 1);
stoken = strtok(NULL, ",");
}
fprintf(f, "\t}\n");
}
fprintf(f,
"\t\"UserConfig\"\n"
"\t{\n"
"\t\t\"language\"\t\t\"%s\"\n"
"\t}\n"
"\t\"MountedConfig\"\n"
"\t{\n"
"\t\t\"language\"\t\t\"%s\"\n"
"\t}\n"
"}\n",
appId, installdir, installdir,
(owner && *owner) ? owner : "0");
languageEsc.c_str(), languageEsc.c_str());
fclose(f);
log_line("[wn-launcher] app manifest staged: %s (installdir=\"%s\", StateFlags=4)",
acf, installdir);
log_line("[wn-launcher] app manifest staged: %s (installdir=\"%s\", "
"depots=%s shared=%s scripts=%s)",
acf, installdir,
depotsEnv && *depotsEnv ? depotsEnv : "(none)",
sharedEnv && *sharedEnv ? sharedEnv : "(none)",
installScriptsEnv && *installScriptsEnv ? installScriptsEnv : "(none)");
}

// Counts running game processes (matches LaunchApp's canonical name or the literal
Expand Down
13 changes: 13 additions & 0 deletions app/src/main/feature/settings/other/OtherSettingsFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ import com.winlator.cmod.app.config.SettingsConfig
import com.winlator.cmod.app.update.UpdateChecker
import com.winlator.cmod.feature.shortcuts.FrontendExporter
import com.winlator.cmod.feature.setup.SetupWizardActivity
import com.winlator.cmod.feature.stores.steam.enums.Language
import com.winlator.cmod.feature.stores.steam.utils.PrefManager
import com.winlator.cmod.runtime.audio.midi.MidiManager
import com.winlator.cmod.runtime.display.environment.ImageFsInstaller
import com.winlator.cmod.shared.ui.toast.WinToast
Expand Down Expand Up @@ -116,6 +118,11 @@ class OtherSettingsFragment : Fragment() {
// AppCompatDelegate recreates attached activities automatically.
}
},
onContainerLanguageSelected = { index ->
val langName = Language.containerLangForIndex(index)
PrefManager.containerLanguage = langName
refresh()
},
onSoundFontSelected = { index ->
// Selection is display-only; no persistence in legacy code.
uiState = uiState.copy(soundFontIndex = index)
Expand Down Expand Up @@ -219,11 +226,17 @@ class OtherSettingsFragment : Fragment() {
ctx,
)

// Game container language
val containerLanguageLabels = Language.displayLabels()
val containerLanguageIndex = Language.indexForContainerLang(PrefManager.containerLanguage)

uiState =
OtherSettingsState(
checkForUpdates = preferences.getBoolean("check_for_updates", false),
languageLabels = languageLabels,
languageIndex = languageIndex,
containerLanguageLabels = containerLanguageLabels,
containerLanguageIndex = containerLanguageIndex,
soundFontFiles = soundFontFiles,
soundFontIndex = soundFontIndex,
winlatorPath = winlatorPath,
Expand Down
14 changes: 14 additions & 0 deletions app/src/main/feature/settings/other/OtherSettingsScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ data class OtherSettingsState(
val checkForUpdates: Boolean = true,
val languageLabels: List<String> = emptyList(),
val languageIndex: Int = 0,
val containerLanguageLabels: List<String> = emptyList(),
val containerLanguageIndex: Int = 0,
val soundFontFiles: List<String> = emptyList(),
val soundFontIndex: Int = 0,
val winlatorPath: String = "",
Expand Down Expand Up @@ -141,6 +143,7 @@ fun OtherSettingsScreen(
onCheckForUpdatesChanged: (Boolean) -> Unit,
onCheckForUpdatesNow: () -> Unit,
onLanguageSelected: (Int) -> Unit,
onContainerLanguageSelected: (Int) -> Unit,
onSoundFontSelected: (Int) -> Unit,
onInstallSoundFont: () -> Unit,
onRemoveSoundFont: () -> Unit,
Expand Down Expand Up @@ -217,6 +220,17 @@ fun OtherSettingsScreen(
)
}

item(key = "game_language_card") {
SettingsDropdownCard(
title = stringResource(R.string.settings_other_game_language_title),
subtitle = stringResource(R.string.settings_other_game_language_summary),
icon = Icons.Outlined.Language,
options = state.containerLanguageLabels,
selectedIndex = state.containerLanguageIndex,
onOptionSelected = onContainerLanguageSelected,
)
}

item(key = "audio_section") {
SectionLabel(stringResource(R.string.settings_audio_sound), modifier = Modifier.padding(top = 8.dp))
}
Expand Down
70 changes: 44 additions & 26 deletions app/src/main/feature/stores/steam/enums/Language.kt
Original file line number Diff line number Diff line change
@@ -1,35 +1,53 @@
package com.winlator.cmod.feature.stores.steam.enums
import timber.log.Timber

enum class Language {
english,
german,
french,
italian,
koreana,
spanish,
schinese,
sc_schinese,
tchinese,
russian,
japanese,
polish,
brazilian,
latam,
vietnamese,
portuguese,
danish,
dutch,
swedish,
norwegian,
finnish,
turkish,
thai,
czech,
unknown,
enum class Language(val displayName: String) {
english("English"),
german("German"),
french("French"),
italian("Italian"),
koreana("Korean"),
spanish("Spanish"),
schinese("Simplified Chinese"),
sc_schinese("Simplified Chinese (SC)"),
tchinese("Traditional Chinese"),
russian("Russian"),
japanese("Japanese"),
polish("Polish"),
brazilian("Portuguese (Brazil)"),
latam("Spanish (Latin America)"),
vietnamese("Vietnamese"),
portuguese("Portuguese"),
danish("Danish"),
dutch("Dutch"),
swedish("Swedish"),
norwegian("Norwegian"),
finnish("Finnish"),
turkish("Turkish"),
thai("Thai"),
czech("Czech"),
unknown("Unknown"),
;

companion object {
/** All display names in enum order, excluding [unknown] and the non-standard [sc_schinese] duplicate. */
fun displayLabels(): List<String> =
entries.filter { it != unknown && it != sc_schinese }.map { it.displayName }

/** Index of the entry whose [name] matches [containerLang], or the index of [english] as fallback. */
fun indexForContainerLang(containerLang: String?): Int {
val code = containerLang?.lowercase() ?: return 0
val filtered = entries.filter { it != unknown && it != sc_schinese }
val match = filtered.indexOfFirst { it.name == code }
return if (match >= 0) match else 0
}

/** Container language enum name for a given display-name [index] (0-based into the filtered list). */
fun containerLangForIndex(index: Int): String {
val filtered = entries.filter { it != unknown && it != sc_schinese }
return filtered.getOrNull(index)?.name ?: english.name
}

fun from(keyValue: String?): Language =
when (keyValue?.lowercase()) {
english.name -> {
Expand Down
4 changes: 3 additions & 1 deletion app/src/main/feature/stores/steam/service/SteamService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -1829,6 +1829,7 @@ class SteamService : Service() {
for (dlcApp in indirectDlcApps) {
val entitledDlcDepotIds = getEntitledDepotIds(dlcApp.packageId)
for ((depotId, depot) in dlcApp.depots) {

if (isDepotEntitled(depotId, depot, entitledDlcDepotIds) &&
filterForDownloadableDepots(depot, has64Bit, preferredLanguage, null)
) {
Expand All @@ -1844,7 +1845,7 @@ class SteamService : Service() {
language = depot.language,
lowViolence = depot.lowViolence,
manifests = depot.manifests,
encryptedManifests = depot.encryptedManifests,
encryptedManifests = depot.encryptedManifests
)
}
}
Expand Down Expand Up @@ -4477,6 +4478,7 @@ class SteamService : Service() {
.distinct(),
)
}
mainAppDlcIds.addAll(calculatedDlcAppIds)

runBlocking(Dispatchers.IO) {
if (mainAppDepots.isNotEmpty()) {
Expand Down
Loading