Skip to content
Merged
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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
11 changes: 2 additions & 9 deletions README.de.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ Spiels eingreift.
- Ein eigener PlayerPrefs-Interceptor leitet Spieleinstellungen in profilspezifischen Speicher um
- Kontowechsel per Klick im Launcher, Profile sind plattformübergreifend portabel
- **Spielverbesserungen** durch den Rust-Mod
- Tastenkürzel: ESC zum Einsammeln von Belohnungen, LEERTASTE zum Angreifen, Abbauen oder Warpen
- Konfigurierbare Tastenkürzel mit Konflikterkennung gegen Spiel-Keybindings
- Einstellbare UI-Skalierung (50-200%), wird live auf das laufende Spiel angewendet
- Automatisches Öffnen der Chat-Sidebar beim Spielstart
- Automatisches Aufklappen des Auftragsqueue-Panels beim Spielstart
Expand Down Expand Up @@ -52,8 +52,7 @@ Nach der Installation starte Project Daystrom und klicke auf den Play-Button, um

Dieses Projekt wurde ursprünglich durch den [STFC Community Mod](https://github.com/netniV/stfc-mod) von
[netniV](https://github.com/netniV), [tashcan](https://github.com/tashcan) und weiteren Mitwirkenden inspiriert.
Der alte C++-Mod-Code liegt als Referenz in `stfc-mod/`. Daystrom nutzt inzwischen einen eigenen Rust-basierten
Mod mit eigener Hook-Engine und Profilsystem.
Daystrom nutzt inzwischen einen eigenen Rust-basierten Mod mit eigener Hook-Engine und Profilsystem.

## Gebaut mit

Expand All @@ -78,7 +77,6 @@ project-daystrom/
│ ├── src/hooks/ # IL2CPP-Hook-Implementierungen
│ ├── src/il2cpp/ # IL2CPP-Laufzeitumgebungs-Bindings
│ └── Cargo.toml # Crate-Konfiguration
├── stfc-mod/ # STFC Community Mod (Legacy, als Referenz behalten)
├── app/ # Project Daystrom App (Tauri 2 + Vue 3)
│ ├── modules/
│ │ ├── app/ # Vue 3 Frontend
Expand Down Expand Up @@ -106,11 +104,6 @@ project-daystrom/
einschließlich eines **Windows SDK** (wird standardmäßig nicht mitinstalliert!)
- Rust: Standardinstallation über [rustup-init.exe](https://rustup.rs/) (Option 1 wählt die MSVC-Toolchain)

### Legacy-C++-Mod (optional)

Zum Bauen des originalen Community Mods in `stfc-mod/` werden zusätzlich
[XMake](https://xmake.io/) und [CMake](https://cmake.org/) benötigt.

## Einrichtung

```sh
Expand Down
13 changes: 3 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ with a custom hook engine (ARM64 + x86_64) that intercepts the game's IL2CPP run
- Custom PlayerPrefs interceptor redirects game settings into per-profile storage
- Switching accounts is a single click in the launcher, profiles are portable across platforms
- **Game enhancements** powered by the Rust mod
- Keyboard hotkeys: ESC to collect rewards, SPACE to engage, mine, or warp
- Configurable keyboard shortcuts with game-binding conflict detection
- Adjustable UI scale (50-200%), applied live to the running game
- Auto-open chat sidebar on game start
- Auto-expand job queue panel on game start
Expand All @@ -49,9 +49,8 @@ After installation, launch Project Daystrom and click the play button to start t
## Acknowledgements

This project was originally inspired by the [STFC Community Mod](https://github.com/netniV/stfc-mod) by
[netniV](https://github.com/netniV), [tashcan](https://github.com/tashcan), and contributors. The legacy C++ mod
code is kept in `stfc-mod/` as reference. Daystrom has since moved to its own Rust-based mod with a custom hook
engine and profile system.
[netniV](https://github.com/netniV), [tashcan](https://github.com/tashcan), and contributors. Daystrom has since
moved to its own Rust-based mod with a custom hook engine and profile system.

## Built with

Expand All @@ -76,7 +75,6 @@ project-daystrom/
│ ├── src/hooks/ # IL2CPP hook implementations
│ ├── src/il2cpp/ # IL2CPP runtime bindings
│ └── Cargo.toml # Crate config
├── stfc-mod/ # STFC Community Mod (legacy, kept as reference)
├── app/ # Project Daystrom app (Tauri 2 + Vue 3)
│ ├── modules/
│ │ ├── app/ # Vue 3 frontend
Expand Down Expand Up @@ -104,11 +102,6 @@ project-daystrom/
including a **Windows SDK** (not installed by default!)
- Rust: standard installation via [rustup-init.exe](https://rustup.rs/) (option 1 selects MSVC toolchain)

### Legacy C++ mod (optional)

Building the original Community Mod in `stfc-mod/` additionally requires
[XMake](https://xmake.io/) and [CMake](https://cmake.org/).

## Setup

```sh
Expand Down
6 changes: 3 additions & 3 deletions app/modules/app/src/commands/__tests__/settings.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ describe('settings commands', () => {

describe('getGameSettings', () => {
it('invokes the correct command without args', async () => {
const expected: GameSettings = {ui: {scale: 120, auto_open_sidebar: false}, banners: {}};
const expected: GameSettings = {ui: {scale: 120, auto_open_sidebar: false}, banners: {}, shortcuts: {}};
mockInvoke.mockResolvedValue(expected);

const {getGameSettings} = await import('../settings');
Expand All @@ -35,7 +35,7 @@ describe('settings commands', () => {
mockInvoke.mockResolvedValue(undefined);

const {setGameSettings} = await import('../settings');
const settings: GameSettings = {ui: {scale: 75, auto_open_sidebar: false}, banners: {}};
const settings: GameSettings = {ui: {scale: 75, auto_open_sidebar: false}, banners: {}, shortcuts: {}};
await setGameSettings(settings);

// The key MUST be "settings" to match the Rust parameter name.
Expand All @@ -47,7 +47,7 @@ describe('settings commands', () => {
mockInvoke.mockResolvedValue(undefined);

const {setGameSettings} = await import('../settings');
const settings: GameSettings = {ui: {scale: 100, auto_open_sidebar: false}, banners: {}};
const settings: GameSettings = {ui: {scale: 100, auto_open_sidebar: false}, banners: {}, shortcuts: {}};
await setGameSettings(settings);

const args = mockInvoke.mock.calls[0][1] as {settings: GameSettings};
Expand Down
149 changes: 148 additions & 1 deletion app/modules/app/src/components/SettingsView.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script setup lang="ts">
import {useSettings} from '@app/composables/useSettings';
import {computed} from 'vue';
import {computed, ref} from 'vue';

import bannerCategories from './toast-banner-categories.json';

Expand Down Expand Up @@ -74,6 +74,89 @@ function onShipNamesVisibleInput(event: Event) {
save();
}

// ---- Shortcut handlers ------------------------------------------------------

/** Known shortcut actions with display labels and default bindings (as event.code values). */
const shortcutActions = [
{key: 'trigger_main_action', label: 'Trigger Main Action', defaultCode: 'Space'},
];

/**
* Display labels for key codes, populated by key capture events.
* Maps event.code (e.g. "Slash") to event.key (e.g. "-" on German layout).
*/
const keyDisplayLabels: Record<string, string> = {Space: 'Space'};

/**
* Get the display label for a shortcut action (localized key name or code fallback).
*
* @param key - The action identifier.
* @param defaultCode - The default key code.
*/
function shortcutDisplayLabel(key: string, defaultCode: string): string {
const code = settings.value.shortcuts?.[key] ?? defaultCode;
return keyDisplayLabels[code] ?? code;
}

/**
* Whether a shortcut is explicitly disabled (empty string).
*
* @param key - The action identifier.
*/
function isShortcutDisabled(key: string): boolean {
return settings.value.shortcuts?.[key] === '';
}

/** The action key currently waiting for a keypress, or null if not capturing. */
const capturingKey = ref<string | null>(null);

/**
* Disable a shortcut by setting it to an empty string.
*
* @param key - The action identifier.
*/
function clearShortcut(key: string) {
const shortcuts = settings.value.shortcuts ??= {};
shortcuts[key] = '';
save();
}

/**
* Start capturing a keypress for a shortcut action.
*
* @param key - The action identifier.
*/
function startCapture(key: string) {
capturingKey.value = key;
window.addEventListener('keydown', onCaptureKey, {once: true});
}

/**
* Handle a captured keypress. Stores event.code (physical key) and caches event.key (display label).
*
* @param event - The keyboard event.
*/
function onCaptureKey(event: KeyboardEvent) {
event.preventDefault();
event.stopPropagation();
const key = capturingKey.value;
capturingKey.value = null;
if (!key) {
return;
}

if (event.code === 'Escape') {
return;
}

// Cache the display label for this physical key.
keyDisplayLabels[event.code] = event.code === 'Space' ? 'Space' : event.key;

const shortcuts = settings.value.shortcuts ??= {};
shortcuts[key] = event.code;
save();
}

// ---- Toast banner handlers --------------------------------------------------

/** Whether all banners are disabled (convenience toggle). */
Expand Down Expand Up @@ -180,6 +263,26 @@ function onBannerTypeToggle(name: string, checked: boolean) {
</div>
</section>

<section class="settings-category">
<h3>Shortcuts</h3>

<div v-for="action in shortcutActions" :key="action.key" class="setting-row">
<label>{{ action.label }}</label>
<span class="shortcut-key"
:class="{ disabled: isShortcutDisabled(action.key), capturing: capturingKey === action.key }"
tabindex="0"
@click="startCapture(action.key)">
{{ capturingKey === action.key ? '...' : isShortcutDisabled(action.key) ? '—'
: shortcutDisplayLabel(action.key, action.defaultCode) }}
</span>
<button v-if="!isShortcutDisabled(action.key) && capturingKey !== action.key"
class="clear-btn shortcut-clear" title="Disable shortcut"
@click="clearShortcut(action.key)">
</button>
</div>
</section>

<section class="settings-category">
<h3>Toast Banners</h3>

Expand Down Expand Up @@ -275,6 +378,50 @@ function onBannerTypeToggle(name: string, checked: boolean) {
font-variant-numeric: tabular-nums;
}

.shortcut-key {
font-family: monospace;
padding: 0.2rem 0.5rem;
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 0.25rem;
min-width: 4rem;
text-align: center;
cursor: pointer;
}

.shortcut-key:hover {
border-color: rgba(255, 255, 255, 0.4);
}

.shortcut-key.disabled {
opacity: 0.4;
font-style: italic;
}

.shortcut-key.capturing {
border-color: rgba(100, 180, 255, 0.6);
animation: pulse 1s ease-in-out infinite;
}

@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}

.shortcut-clear {
background: none;
border: none;
cursor: pointer;
padding: 0.25rem 0.4rem;
line-height: 1;
color: inherit;
opacity: 0.6;
font-size: 1rem;
}

.shortcut-clear:hover {
opacity: 1;
}

.banner-categories {
margin-top: 0.25rem;
}
Expand Down
2 changes: 1 addition & 1 deletion app/modules/app/src/composables/useSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const log = getLogger('Settings');

// ---- State -----------------------------------------------------------------

const settings = ref<GameSettings>({ui: {}, banners: {}});
const settings = ref<GameSettings>({ui: {}, banners: {}, shortcuts: {}});

// ---- Public API ------------------------------------------------------------

Expand Down
Loading