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
19 changes: 13 additions & 6 deletions src-tauri/src/northstar/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,11 +160,12 @@ pub fn get_northstar_version_number(game_install: GameInstall) -> Result<String,
pub fn launch_northstar(
game_install: GameInstall,
launch_options: NorthstarLaunchOptions,
launch_arguments: Vec<&str>,
) -> Result<String, String> {
dbg!(game_install.clone());

if launch_options.launch_via_steam {
return launch_northstar_steam(game_install);
return launch_northstar_steam(game_install, launch_arguments);
}

let host_os = get_host_os();
Expand All @@ -179,7 +180,7 @@ pub fn launch_northstar(
));
}

return launch_northstar_steam(game_install);
return launch_northstar_steam(game_install, launch_arguments);
}

// Only check guards if bypassing checks is not enabled
Expand Down Expand Up @@ -213,9 +214,11 @@ pub fn launch_northstar(
{
let ns_exe_path = format!("{}/NorthstarLauncher.exe", game_install.game_path);
let ns_profile_arg = format!("-profile={}", game_install.profile);
let mut arguments = vec!["/C", "start", "", &ns_exe_path, &ns_profile_arg];
arguments.extend(launch_arguments);

let mut output = std::process::Command::new("C:\\Windows\\System32\\cmd.exe")
.args(["/C", "start", "", &ns_exe_path, &ns_profile_arg])
.args(arguments)
.spawn()
.expect("failed to execute process");
output.wait().expect("failed waiting on child process");
Expand All @@ -230,7 +233,10 @@ pub fn launch_northstar(
}

/// Prepare Northstar and Launch through Steam using the Browser Protocol
pub fn launch_northstar_steam(game_install: GameInstall) -> Result<String, String> {
pub fn launch_northstar_steam(
game_install: GameInstall,
launch_arguments: Vec<&str>,
) -> Result<String, String> {
if !matches!(game_install.install_type, InstallType::STEAM) {
return Err("Titanfall2 was not installed via Steam".to_string());
}
Expand Down Expand Up @@ -266,9 +272,10 @@ pub fn launch_northstar_steam(game_install: GameInstall) -> Result<String, Strin
}

match open::that(format!(
"steam://run/{}//-profile={} --northstar/",
"steam://run/{}//-profile={} --northstar {}/",
thermite::TITANFALL2_STEAM_ID,
game_install.profile
game_install.profile,
&launch_arguments.join(" ") // Pass launch arguments to Steam
)) {
Ok(()) => Ok("Started game".to_string()),
Err(_err) => Err("Failed to launch Titanfall 2 via Steam".to_string()),
Expand Down
6 changes: 3 additions & 3 deletions src-tauri/src/platform_specific/linux.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,14 +65,14 @@ pub fn install_ns_proton() -> Result<(), String> {
/// Remove NS Proton
pub fn uninstall_ns_proton() -> Result<(), String> {
let compat_dir = get_proton_dir()?;
let pattern = format!("{}/NorthstarProton*", compat_dir);
let pattern = format!("{compat_dir}/NorthstarProton*");
for e in glob::glob(&pattern).expect("Failed to read glob pattern") {
match e {
Ok(path) => match std::fs::remove_dir_all(path.clone()) {
Ok(_) => {}
Err(_) => return Err(format!("Failed to remove {}", path.display())),
},
Err(e) => return Err(format!("Found unprocessable entry {}", e)),
Err(e) => return Err(format!("Found unprocessable entry {e}")),
}
}

Expand All @@ -82,7 +82,7 @@ pub fn uninstall_ns_proton() -> Result<(), String> {
/// Get the latest installed NS Proton version
pub fn get_local_ns_proton_version() -> Result<String, String> {
let compat_dir = get_proton_dir().unwrap();
let pattern = format!("{}/NorthstarProton*/version", compat_dir);
let pattern = format!("{compat_dir}/NorthstarProton*/version");

if let Some(e) = glob::glob(&pattern)
.expect("Failed to read glob pattern")
Expand Down
235 changes: 235 additions & 0 deletions src-vue/src/components/LaunchArgumentsSelector.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
<template>
<div>
<div :class="containerClasses">
<div class="fc-launch_arg_tag_container"
v-for="(argument, index) in arguments">
<!-- Official arguments -->
<el-tooltip
v-if="argument.i18nEntry.length !== 0"
class="box-item"
:content="$t(argument.i18nEntry)"
placement="bottom"
>
<el-check-tag
:checked="values[index]"
@change="onChange(index)"
>
{{ argument.argumentName }}
</el-check-tag>
</el-tooltip>

<!-- Custom arguments -->
<el-tag
v-else
closable
disable-transitions
@close="onClose(index, argument.argumentName)"
>
{{ argument.argumentName }}
</el-tag>
</div>

<!-- User-input tag -->
<div class="fc-launch_arg_tag_container">
<el-input
v-if="inputVisible"
ref="InputRef"
class="fc-tag__input"
v-model="inputValue"
@keyup.enter="handleInputConfirm"
@blur="handleInputConfirm"
/>
<el-button v-else class="button-new-tag ml-1 fc-tag__input" @click="showInput">
{{ $t('settings.launch_args.new_arg_btn') }}
</el-button>
</div>
</div>


<!-- Language selector -->
<div :class="containerClasses">
<el-select v-if="displayLanguageSelector"
v-model="langArgumentValue"
class="m-2 fc-launch_arg_tag_container fc-tag__input"
:placeholder="$t('settings.launch_args.select_game_language')"
@change="onLanguageSelection"
>
<el-option
v-for="item in langArgumentOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</div>
</div>
</template>

<script lang="ts">
import { defineComponent } from 'vue';
import { LaunchArgument } from '../utils/LaunchArgument';
import { NorthstarState } from '../utils/NorthstarState';
import { showErrorNotification } from '../utils/ui';
import {argumentsStoreKey} from "../main";
import { load } from '@tauri-apps/plugin-store';
const persistentStore = await load('flight-core-settings.json', { autoSave: false });
export default defineComponent({
name: 'LaunchArgumentsSelector',
computed: {
arguments(): LaunchArgument[] {
return (this.localCustomArgs.concat(this.officialArguments))
.sort((a, b) => a.argumentName.localeCompare(b.argumentName));
},
displayLanguageSelector(): boolean {
const langArgPrefix = '-language=';
return this.arguments
.map(arg => arg.argumentName)
.filter(name => name.substring(0, langArgPrefix.length) === langArgPrefix)
.length === 0;
},
officialArguments(): LaunchArgument[] {
return [
new LaunchArgument("-disablelogs", "settings.launch_args.descriptions.disablelogs"),
new LaunchArgument("-vanilla", "settings.launch_args.descriptions.vanilla"),
new LaunchArgument("-northstar", "settings.launch_args.descriptions.northstar"),
new LaunchArgument("-dedicated", "settings.launch_args.descriptions.dedicated"),
new LaunchArgument("-waitfordebugger", "settings.launch_args.descriptions.waitfordebugger"),
new LaunchArgument("-enablechathooks", "settings.launch_args.descriptions.enablechathooks"),
new LaunchArgument("-noplugins", "settings.launch_args.descriptions.noplugins"),
new LaunchArgument("-novid", "settings.launch_args.descriptions.novid"),
new LaunchArgument("-nosound", "settings.launch_args.descriptions.nosound")
];
},
containerClasses(): string {
return this.gamePathIsSelected ? 'fc-tags_container' : 'fc-tags_container disabled_container';
},
gamePathIsSelected(): boolean {
return this.$store.state.northstar_state !== NorthstarState.GAME_NOT_FOUND;
},
langArgumentOptions() {
const languages = ['english', 'french', 'german', 'italian', 'japanese', 'mspanish', 'portuguese', 'russian', 'spanish', 'tchinese'];
return languages.map(lang => ({value: lang, label: lang}));
}
},
data: () => ({
inputValue: '',
inputVisible: false,
langArgumentValue: '',
values: [] as boolean[],
localCustomArgs: [] as LaunchArgument[]
}),
methods: {
onLanguageSelection( lang: string ) {
this.createNewArgument( `-language=${lang}` );
this.langArgumentValue = '';
},
createNewArgument(arg: string) {
let allArgumentsNames: string[] = this.arguments.map(arg => arg.argumentName);
if (allArgumentsNames.indexOf(arg) !== -1) {
console.warn(`Argument "${arg}" already present, ignoring.`);
} else {
const newArgument: LaunchArgument = new LaunchArgument(arg);
this.localCustomArgs.push( newArgument );
allArgumentsNames = this.arguments.map(arg => arg.argumentName);
const index: number = allArgumentsNames.indexOf(newArgument.argumentName);
this.values.splice(index, 0, true);
this.saveLaunchArgumentsToFile();
}
},
onChange(index: number) {
this.values[index] = !this.values[index];
this.saveLaunchArgumentsToFile();
},
onClose(index: number, argumentName: string) {
// remove item state value
this.values.splice(index, 1);
// remove item from list of custom arguments
const localIndex = this.localCustomArgs.map(arg => arg.argumentName).indexOf(argumentName);
if (localIndex === -1) {
console.error(`Failed removing argument "${argumentName}".`);
return;
}
this.localCustomArgs.splice(localIndex, 1);
this.saveLaunchArgumentsToFile();
},
saveLaunchArgumentsToFile() {
const newArgs = this.arguments
.filter((_value: LaunchArgument, index: number) => {
return this.values[index];
})
.map((value: LaunchArgument) => value.argumentName);
persistentStore.set(argumentsStoreKey, newArgs)
.then(async () => {
await persistentStore.save();
console.log(`Launch arguments updated (now "${newArgs.join(' ')}").`);
})
.catch((err: any) => {
showErrorNotification(err);
});
},
showInput() {
this.inputVisible = true;
this.$nextTick(() => {
// @ts-ignore
this.$refs.InputRef.input.focus();
});
},
handleInputConfirm() {
if (this.inputValue.length !== 0) {
this.createNewArgument(this.inputValue);
}
this.inputVisible = false;
this.inputValue = '';
},
},
async mounted() {
this.values = this.arguments.map(_a => false);
const fileArgs: string[] | null | undefined = await persistentStore.get(argumentsStoreKey);
if (fileArgs === null || fileArgs == undefined) {
console.log('No local launch arguments to load, exiting.');
return;
}
// Only add to local arguments those who are not in official arguments array
this.localCustomArgs = fileArgs
.filter(arg => this.officialArguments.map(oArg => oArg.argumentName).indexOf(arg) === -1)
.map(arg => new LaunchArgument(arg));
this.arguments.forEach((argument, index) => {
if (fileArgs.includes(argument.argumentName)) {
this.values[index] = true;
}
});
}
});
</script>

<style scoped>
.fc-launch_arg_tag_container {
display: inline-block;
margin: 0 8px 8px 8px;
}
.fc-tags_container {
transform: translateX(-8px);
}
.fc-tags_container .el-select {
min-width: 222px;
}
.el-tag {
background-color: var(--el-color-primary-light-8);
color: var(--el-color-primary);
font-size: var(--el-font-size-base);
line-height: var(--el-font-size-base);
padding: 13px 15px;
vertical-align: initial;
transition: var(--el-transition-all);
font-weight: 700;
}
.disabled_container {
pointer-events: none;
filter: grayscale(1);
}
.fc-tag__input {
width: auto;
height: 28px;
--el-component-size: 28px;
}
</style>
17 changes: 17 additions & 0 deletions src-vue/src/i18n/lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,23 @@
"reinstall_text": "Please wait",
"reinstall_success": "Successfully reinstalled Northstar"
}
},

"launch_args": {
"title": "Launch arguments",
"new_arg_btn": "+ New launch argument",
"select_game_language": "Select game language",
"descriptions": {
"disablelogs": "Disable logging and creation of log files",
"vanilla": "Disables Northstar loading",
"northstar": "Enables Northstar loading",
"dedicated": "Starts a dedicated server without video output",
"waitfordebugger": "Waits for debugger to connect before launching game",
"enablechathooks": "Enables the use of chathooks for use by mods",
"noplugins": "Disables the plugin system",
"novid": "Disables startup videos",
"nosound": "Disables all game sounds"
}
}
},

Expand Down
19 changes: 18 additions & 1 deletion src-vue/src/i18n/lang/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,24 @@
"new_profile_name": "Entrez le nouveau nom du profil"
}
},
"show_nsfw_mods": "Montrer les mods NSFW de Thunderstore"
"show_nsfw_mods": "Montrer les mods NSFW de Thunderstore",

"launch_args": {
"title": "Paramètres de démarrage",
"new_arg_btn": "+ Nouveau paramètre",
"select_game_language": "Langage du jeu",
"descriptions": {
"disablelogs": "Désactive la journalisation et la création de journaux",
"vanilla": "Désactive le chargement de Northstar",
"northstar": "Active le chargement de Northstar",
"dedicated": "Démarre un serveur dédié sans sortie graphique",
"waitfordebugger": "Attend la connexion au débuggueur avant de lancer le jeu",
"enablechathooks": "Active les chathooks pour les mods",
"noplugins": "Désactive le système de plugins",
"novid": "Désactive les vidéos au démarrage",
"nosound": "Désactive tous les sons du jeu"
}
}
},
"notification": {
"date_prefix": "à",
Expand Down
2 changes: 2 additions & 0 deletions src-vue/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,5 +64,7 @@ export const router = createRouter({
});
app.use(router);

// Store keys
export const argumentsStoreKey = 'launch_arguments';

app.mount('#app')
Loading
Loading