| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653 |
- using System;
- using System.Collections.Generic;
- using System.Collections.ObjectModel;
- using System.IO;
- using System.Linq;
- using System.Runtime.InteropServices;
- using System.Text.Json;
- using System.Diagnostics;
- using System.Text.RegularExpressions;
- using System.Threading;
- using System.Threading.Tasks;
- using CommunityToolkit.Mvvm.ComponentModel;
- using CommunityToolkit.Mvvm.Input;
- using SlamCross.Models;
- using SlamCross.Services;
- namespace SlamCross.ViewModels;
- public partial class TrackItem : ObservableObject
- {
- [ObservableProperty] private bool _isPlaying;
- public string Name { get; set; } = "";
- public string Hotkey { get; set; } = "";
- public string Volume { get; set; } = "";
- public string Trimmed { get; set; } = "";
- public string Tags { get; set; } = "";
- public int Index { get; set; }
- }
- public partial class MainWindowViewModel : ViewModelBase
- {
- private readonly SettingsService _settings = new();
- private readonly IAudioService _audio = AudioServiceFactory.Create();
- private SourceGame _game = null!;
- private string? _steamAppsPath;
- private CancellationTokenSource? _pollCts;
- [ObservableProperty] private ObservableCollection<TrackItem> _tracks = new();
- [ObservableProperty] private ObservableCollection<AudioDevice> _outputDevices = new();
- [ObservableProperty] private ObservableCollection<AudioDevice> _inputDevices = new();
- [ObservableProperty] private AudioDevice? _selectedVirtualInCable;
- [ObservableProperty] private AudioDevice? _selectedVirtualOutCable;
- [ObservableProperty] private AudioDevice? _selectedRegularMic;
- [ObservableProperty] private string _statusText = "Status: Idle.";
- [ObservableProperty] private bool _isRunning;
- [ObservableProperty] private bool _interfaceEnabled = true;
- [ObservableProperty] private double _progress;
- [ObservableProperty] private double _progressMax = 1;
- [ObservableProperty] private string _startButtonText = "Start";
- [ObservableProperty] private string _playKeyButtonText = "Play key: (change)";
- [ObservableProperty] private bool _isStreaming;
- [ObservableProperty] private string _streamButtonText = "Stream to Mic";
- [ObservableProperty] private string _streamStatusText = "";
- [ObservableProperty] private int _globalVolume = 50;
- public AppSettings Settings => _settings.Settings;
- public MainWindowViewModel()
- {
- _settings.Load();
- _globalVolume = Settings.GlobalVolume;
- InitGame();
- LoadAudioDevices();
- RefreshTracks();
- RefreshPlayKey();
- if (Settings.StartEnabled)
- _ = StartPollAsync();
- }
- partial void OnGlobalVolumeChanged(int value)
- {
- Settings.GlobalVolume = value;
- _settings.Save();
- }
- private void InitGame()
- {
- var sep = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? '\\' : '/';
- _game = new SourceGame
- {
- Name = "Counter-Strike 2",
- Id = 730,
- Directory = $"common{sep}Counter-Strike Global Offensive{sep}",
- ToCfg = $"game{sep}csgo{sep}cfg{sep}",
- LibraryName = $"game{sep}csgo{sep}",
- ExeName = "cs2",
- SampleRate = 22050,
- VoiceFadeOut = false
- };
- _game.Blacklist.AddRange(new[]
- {
- "attack","attack2","autobuy","back","buy","buyammo1","buyammo2","buymenu","callvote",
- "cancelselect","cheer","compliment","coverme","drop","duck","enemydown","enemyspot",
- "fallback","followme","forward","getout","go","holdpos","inposition","invnext","invprev",
- "jump","lastinv","messagemode","messagemode2","moveleft","moveright","mute","negative",
- "quit","radio1","radio2","radio3","rebuy","regroup","reload","reporting_in",
- "reporting_in_team_only","roger","sectorclear","showscores","slot1","slot10","slot2",
- "slot3","slot4","slot5","slot6","slot7","slot8","slot9","speed","sticktog","takepoint",
- "takingfire","teammenu","thanks","toggleconsole","use","voicerecord",
- "say","say_team","exec","echo","alias","bind","unbind","unbindall","kill","explode",
- "disconnect","retry","status","map","volume","stop","play","record","screenshot",
- "host_writeconfig","voice_loopback","con_enable","clear","help","find"
- });
- }
- private void LoadAudioDevices()
- {
- OutputDevices.Clear();
- InputDevices.Clear();
- foreach (var d in _audio.GetOutputDevices())
- OutputDevices.Add(d);
- foreach (var d in _audio.GetInputDevices())
- InputDevices.Add(d);
- if (Settings.VirtualMicInDeviceIndex >= 0 && Settings.VirtualMicInDeviceIndex < OutputDevices.Count)
- SelectedVirtualInCable = OutputDevices[Settings.VirtualMicInDeviceIndex];
- if (Settings.VirtualMicOutDeviceIndex >= 0 && Settings.VirtualMicOutDeviceIndex < InputDevices.Count)
- SelectedVirtualOutCable = InputDevices[Settings.VirtualMicOutDeviceIndex];
- if (Settings.RegularMicDeviceIndex >= 0 && Settings.RegularMicDeviceIndex < InputDevices.Count)
- SelectedRegularMic = InputDevices[Settings.RegularMicDeviceIndex];
- }
- partial void OnSelectedVirtualInCableChanged(AudioDevice? value)
- {
- if (value == null) return;
- Settings.VirtualMicInDeviceIndex = value.Index;
- Settings.VirtualMicInDeviceName = value.Name;
- Settings.VirtualMicInPactlId = value.PactlId;
- _settings.Save();
- }
- partial void OnSelectedVirtualOutCableChanged(AudioDevice? value)
- {
- if (value == null) return;
- Settings.VirtualMicOutDeviceIndex = value.Index;
- Settings.VirtualMicOutDeviceName = value.Name;
- Settings.VirtualMicOutPactlId = value.PactlId;
- _settings.Save();
- }
- partial void OnSelectedRegularMicChanged(AudioDevice? value)
- {
- if (value == null) return;
- Settings.RegularMicDeviceIndex = value.Index;
- Settings.RegularMicDeviceName = value.Name;
- Settings.RegularMicPactlId = value.PactlId;
- _settings.Save();
- }
- public void RefreshTracks()
- {
- if (!Directory.Exists(_game.LibraryName))
- Directory.CreateDirectory(_game.LibraryName);
- _game.Tracks.Clear();
- foreach (var f in Directory.GetFiles(_game.LibraryName))
- {
- if (Path.GetExtension(f) == _game.FileExtension)
- _game.Tracks.Add(new Track { Name = Path.GetFileNameWithoutExtension(f) });
- }
- CreateTags(_game);
- TrackSettingsService.Load(_game);
- TrackSettingsService.Save(_game);
- RebuildTrackList();
- }
- private void RebuildTrackList()
- {
- Tracks.Clear();
- for (int i = 0; i < _game.Tracks.Count; i++)
- {
- var t = _game.Tracks[i];
- Tracks.Add(new TrackItem
- {
- Index = i,
- Name = t.Name,
- Hotkey = t.Hotkey,
- Volume = t.Volume + "%",
- Trimmed = t.EndPos > 0 ? "Yes" : "",
- Tags = t.Tags.Count > 0 ? "\"" + string.Join("\", \"", t.Tags) + "\"" : ""
- });
- }
- }
- public async Task StartPollAsync()
- {
- IsRunning = true;
- StartButtonText = "Stop";
- InterfaceEnabled = false;
- _pollCts = new CancellationTokenSource();
- try { await Task.Run(() => PollLoop(_pollCts.Token)); }
- catch (OperationCanceledException) { }
- catch (Exception ex) { ShowError(ex.Message); }
- finally
- {
- IsRunning = false;
- StartButtonText = "Start";
- InterfaceEnabled = true;
- StatusText = "Status: Idle.";
- RebuildTrackList();
- }
- }
- public void StopPoll()
- {
- _pollCts?.Cancel();
- _audio.Stop();
- }
- private void PollLoop(CancellationToken ct)
- {
- StatusText = "Status: Searching...";
- if (!Settings.OverrideFolders)
- {
- while (!ct.IsCancellationRequested)
- {
- _steamAppsPath = GameLocatorService.FindSteamAppsPath(_game.ExeName);
- if (!string.IsNullOrEmpty(_steamAppsPath) && Directory.Exists(_steamAppsPath))
- break;
- Thread.Sleep(_game.PollInterval);
- }
- }
- else
- {
- _steamAppsPath = Settings.SteamAppsOverride;
- if (!Directory.Exists(_steamAppsPath))
- throw new Exception("Steam apps folder does not exist. Check Settings.");
- }
- ct.ThrowIfCancellationRequested();
- CfgService.WriteCfgFiles(_game, _steamAppsPath!, _settings.Settings);
- StatusText = "Status: Working.";
- var virtualMicOutPactl = string.IsNullOrEmpty(Settings.VirtualMicOutPactlId) ? "VirtualMic.monitor" : Settings.VirtualMicOutPactlId;
- var cs2SrcOutputIds = FindCs2SourceOutputIds();
- DebugLog($"CS2 pactl source-outputs: [{string.Join(",", cs2SrcOutputIds)}] moving to {virtualMicOutPactl}");
- MoveCs2SourceOutputs(cs2SrcOutputIds, virtualMicOutPactl);
- var userDataDir = Settings.OverrideFolders && !string.IsNullOrEmpty(Settings.UserDataOverride)
- ? Settings.UserDataOverride
- : FindUserDataDir(_steamAppsPath!);
- var startTime = DateTime.UtcNow;
- var seenMtimes = new Dictionary<string, DateTime>();
- // seed from the newest existing vcfg so a stale bind from a previous session doesn't fire
- string? lastRelayValue = null;
- if (userDataDir != null && Directory.Exists(userDataDir))
- {
- var newest = FindKeyVcfgs(userDataDir).OrderByDescending(File.GetLastWriteTimeUtc).FirstOrDefault();
- if (newest != null)
- try { lastRelayValue = ParseRelayCommand(File.ReadAllText(newest), Settings.RelayKey); } catch { }
- }
- DebugLog($"SLAM started. userDataDir={userDataDir ?? "null"} startTime={startTime:O} lastRelayValue={lastRelayValue}");
- while (!ct.IsCancellationRequested)
- {
- try
- {
- if (userDataDir != null && Directory.Exists(userDataDir))
- {
- foreach (var vcfg in FindKeyVcfgs(userDataDir))
- {
- var mtime = File.GetLastWriteTimeUtc(vcfg);
- if (mtime <= startTime) continue;
- if (seenMtimes.TryGetValue(vcfg, out var lastSeen) && mtime <= lastSeen) continue;
- seenMtimes[vcfg] = mtime;
- DebugLog($"New vcfg: {vcfg} mtime={mtime:O}");
- var content = File.ReadAllText(vcfg);
- var command = ParseRelayCommand(content, Settings.RelayKey);
- DebugLog($" relayKey={Settings.RelayKey} command={command}");
- // unchanged bind value means CS2 rewrote the file on its own (round start, buy menu)
- if (command == lastRelayValue) continue;
- lastRelayValue = command;
- int trackNum = ParseTrackNumber(command);
- if (trackNum > 0)
- {
- var idx = trackNum - 1;
- if (idx >= 0 && idx < _game.Tracks.Count)
- {
- // CS2 voice stream (SDL Application, media.role=game) only exists while +voicerecord is active.
- // host_writeconfig runs before +voicerecord, so the stream may not exist yet - poll briefly.
- var freshSrcIds = FindCs2SourceOutputIds();
- if (freshSrcIds.Count == 0) { Thread.Sleep(150); freshSrcIds = FindCs2SourceOutputIds(); }
- if (freshSrcIds.Count == 0) { Thread.Sleep(150); freshSrcIds = FindCs2SourceOutputIds(); }
- DebugLog($" relay: game-capture source-outputs [{string.Join(",", freshSrcIds)}] -> {virtualMicOutPactl}");
- MoveCs2SourceOutputs(freshSrcIds, virtualMicOutPactl);
- PlayTrack(idx);
- Avalonia.Threading.Dispatcher.UIThread.Post(() =>
- {
- foreach (var ti in Tracks) ti.IsPlaying = false;
- if (idx < Tracks.Count) Tracks[idx].IsPlaying = true;
- });
- }
- // Don't delete remote/ files - they are Steam Cloud data CS2 relies on.
- // seenMtimes prevents re-processing until the next host_writeconfig.
- if (!vcfg.Contains("/remote/"))
- {
- try { File.Delete(vcfg); seenMtimes.Remove(vcfg); } catch { }
- }
- }
- }
- }
- }
- catch (Exception ex) when (ex.HResult != -2147024864)
- {
- LogError(ex);
- }
- Thread.Sleep(_game.PollInterval);
- }
- _audio.Stop();
- var regularMicPactl = string.IsNullOrEmpty(Settings.RegularMicPactlId) ? "@DEFAULT_SOURCE@" : Settings.RegularMicPactlId;
- MoveCs2SourceOutputs(FindCs2SourceOutputIds(), regularMicPactl);
- if (!string.IsNullOrEmpty(_steamAppsPath))
- CfgService.DeleteCfgFiles(_game, _steamAppsPath);
- }
- private static List<int> FindCs2SourceOutputIds()
- {
- var ids = new List<int>();
- if (!RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) return ids;
- try
- {
- var psi = new ProcessStartInfo("pactl") { Arguments = "--format=json list source-outputs", UseShellExecute = false, RedirectStandardOutput = true, CreateNoWindow = true };
- using var proc = Process.Start(psi)!;
- var json = proc.StandardOutput.ReadToEnd();
- proc.WaitForExit();
- using var doc = JsonDocument.Parse(json);
- foreach (var so in doc.RootElement.EnumerateArray())
- {
- if (!so.TryGetProperty("properties", out var props)) continue;
- var mediaRole = props.TryGetProperty("media.role", out var r) ? r.GetString() : null;
- var mediaCategory = props.TryGetProperty("media.category", out var c) ? c.GetString() : null;
- var binary = props.TryGetProperty("application.process.binary", out var b) ? b.GetString() : null;
- // CS2 voice capture appears as "SDL Application" with media.role=game, media.category=Capture
- if ((mediaRole == "game" && mediaCategory == "Capture") || binary == "cs2")
- if (so.TryGetProperty("index", out var idx)) ids.Add(idx.GetInt32());
- }
- }
- catch (Exception ex) { DebugLog($"FindCs2SourceOutputIds error: {ex.Message}"); }
- return ids;
- }
- private static void MoveCs2SourceOutputs(IEnumerable<int> sourceOutputIds, string pactlSourceName)
- {
- if (!RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) return;
- foreach (var id in sourceOutputIds)
- {
- try
- {
- var args = $"move-source-output {id} {pactlSourceName}";
- var psi = new ProcessStartInfo("pactl", args) { UseShellExecute = false, CreateNoWindow = true };
- Process.Start(psi)?.WaitForExit();
- DebugLog($"pactl {args}");
- }
- catch { }
- }
- }
- private static string? FindUserDataDir(string steamAppsPath)
- {
- var steamDir = Path.GetDirectoryName(steamAppsPath);
- if (steamDir == null) return null;
- var candidate = Path.Combine(steamDir, "userdata");
- if (Directory.Exists(candidate)) return candidate;
- if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
- {
- var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
- candidate = Path.Combine(home, ".local/share/Steam/userdata");
- if (Directory.Exists(candidate)) return candidate;
- }
- return null;
- }
- private List<string> FindKeyVcfgs(string userDataDir)
- {
- // CS2 writes key bindings to remote/ (Steam Cloud) and optionally local/cfg/
- var candidates = new List<string>();
- foreach (var userDir in Directory.GetDirectories(userDataDir))
- {
- var gameUserDir = Path.Combine(userDir, _game.Id.ToString());
- if (!Directory.Exists(gameUserDir)) continue;
- var remoteKeys = Path.Combine(gameUserDir, "remote", "cs2_user_keys.vcfg");
- if (File.Exists(remoteKeys)) candidates.Add(remoteKeys);
- var localCfg = Path.Combine(gameUserDir, "local", "cfg");
- if (Directory.Exists(localCfg))
- candidates.AddRange(Directory.GetFiles(localCfg, "*keys*.vcfg"));
- }
- return candidates;
- }
- private static string ParseRelayCommand(string cfgContent, string relayKey)
- {
- var pattern = $"\"{Regex.Escape(relayKey)}\"\\s+\"(.*?)\"";
- var m = Regex.Match(cfgContent, pattern, RegexOptions.IgnoreCase);
- return m.Success ? m.Groups[1].Value : "";
- }
- private static int ParseTrackNumber(string relayValue)
- {
- var m = Regex.Match(relayValue, @"^slam_r_(\d+)_[ab]$");
- if (m.Success) return int.Parse(m.Groups[1].Value);
- return int.TryParse(relayValue, out var n) ? n : 0; // plain number from an old slam.cfg
- }
- public void PlayTrack(int index)
- {
- DebugLog($"PlayTrack({index}) trackCount={_game.Tracks.Count}");
- if (index < 0 || index >= _game.Tracks.Count) { DebugLog(" -> index out of range"); return; }
- var track = _game.Tracks[index];
- var filePath = Path.Combine(_game.LibraryName, track.Name + _game.FileExtension);
- DebugLog($" filePath={filePath} exists={File.Exists(filePath)} globalVol={Settings.GlobalVolume} trackVol={track.Volume} pactlId={Settings.VirtualMicInPactlId}");
- if (!File.Exists(filePath)) { DebugLog(" -> file not found"); return; }
- try
- {
- float volume = (Settings.GlobalVolume / 100f) * (track.Volume / 100f);
- _audio.Play(filePath, Settings.VirtualMicInDeviceIndex, volume, track.StartPos, track.EndPos, Settings.VirtualMicInPactlId);
- DebugLog(" -> audio.Play called");
- if (!string.IsNullOrEmpty(_steamAppsPath))
- CfgService.WriteTrackCfgs(_game, _steamAppsPath, track);
- }
- catch (Exception ex)
- {
- LogError(ex);
- ShowError("Playback failed: " + ex.Message);
- }
- }
- public void StreamToMic(int index)
- {
- if (index < 0 || index >= _game.Tracks.Count) return;
- var track = _game.Tracks[index];
- var filePath = Path.Combine(_game.LibraryName, track.Name + _game.FileExtension);
- if (!File.Exists(filePath)) return;
- try
- {
- _audio.Play(filePath, Settings.VirtualMicInDeviceIndex, (Settings.GlobalVolume / 100f) * (track.Volume / 100f), track.StartPos, track.EndPos, Settings.VirtualMicInPactlId);
- }
- catch (Exception ex)
- {
- LogError(ex);
- ShowError("Playback failed: " + ex.Message);
- return;
- }
- IsStreaming = true;
- StreamButtonText = "Stop Stream";
- StreamStatusText = $"Streaming: {track.Name}";
- foreach (var ti in Tracks) ti.IsPlaying = false;
- if (index < Tracks.Count) Tracks[index].IsPlaying = true;
- }
- public void StopStream()
- {
- _audio.Stop();
- IsStreaming = false;
- StreamButtonText = "Stream to Mic";
- StreamStatusText = "";
- foreach (var ti in Tracks) ti.IsPlaying = false;
- }
- public async Task ImportFilesAsync(IEnumerable<string> files, bool deleteSource = false)
- {
- var fileList = files.ToList();
- InterfaceEnabled = false;
- ProgressMax = fileList.Count;
- Progress = 0;
- var failed = new List<string>();
- await Task.Run(() =>
- {
- foreach (var file in fileList)
- {
- try
- {
- var outFile = Path.Combine(_game.LibraryName,
- Path.GetFileNameWithoutExtension(file) + ".wav");
- ConversionService.ConvertToWav(file, outFile, _game.SampleRate, _game.Channels);
- if (deleteSource) File.Delete(file);
- }
- catch (Exception ex)
- {
- LogError(ex);
- failed.Add(Path.GetFileName(file));
- }
- Avalonia.Threading.Dispatcher.UIThread.Post(() => Progress++);
- }
- });
- Progress = 0;
- RefreshTracks();
- InterfaceEnabled = true;
- if (failed.Count > 0)
- ShowError("Conversion complete! Failed: " + string.Join(", ", failed));
- }
- public void DeleteTracks(IEnumerable<int> indices)
- {
- foreach (var idx in indices.OrderByDescending(x => x))
- {
- var t = _game.Tracks[idx];
- var path = Path.Combine(_game.LibraryName, t.Name + _game.FileExtension);
- if (File.Exists(path)) try { File.Delete(path); } catch { }
- }
- RefreshTracks();
- }
- public void SetTrackVolume(IEnumerable<int> indices, int volume)
- {
- foreach (var idx in indices)
- if (idx < _game.Tracks.Count)
- _game.Tracks[idx].Volume = volume;
- TrackSettingsService.Save(_game);
- RefreshTracks();
- }
- public bool SetTrackHotkey(int index, string key)
- {
- if (index < 0 || index >= _game.Tracks.Count) return false;
- if (key.Equals(Settings.PlayKey, StringComparison.OrdinalIgnoreCase) ||
- key.Equals(Settings.RelayKey, StringComparison.OrdinalIgnoreCase))
- { ShowError($"\"{key}\" is used as the play/relay key!"); return false; }
- bool inUse = _game.Tracks.Where((_, i) => i != index).Any(t => t.Hotkey == key);
- if (inUse) { ShowError($"\"{key}\" is already assigned!"); return false; }
- _game.Tracks[index].Hotkey = key;
- TrackSettingsService.Save(_game);
- RefreshTracks();
- return true;
- }
- public void RemoveHotkey(int index)
- {
- if (index < 0 || index >= _game.Tracks.Count) return;
- _game.Tracks[index].Hotkey = "";
- TrackSettingsService.Save(_game);
- RefreshTracks();
- }
- public void SetTrackTrim(int index, int startPos, int endPos)
- {
- if (index < 0 || index >= _game.Tracks.Count) return;
- _game.Tracks[index].StartPos = startPos;
- _game.Tracks[index].EndPos = endPos;
- TrackSettingsService.Save(_game);
- RefreshTracks();
- }
- public void RenameTrack(int index, string newName)
- {
- if (index < 0 || index >= _game.Tracks.Count) return;
- var track = _game.Tracks[index];
- var oldPath = Path.Combine(_game.LibraryName, track.Name + _game.FileExtension);
- var newPath = Path.Combine(_game.LibraryName, newName + _game.FileExtension);
- if (File.Exists(newPath)) { ShowError("A track with that name already exists."); return; }
- try { File.Move(oldPath, newPath); }
- catch (Exception ex) { ShowError("Rename failed: " + ex.Message); return; }
- track.Name = newName;
- TrackSettingsService.Save(_game);
- RefreshTracks();
- }
- public string GetTrackFilePath(int index)
- {
- if (index < 0 || index >= _game.Tracks.Count) return "";
- return Path.Combine(_game.LibraryName, _game.Tracks[index].Name + _game.FileExtension);
- }
- public Track? GetTrack(int index)
- => index >= 0 && index < _game.Tracks.Count ? _game.Tracks[index] : null;
- public void SetPlayKey(string key)
- {
- if (key == Settings.RelayKey) { ShowError("Play key and relay key cannot be the same!"); return; }
- Settings.PlayKey = key;
- _settings.Save();
- PlayKeyButtonText = $"Play key: \"{Settings.PlayKey}\" (change)";
- }
- private void RefreshPlayKey()
- {
- PlayKeyButtonText = $"Play key: \"{Settings.PlayKey}\" (change)";
- }
- private static void CreateTags(SourceGame game)
- {
- var wordMap = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
- var ambiguous = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
- for (int i = 0; i < game.Tracks.Count; i++)
- {
- foreach (var w in game.Tracks[i].Name.Split(' ', '.', '-', '_'))
- {
- // only plain alphanumeric words survive as cfg aliases
- if (w.Length == 0 || w.Length >= 32) continue;
- if (!w.All(char.IsAsciiLetterOrDigit) || w.All(char.IsDigit)) continue;
- if (game.Blacklist.Contains(w.ToLower()) || ambiguous.Contains(w)) continue;
- if (wordMap.TryGetValue(w, out var owner))
- {
- if (owner != i) { wordMap.Remove(w); ambiguous.Add(w); }
- }
- else wordMap[w] = i;
- }
- }
- foreach (var (word, idx) in wordMap)
- game.Tracks[idx].Tags.Add(word);
- }
- private void LogError(Exception ex)
- {
- if (!Settings.LogError) return;
- File.AppendAllText("errorlog.txt", $"----{DateTime.UtcNow} UTC----\n{ex}\n");
- }
- private static readonly bool DebugLogging = false; // flip to true to log to slam_debug.log
- private static void DebugLog(string msg)
- {
- if (DebugLogging)
- File.AppendAllText("slam_debug.log", $"{DateTime.UtcNow:O} {msg}\n");
- }
- public event Action<string>? ErrorOccurred;
- public void ShowError(string msg) => ErrorOccurred?.Invoke(msg);
- public void SaveSettings() => _settings.Save();
- }
|