├── .gitignore ├── SQL2 ├── icon.ico ├── Resources │ ├── Copy.png │ ├── Delete.png │ ├── Desktop.png │ ├── Folder.png │ └── Shortcut.png ├── DataReaders │ ├── ResourceType.cs │ ├── DeflateStreamWrapper.cs │ ├── DirectoryReader.cs │ ├── PK3Reader.cs │ └── PAKReader.cs ├── App.config ├── Properties │ ├── Settings.settings │ ├── Settings.Designer.cs │ ├── AssemblyInfo.cs │ ├── Resources.Designer.cs │ └── Resources.resx ├── App.xaml ├── Data │ ├── VideoModeInfo.cs │ ├── Quake2Font.cs │ ├── QuakeFont.cs │ └── Configuration.cs ├── Items │ ├── ClassItem.cs │ ├── SkillItem.cs │ ├── EngineItem.cs │ ├── GameItem.cs │ ├── ItemType.cs │ ├── ModItem.cs │ ├── MapItem.cs │ ├── ResolutionItem.cs │ ├── AbstractItem.cs │ └── DemoItem.cs ├── Controls │ ├── PreviewRun.cs │ └── PreviewTextBox.cs ├── Games │ ├── HalfLife │ │ ├── HalfLifeDemoReader.cs │ │ └── HalfLifeHandler.cs │ ├── Quake2 │ │ ├── Quake2BSPReader.cs │ │ ├── Quake2DemoReader.cs │ │ └── Quake2Handler.cs │ ├── Quake │ │ ├── QuakeBSPReader.cs │ │ ├── QuakeHandler.cs │ │ └── QuakeDemoReader.cs │ ├── Hexen2 │ │ ├── Hexen2DemoReader.cs │ │ └── Hexen2Handler.cs │ └── GameHandler.cs ├── Tools │ ├── StringEx.cs │ ├── BinaryReaderEx.cs │ └── DisplayTools.cs ├── App.xaml.cs ├── SQL2.csproj ├── MainWindow.xaml └── MainWindow.xaml.cs ├── SQL2.sln ├── LICENSE ├── README.md └── SQL2_Readme.txt /.gitignore: -------------------------------------------------------------------------------- 1 | .vs 2 | bin 3 | obj 4 | *.user 5 | -------------------------------------------------------------------------------- /SQL2/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m-x-d/Simple-Quake-Launcher-2/HEAD/SQL2/icon.ico -------------------------------------------------------------------------------- /SQL2/Resources/Copy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m-x-d/Simple-Quake-Launcher-2/HEAD/SQL2/Resources/Copy.png -------------------------------------------------------------------------------- /SQL2/Resources/Delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m-x-d/Simple-Quake-Launcher-2/HEAD/SQL2/Resources/Delete.png -------------------------------------------------------------------------------- /SQL2/Resources/Desktop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m-x-d/Simple-Quake-Launcher-2/HEAD/SQL2/Resources/Desktop.png -------------------------------------------------------------------------------- /SQL2/Resources/Folder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m-x-d/Simple-Quake-Launcher-2/HEAD/SQL2/Resources/Folder.png -------------------------------------------------------------------------------- /SQL2/Resources/Shortcut.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m-x-d/Simple-Quake-Launcher-2/HEAD/SQL2/Resources/Shortcut.png -------------------------------------------------------------------------------- /SQL2/DataReaders/ResourceType.cs: -------------------------------------------------------------------------------- 1 | namespace mxd.SQL2.DataReaders 2 | { 3 | public enum ResourceType 4 | { 5 | NONE, 6 | FOLDER, 7 | PK3, 8 | PAK 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /SQL2/App.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /SQL2/Properties/Settings.settings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /SQL2/App.xaml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /SQL2/Data/VideoModeInfo.cs: -------------------------------------------------------------------------------- 1 | namespace mxd.SQL2.Data 2 | { 3 | // Hardcoded video mode used by older engines... 4 | public struct VideoModeInfo 5 | { 6 | private readonly int width; 7 | private readonly int height; 8 | private readonly int index; 9 | 10 | public int Width => width; 11 | public int Height => height; 12 | public int Index => index; 13 | 14 | public VideoModeInfo(int width, int height, int index) 15 | { 16 | this.width = width; 17 | this.height = height; 18 | this.index = index; 19 | } 20 | 21 | public override string ToString() 22 | { 23 | return width + "x" + height; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /SQL2/Items/ClassItem.cs: -------------------------------------------------------------------------------- 1 | namespace mxd.SQL2.Items 2 | { 3 | public class ClassItem : AbstractItem 4 | { 5 | #region ================= Default items 6 | 7 | public static readonly ClassItem Default = new ClassItem(NAME_DEFAULT, NAME_DEFAULT, true); 8 | public static readonly ClassItem Random = new ClassItem(NAME_RANDOM, NAME_RANDOM); 9 | 10 | #endregion 11 | 12 | #region ================= Variables 13 | 14 | protected override ItemType type => ItemType.CLASS; 15 | 16 | #endregion 17 | 18 | #region ================= Constructors 19 | 20 | public ClassItem(string title, string value, bool isdefault = false) : base(title, value) 21 | { 22 | this.isdefault = isdefault; 23 | if(isdefault) this.value = NAME_DEFAULT; 24 | } 25 | 26 | #endregion 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /SQL2/Items/SkillItem.cs: -------------------------------------------------------------------------------- 1 | #region ================= Namespaces 2 | 3 | using System.Windows.Media; 4 | 5 | #endregion 6 | 7 | namespace mxd.SQL2.Items 8 | { 9 | public class SkillItem : AbstractItem 10 | { 11 | #region ================= Default items 12 | 13 | public static readonly SkillItem Default = new SkillItem(NAME_DEFAULT, NAME_DEFAULT, true); 14 | public static readonly SkillItem Random = new SkillItem(NAME_RANDOM, NAME_RANDOM); 15 | 16 | #endregion 17 | 18 | #region ================= Variables 19 | 20 | protected override ItemType type => ItemType.SKILL; 21 | 22 | #endregion 23 | 24 | #region ================= Constructor 25 | 26 | public SkillItem(string title, string value, bool isdefault = false, bool isnightmare = false) : base(title, value) 27 | { 28 | this.isdefault = isdefault; 29 | if(isdefault) this.value = NAME_DEFAULT; 30 | if(isnightmare) foreground = Brushes.DarkRed; 31 | } 32 | 33 | #endregion 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /SQL2/Items/EngineItem.cs: -------------------------------------------------------------------------------- 1 | #region ================= Namespaces 2 | 3 | using System.IO; 4 | using System.Windows.Media; 5 | 6 | #endregion 7 | 8 | namespace mxd.SQL2.Items 9 | { 10 | public class EngineItem : AbstractItem 11 | { 12 | #region ================= Variables 13 | 14 | private string filename; 15 | private ImageSource icon; 16 | 17 | protected override ItemType type => ItemType.ENGINE; 18 | 19 | #endregion 20 | 21 | #region ================= Properties 22 | 23 | // Value: quake.exe 24 | // Title: quake 25 | public string FileName => filename; // c:\games\quake\quake.exe 26 | public ImageSource Icon => icon; 27 | 28 | private new bool IsRandom; // No random engines 29 | 30 | #endregion 31 | 32 | #region ================= Constructor 33 | 34 | public EngineItem(ImageSource icon, string path) : base(Path.GetFileNameWithoutExtension(path), Path.GetFileName(path)) 35 | { 36 | this.icon = icon; 37 | this.filename = path; 38 | } 39 | 40 | #endregion 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /SQL2.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 14 4 | VisualStudioVersion = 14.0.25420.1 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SQL2", "SQL2\SQL2.csproj", "{4B99AA35-9D15-47FA-BEEE-225402E7D77E}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {4B99AA35-9D15-47FA-BEEE-225402E7D77E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {4B99AA35-9D15-47FA-BEEE-225402E7D77E}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {4B99AA35-9D15-47FA-BEEE-225402E7D77E}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {4B99AA35-9D15-47FA-BEEE-225402E7D77E}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | EndGlobal 23 | -------------------------------------------------------------------------------- /SQL2/Items/GameItem.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using mxd.SQL2.Games; 4 | 5 | namespace mxd.SQL2.Items 6 | { 7 | public class GameItem : AbstractItem 8 | { 9 | #region ================= Variables 10 | 11 | private readonly string modfolder; 12 | 13 | protected override ItemType type => ItemType.GAME; 14 | 15 | #endregion 16 | 17 | #region ================= Properties 18 | 19 | // Value: -hipnotic 20 | // Title: EP1: Scourge of Armagon 21 | public string ModFolder => modfolder; // c:\Quake\mymod 22 | 23 | private new bool IsRandom; // No random base games 24 | 25 | #endregion 26 | 27 | #region ================= Constructor 28 | 29 | // "EP1: Scourge of Armagon", "HIPNOTIC", -hipnotic 30 | public GameItem(string name, string modfolder, string arg) : base(name, arg) 31 | { 32 | this.modfolder = Path.Combine(App.GamePath, modfolder); 33 | this.isdefault = (string.Equals(GameHandler.Current.DefaultModPath, this.modfolder, StringComparison.OrdinalIgnoreCase)); 34 | } 35 | 36 | #endregion 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 MaxED 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /SQL2/Items/ItemType.cs: -------------------------------------------------------------------------------- 1 | #region ================= Namespaces 2 | 3 | using System.Collections.Generic; 4 | 5 | #endregion 6 | 7 | namespace mxd.SQL2.Items 8 | { 9 | #region ================= ItemType 10 | 11 | public enum ItemType 12 | { 13 | UNKNOWN, 14 | ENGINE, 15 | RESOLUTION, 16 | GAME, 17 | MOD, 18 | MAP, 19 | SKILL, 20 | CLASS, 21 | DEMO, 22 | } 23 | 24 | #endregion 25 | 26 | #region ================= ItemTypes 27 | 28 | public static class ItemTypes 29 | { 30 | public static readonly Dictionary Types; 31 | public static readonly Dictionary Markers; 32 | 33 | static ItemTypes() 34 | { 35 | Types = new Dictionary 36 | { 37 | { ItemType.ENGINE, "%E" }, 38 | { ItemType.RESOLUTION, "%R"}, 39 | { ItemType.GAME, "%G" }, 40 | { ItemType.MOD, "%M" }, 41 | { ItemType.MAP, "%m" }, 42 | { ItemType.SKILL, "%S" }, 43 | { ItemType.CLASS, "%C" }, 44 | { ItemType.DEMO, "%D" } 45 | }; 46 | 47 | Markers = new Dictionary(); 48 | foreach(var group in Types) Markers.Add(group.Value, group.Key); 49 | } 50 | } 51 | 52 | #endregion 53 | } 54 | -------------------------------------------------------------------------------- /SQL2/Properties/Settings.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace mxd.SQL2.Properties { 12 | 13 | 14 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 15 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "14.0.0.0")] 16 | internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { 17 | 18 | private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); 19 | 20 | public static Settings Default { 21 | get { 22 | return defaultInstance; 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /SQL2/Controls/PreviewRun.cs: -------------------------------------------------------------------------------- 1 | #region ================= Namespaces 2 | 3 | using System.Windows; 4 | using System.Windows.Documents; 5 | using mxd.SQL2.Items; 6 | 7 | #endregion 8 | 9 | namespace mxd.SQL2.Controls 10 | { 11 | public class PreviewRun : Run 12 | { 13 | #region ================= Variables 14 | 15 | private readonly bool editable; 16 | private readonly ItemType itemtype; 17 | private readonly AbstractItem item; 18 | 19 | #endregion 20 | 21 | #region ================= Properties 22 | 23 | public bool IsEditable => editable; // Doesn't actually block text editability... 24 | public ItemType ItemType => itemtype; 25 | public AbstractItem Item => item; 26 | 27 | #endregion 28 | 29 | #region ================= Constructor 30 | 31 | public PreviewRun() { } 32 | 33 | public PreviewRun(string text, AbstractItem item, ItemType itemtype, bool editable) 34 | { 35 | base.SetValue(Run.TextProperty, text); 36 | this.itemtype = itemtype; 37 | this.item = item; 38 | this.editable = editable; 39 | 40 | if(editable) 41 | { 42 | this.SetValue(TextElement.ForegroundProperty, SystemColors.HotTrackBrush); 43 | this.SetValue(TextElement.BackgroundProperty, SystemColors.GradientInactiveCaptionBrush); 44 | } 45 | } 46 | 47 | #endregion 48 | 49 | #region ================= Methods 50 | 51 | public override string ToString() 52 | { 53 | return this.Text; 54 | } 55 | 56 | #endregion 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /SQL2/Games/HalfLife/HalfLifeDemoReader.cs: -------------------------------------------------------------------------------- 1 | #region ================= Namespaces 2 | 3 | using System.IO; 4 | using mxd.SQL2.DataReaders; 5 | using mxd.SQL2.Items; 6 | using mxd.SQL2.Tools; 7 | 8 | #endregion 9 | 10 | namespace mxd.SQL2.Games.HalfLife 11 | { 12 | public static class HalfLifeDemoReader 13 | { 14 | #region ================= Constants 15 | 16 | private const string MAGIC = "HLDEMO"; 17 | 18 | #endregion 19 | 20 | #region ================= GetDemoInfo 21 | 22 | // https://sourceforge.net/p/lmpc/git/ci/master/tree/spec/dem-hl.spec 23 | public static DemoItem GetDemoInfo(string demoname, BinaryReader reader, ResourceType restype) 24 | { 25 | /* 0 char[8] magic; /* == "HLDEMO\0\0" */ 26 | /* 8 uint32 demo_version; /* == 5 (HL 1.1.0.1) */ 27 | /* c uint32 network_version; /* == 42 (HL 1.1.0.1) */ 28 | /* 10 char[0x104] map_name; /* eg "c0a0e" */ 29 | /*114 char[0x108] game_dll; /* eg "valve" */ 30 | 31 | // Header is 544 bytes 32 | if(reader.BaseStream.Length - reader.BaseStream.Position < 544) return null; 33 | 34 | // Read header 35 | if(reader.ReadStringExactLength(8) != MAGIC) return null; 36 | reader.BaseStream.Position += 8; // Skip demo_version and network_version 37 | string mapname = reader.ReadStringExactLength(260); 38 | string modname = reader.ReadString('\0'); 39 | 40 | // Done. Easiest of them all :) 41 | return new DemoItem(modname, demoname, mapname, mapname, restype); 42 | } 43 | 44 | #endregion 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Simple Quake Launcher 2 2 | A simple [ZDL](http://zdoom.org/wiki/ZDL)-inspired map/mod/demo launcher for Quake, Quake 2, Hexen 2 and Half-Life. 3 | 4 | ![SQL2 Screenshot](https://lh3.googleusercontent.com/QXBiRtOSrsn__5bU1UgNYtk48DjAR8jCEfySO3Rx1j4RmXT92uIAl7LllygkE3WF4nNnrtYSJSixBgcvfcupioHSUAwpEg646mlDYyaYwD6cjA48gLjpmlQd0-2-iKCkDQKSktEubytbAXp46JEI9zm2oycOtM_0GdTEURXX4c4_7GB7Uj0Huc4IcX6IskGfnQB2b8qUDGVVgmbpztPn-guHbatMVdAFCRb7e1wiFzBW-qaa3vxPP9T44cxjtHgukRvJTufOwkR9CHO8ER2KmH-jERv2Zqi57HGoi1mCxip6XQV3J978991cWAtIITElmfMQAoRocQRdtwVQNF_p5hvpOre7t67lQ40tYhUDwpFFL1ecbok4f6p2PeKiX4MIDVZYra5HqBrea4gY4o2DiI3_S7o64FuhydmuJCAbcgtN5LCfzXhX6l8wqpqAQvoNuuBLjbD0yQPtXxwOP-E381ETtF1WyaOYHP8AHiGyIZLvHpu4qZILTOUlmD6Vsywk4rp1fsCKsuJYFeU7u_gU5Pu_VjRfPChSowfKeJDHNzQJ6083ETl2G2jf3Vis1YAhbfJe5JsjhagdS6jYsX3U-r7kgzrpdM2oQNWHJTX9qQ=w0) 5 | 6 | ### FEATURES: 7 | - Small and easy to use. 8 | - Detects maps and demos in folders, .pak and .pk3 files. 9 | - Displays map titles ("message" worldspawn key). 10 | - Can launch demos (displays map titles for them as well). 11 | - Can create shortcuts to play the game using currently selected options. 12 | - Can run a random map at random skill. 13 | 14 | ### INSTALLATION: 15 | Extract SQLauncher2.exe into your Quake / Quake 2 / Hexen 2 / Half-Life directory. 16 | 17 | ### SYSTEM REQUIREMENTS: 18 | [.net Framework 4.5](https://www.microsoft.com/download/details.aspx?id=30653). 19 | 20 | ### LEGACY: 21 | The older, WinXP-compartible iteration of this project can be found [here](https://sourceforge.net/projects/simplequakelauncher/). -------------------------------------------------------------------------------- /SQL2/Data/Quake2Font.cs: -------------------------------------------------------------------------------- 1 | namespace mxd.SQL2.Data 2 | { 3 | public static class Quake2Font 4 | { 5 | // Remap quake2 font chars to something we can display... 6 | public static readonly string[] CharMap = 7 | { 8 | "*", "", "", "", "", "*", "", "", "", "", "", "", "", "", "*", "*", 9 | "[", "]", "", "", "", "", "", "", "", "", "", "_", "*", "", "", "", 10 | " ", "!", "\"", "#", "$", "%", "&", "'", "(", ")", "*", "+", ",", "-", ".", "/", 11 | "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", ":", ";", "<", "=", ">", "?", 12 | "@", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", 13 | "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "[", "\\", "]", "^", "_", 14 | "`", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", 15 | "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "{", "|", "}", "''", "", 16 | "", "", "", "", "", "*", "", "", "", "", "", "", "", "", "*", "*", 17 | "[", "]", "0", "1", "2", "3", "4", "5", "6", "7", "8", "_", "*", "", "", "", 18 | " ", "!", "\"", "#", "$", "%", "&", "'", "(", ")", "*", "+", ",", "-", ".", "/", 19 | "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", ":", ";", "<", "=", ">", "?", 20 | "@", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", 21 | "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "[", "\\", "]", "^", "_", 22 | "`", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", 23 | "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "{", "|", "}", "''", "" 24 | }; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /SQL2/Data/QuakeFont.cs: -------------------------------------------------------------------------------- 1 | namespace mxd.SQL2.Data 2 | { 3 | public static class QuakeFont 4 | { 5 | // Remap quake font chars to something we can display... 6 | public static readonly string[] CharMap = 7 | { 8 | "*", "", "", "", "", "*", "", "", "", "", "", "", "", "", "*", "*", 9 | "[", "]", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "*", "", "", "", 10 | " ", "!", "\"", "#", "$", "%", "&", "'", "(", ")", "*", "+", ",", "-", ".", "/", 11 | "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", ":", ";", "<", "=", ">", "?", 12 | "@", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", 13 | "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "[", "\\", "]", "^", "_", 14 | "`", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", 15 | "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "{", "|", "}", "~", "", 16 | "", "", "", "", "", "*", "", "", "", "", "", "", "", "", "*", "*", 17 | "[", "]", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "*", "", "", "", 18 | " ", "!", "\"", "#", "$", "%", "&", "'", "(", ")", "*", "+", ",", "-", ".", "/", 19 | "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", ":", ";", "<", "=", ">", "?", 20 | "@", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", 21 | "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "[", "\\", "]", "^", "_", 22 | "`", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", 23 | "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "{", "|", "}", "~", "" 24 | }; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /SQL2/Items/ModItem.cs: -------------------------------------------------------------------------------- 1 | #region ================= Namespaces 2 | 3 | using System; 4 | using System.IO; 5 | using mxd.SQL2.Games; 6 | 7 | #endregion 8 | 9 | namespace mxd.SQL2.Items 10 | { 11 | public class ModItem : AbstractItem 12 | { 13 | #region ================= Default items 14 | 15 | public static readonly ModItem Default = new ModItem(NAME_DEFAULT, GameHandler.Current.DefaultModPath); 16 | 17 | #endregion 18 | 19 | #region ================= Variables 20 | 21 | private string modpath; // c:\quake\mymod 22 | private bool isbuiltin; // true for mods enabled by special cmdline params, like -rogue 23 | 24 | protected override ItemType type => ItemType.MOD; 25 | 26 | #endregion 27 | 28 | #region ================= Properties 29 | 30 | // Value: "Arcane Dimensions" 31 | // Title: Arcane Dimensions 32 | public string ModPath => modpath; // c:\quake\Arcane Dimensions 33 | public bool IsBuiltIn => isbuiltin; 34 | 35 | private new bool IsRandom; // No random mods 36 | 37 | #endregion 38 | 39 | #region ================= Constructors 40 | 41 | // mods\Arcane Dimensions, "c:\Quake\mods\Arcane Dimensions" 42 | public ModItem(string modname, string modpath, bool isbuiltin = false) : base(modname, modname) 43 | { 44 | #if DEBUG 45 | if(!Directory.Exists(modpath)) throw new Exception("Invalid modpath!"); 46 | #endif 47 | this.modpath = modpath; 48 | this.isbuiltin = isbuiltin; 49 | } 50 | 51 | // "MP2: Ground Zero", XATRIX, "c:\Quake2\XATRIX" 52 | public ModItem(string modtitle, string modname, string modpath, bool isbuiltin = false) : base(modtitle, modname) 53 | { 54 | #if DEBUG 55 | if(!Directory.Exists(modpath)) throw new Exception("Invalid modpath!"); 56 | #endif 57 | this.modpath = modpath; 58 | this.isbuiltin = isbuiltin; 59 | } 60 | 61 | #endregion 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /SQL2/Items/MapItem.cs: -------------------------------------------------------------------------------- 1 | #region ================= Namespaces 2 | 3 | using System; 4 | using System.Windows; 5 | using System.Windows.Media; 6 | using mxd.SQL2.DataReaders; 7 | 8 | #endregion 9 | 10 | namespace mxd.SQL2.Items 11 | { 12 | public class MapItem : AbstractItem 13 | { 14 | #region ================= Default items 15 | 16 | public static readonly MapItem Default = new MapItem(NAME_NONE, ResourceType.NONE); 17 | public static readonly MapItem Random = new MapItem(NAME_RANDOM, ResourceType.NONE); 18 | 19 | #endregion 20 | 21 | #region ================= Variables 22 | 23 | private readonly string maptitle; // "The Introduction" 24 | private readonly ResourceType restype; 25 | 26 | protected override ItemType type => ItemType.MAP; 27 | 28 | #endregion 29 | 30 | #region ================= Properties 31 | 32 | public string MapTitle => maptitle; 33 | public ResourceType ResourceType => restype; 34 | 35 | #endregion 36 | 37 | #region ================= Constructors 38 | 39 | // Map title, e1m1 40 | public MapItem(string title, string mapname, ResourceType restype) : base(mapname + " | " + title, mapname) 41 | { 42 | this.maptitle = title; 43 | this.restype = restype; 44 | SetColor(); 45 | } 46 | 47 | // e1m1 48 | public MapItem(string mapname, ResourceType restype) : base(mapname, mapname) 49 | { 50 | this.maptitle = mapname; 51 | this.restype = restype; 52 | SetColor(); 53 | } 54 | 55 | #endregion 56 | 57 | #region ================= Methods 58 | 59 | private void SetColor() 60 | { 61 | switch(restype) 62 | { 63 | case ResourceType.NONE: break; // Already set in AbstractItem 64 | case ResourceType.FOLDER: foreground = SystemColors.ActiveCaptionTextBrush; break; 65 | case ResourceType.PAK: foreground = Brushes.DarkGreen; break; 66 | case ResourceType.PK3: foreground = Brushes.DarkBlue; break; 67 | default: throw new NotImplementedException("Unknown ResourceType!"); 68 | } 69 | } 70 | 71 | #endregion 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /SQL2/Items/ResolutionItem.cs: -------------------------------------------------------------------------------- 1 | #region ================= Namespaces 2 | 3 | using System; 4 | using System.IO; 5 | using mxd.SQL2.Games; 6 | 7 | #endregion 8 | 9 | namespace mxd.SQL2.Items 10 | { 11 | public class ResolutionItem : AbstractItem 12 | { 13 | #region ================= Default items 14 | 15 | public static readonly ResolutionItem Default = new ResolutionItem(); 16 | 17 | #endregion 18 | 19 | #region ================= Variables 20 | 21 | protected override ItemType type => ItemType.RESOLUTION; 22 | 23 | #endregion 24 | 25 | #region ================= Properties 26 | 27 | public readonly int Width; 28 | public readonly int Height; 29 | 30 | private new bool IsRandom; // No random resolutions 31 | 32 | #endregion 33 | 34 | #region ================= Constructors 35 | 36 | private ResolutionItem() : base(NAME_DEFAULT, string.Empty) { } 37 | 38 | public ResolutionItem(int width, int height, int index = -1, bool fullscreen = false) 39 | : base(width + "x" + height + (fullscreen ? " (fullscreen)" : ""), (index == -1 ? width + "x" + height : index.ToString()) + "x" + fullscreen) 40 | { 41 | Width = width; 42 | Height = height; 43 | } 44 | 45 | protected override string GetArgument(string val) 46 | { 47 | // Skip parsing shenanigans for the default item... 48 | if(title == NAME_DEFAULT) return val; 49 | 50 | // val is either WIDTHxHEIGHTxFULLSCREEN or INDEXxFULLSCREEN... 51 | int w, h; 52 | bool fullscreen; 53 | string[] pieces = value.Split(new[] { "x" }, StringSplitOptions.None); 54 | 55 | if(pieces.Length == 3 && int.TryParse(pieces[0], out w) && int.TryParse(pieces[1], out h) && bool.TryParse(pieces[2], out fullscreen)) 56 | return string.Format(param, w, h, GameHandler.Current.FullScreenArg[fullscreen]); 57 | 58 | if(pieces.Length == 2 && int.TryParse(pieces[0], out w) && bool.TryParse(pieces[1], out fullscreen)) 59 | return string.Format(param, w, GameHandler.Current.FullScreenArg[fullscreen]); 60 | 61 | // Should never happen 62 | throw new InvalidDataException("Unexpected screen resolution: " + val); 63 | } 64 | 65 | #endregion 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /SQL2_Readme.txt: -------------------------------------------------------------------------------- 1 | ==== Simple Quake Launcher 2 ==== 2 | A simple ZDL-inspired map/mod/demo launcher for Quake, Quake 2, Hexen 2 and Half-Life. 3 | 4 | ==== FEATURES ==== 5 | - Small and easy to use. 6 | - Detects maps and demos in folders, .pak and .pk3 files. 7 | - Displays map titles ("message" worldspawn key). 8 | - Can launch demos (displays map titles for them as well). 9 | - Can create shortcuts to play the game using currently selected options. 10 | - Can run a random map at random skill. 11 | 12 | ==== INSTALLATION ==== 13 | Extract SQLauncher2.exe into your Quake / Quake 2 / Hexen 2 / Half-Life directory. 14 | 15 | ==== SYSTEM REQUIREMENTS ==== 16 | .net Framework 4.5 (https://www.microsoft.com/download/details.aspx?id=30653). 17 | 18 | ==== LEGACY ==== 19 | The older, WinXP-compartible iteration of this project can be found here: https://sourceforge.net/projects/simplequakelauncher/. 20 | 21 | ==== CHANGELOG ==== 22 | 23 | 2.7: 24 | - Implemented natural string sorting for map and demo filenames (now sorted like this: "e1m1, e1m2, e1m3, e1m10, e1m20" instead of this: "e1m1, e1m10, e1m2, e1m20, e1m3"). 25 | 26 | 2.6: 27 | - Fixed a crash when trying to read non-ASCII chars from worldspawn entity of a .bsp inside a .pk3 archive. 28 | 29 | 2.5: 30 | Added handling for mods without map files. More specifically: 31 | - Quake, Hexen 2: count folder as a mod if it (or PAK/PK3 files within it) contains progs.dat. 32 | - Quake2: count folder as a mod if it contains a variant of gamex86.dll. 33 | - Half-Life: count folder as a mod if it contains "cl_dlls\client.dll". 34 | 35 | 2.4: 36 | - Quake: improved .DEM reader compatibility (FTE/FTE2 demos support). 37 | - Quake, UI: renamed "Medium" skill to "Normal". 38 | 39 | 2.3: 40 | - Desktop resolution can now be selected in the "Resolution" combo box. 41 | - Fixed, Quake 2: Official Mission Pack names were swapped. 42 | 43 | 2.2: 44 | - Quake: improved demo map path validation logic. 45 | - Quake: renamed official mission pack menu items ("EP" -> "MP"). 46 | - UI: added a tooltip to Command Line textbox. 47 | - UI: custom command line parameters can now be cleared by MMB-clicking them. 48 | - Fixed several issues related to (re)storing and displaying of custom command line parameters. 49 | - Fixed a bug in folder maps detection logic. 50 | 51 | 2.1: 52 | - Quake: improved .DEM reader compatibility. 53 | - Optimized PK3 entries processing speed. 54 | 55 | 2.0: 56 | - First public release. -------------------------------------------------------------------------------- /SQL2/Tools/StringEx.cs: -------------------------------------------------------------------------------- 1 | namespace mxd.SQL2.Tools 2 | { 3 | public static class StringEx 4 | { 5 | public static string UppercaseFirst(this string s) 6 | { 7 | if(string.IsNullOrEmpty(s)) return string.Empty; 8 | char[] a = s.ToCharArray(); 9 | a[0] = char.ToUpper(a[0]); 10 | return new string(a); 11 | } 12 | 13 | // Compares two strings and returns a value indicating whether one is less than, equal to, or greater than the other, according to a "natural sort" algorithm. 14 | // Source: naturalstringcomparer.cs by Nazardo (https://gist.github.com/Nazardo/e42de483a03ec2e1ef9348e23bec4f95) 15 | public static int CompareNatural(this string x, string y) 16 | { 17 | int indexX = 0; 18 | int indexY = 0; 19 | 20 | while (true) 21 | { 22 | // Handle the case when one string has ended. 23 | if (indexX == x.Length) 24 | return indexY == y.Length ? 0 : -1; 25 | 26 | if (indexY == y.Length) 27 | return 1; 28 | 29 | char charX = x[indexX]; 30 | char charY = y[indexY]; 31 | 32 | if (char.IsDigit(charX) && char.IsDigit(charY)) 33 | { 34 | // Skip leading zeroes in numbers. 35 | while (indexX < x.Length && x[indexX] == '0') 36 | indexX++; 37 | 38 | while (indexY < y.Length && y[indexY] == '0') 39 | indexY++; 40 | 41 | // Find the end of numbers 42 | int endNumberX = indexX; 43 | int endNumberY = indexY; 44 | 45 | while (endNumberX < x.Length && char.IsDigit(x[endNumberX])) 46 | endNumberX++; 47 | 48 | while (endNumberY < y.Length && char.IsDigit(y[endNumberY])) 49 | endNumberY++; 50 | 51 | int digitsLengthX = endNumberX - indexX; 52 | int digitsLengthY = endNumberY - indexY; 53 | 54 | // If the lengths are different, then the longer number is bigger 55 | if (digitsLengthX != digitsLengthY) 56 | return digitsLengthX - digitsLengthY; 57 | 58 | // Compare numbers digit by digit 59 | while (indexX < endNumberX) 60 | { 61 | if (x[indexX] != y[indexY]) 62 | return x[indexX] - y[indexY]; 63 | 64 | indexX++; 65 | indexY++; 66 | } 67 | } 68 | else 69 | { 70 | // Plain characters comparison 71 | int compareResult = char.ToUpperInvariant(charX).CompareTo(char.ToUpperInvariant(charY)); 72 | if (compareResult != 0) 73 | return compareResult; 74 | 75 | indexX++; 76 | indexY++; 77 | } 78 | } 79 | } 80 | 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /SQL2/DataReaders/DeflateStreamWrapper.cs: -------------------------------------------------------------------------------- 1 | #region ================= Namespaces 2 | 3 | using System; 4 | using System.IO; 5 | using System.IO.Compression; 6 | 7 | #endregion 8 | 9 | namespace mxd.SQL2.DataReaders 10 | { 11 | public class DeflateStreamWrapper : Stream 12 | { 13 | #region ================= Variables 14 | 15 | private Stream stream; 16 | private long position; 17 | private long length; 18 | 19 | #endregion 20 | 21 | #region ================= Properties 22 | 23 | // Obligatory abstract property overrides 24 | public override bool CanRead => stream.CanRead; 25 | public override bool CanSeek => stream.CanSeek; 26 | public override bool CanWrite => stream.CanWrite; 27 | 28 | // Emulated stream properties 29 | public override long Length => length; 30 | public override long Position { get { return position; } set { SkipTo(value - position); } } 31 | 32 | #endregion 33 | 34 | #region ================= Constructor 35 | 36 | // DeflateStream cannot return Position or Length 37 | public DeflateStreamWrapper(DeflateStream stream, long length) 38 | { 39 | if(stream == null) throw new NullReferenceException("Stream is null!"); 40 | 41 | this.stream = stream; 42 | this.length = length; 43 | this.position = 0; 44 | } 45 | 46 | #endregion 47 | 48 | #region ================= Methods 49 | 50 | public override long Seek(long offset, SeekOrigin origin) 51 | { 52 | switch(origin) 53 | { 54 | case SeekOrigin.Current: SkipTo(offset); break; 55 | case SeekOrigin.Begin: SkipTo(offset - position); break; 56 | case SeekOrigin.End: SkipTo(length + offset - position); break; 57 | } 58 | 59 | return position; 60 | } 61 | 62 | public override int Read(byte[] buffer, int offset, int count) 63 | { 64 | int result = stream.Read(buffer, offset, count); 65 | position += result; 66 | return result; 67 | } 68 | 69 | private void SkipTo(long offset) 70 | { 71 | if(offset < 0 || length - position < offset) throw new Exception("Stream cannot be rewinded!"); 72 | if(offset == 0) return; 73 | 74 | byte[] unused = new byte[offset]; 75 | int result = stream.Read(unused, 0, (int)offset); 76 | position += result; 77 | } 78 | 79 | // Obligatory abstract method overrides 80 | public override void Flush() { stream.Flush(); } 81 | public override void SetLength(long value) { stream.SetLength(value); } 82 | public override void Write(byte[] buffer, int offset, int count) { stream.Write(buffer, offset, count); } 83 | 84 | #endregion 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /SQL2/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.InteropServices; 3 | using System.Windows; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("Simple Quake Launcher 2")] 9 | [assembly: AssemblyDescription("Quake / Quake II / Hexen II / Half-Life map launcher")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("Simple Quake Launcher 2")] 13 | [assembly: AssemblyCopyright("Copyright © MaxED 2017, 2023")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | //In order to begin building localizable applications, set 23 | //CultureYouAreCodingWith in your .csproj file 24 | //inside a . For example, if you are using US english 25 | //in your source files, set the to en-US. Then uncomment 26 | //the NeutralResourceLanguage attribute below. Update the "en-US" in 27 | //the line below to match the UICulture setting in the project file. 28 | 29 | //[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)] 30 | 31 | 32 | [assembly: ThemeInfo( 33 | ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located 34 | //(used if a resource is not found in the page, 35 | // or application resource dictionaries) 36 | ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located 37 | //(used if a resource is not found in the page, 38 | // app, or any theme specific resource dictionaries) 39 | )] 40 | 41 | 42 | // Version information for an assembly consists of the following four values: 43 | // 44 | // Major Version 45 | // Minor Version 46 | // Build Number 47 | // Revision 48 | // 49 | // You can specify all the values or you can default the Build and Revision Numbers 50 | // by using the '*' as shown below: 51 | // [assembly: AssemblyVersion("1.0.*")] 52 | [assembly: AssemblyVersion("2.7.0.0")] -------------------------------------------------------------------------------- /SQL2/App.xaml.cs: -------------------------------------------------------------------------------- 1 | #region ================= Namespaces 2 | 3 | using System; 4 | using System.Globalization; 5 | using System.IO; 6 | using System.Reflection; 7 | using System.Threading; 8 | using System.Windows; 9 | using mxd.SQL2.Games; 10 | 11 | #endregion 12 | 13 | namespace mxd.SQL2 14 | { 15 | public partial class App : Application 16 | { 17 | #region ================= Variables 18 | 19 | private static string appname; // SQLauncher 20 | private static string version; 21 | private static string gamepath; // c:\Games\Quake\ 22 | private static string inipath; // c:\Games\Quake\SQLauncher.ini 23 | private static Random random; // 42. Or 667. Or 1? 24 | 25 | #endregion 26 | 27 | #region ================= Properties 28 | 29 | public static string AppName => appname; 30 | public static string Version => version; 31 | public static string GamePath => gamepath; 32 | public static string IniPath => inipath; 33 | public static Random Random => random; 34 | 35 | public static string ErrorMessageTitle = "Serious Error!"; 36 | 37 | #endregion 38 | 39 | private void App_OnStartup(object sender, StartupEventArgs e) 40 | { 41 | //Application.Current.DispatcherUnhandledException += delegate(object o, DispatcherUnhandledExceptionEventArgs args) { throw args.Exception; }; 42 | //AppDomain.CurrentDomain.UnhandledException += delegate(object o, UnhandledExceptionEventArgs args) { throw new Exception(args.ExceptionObject.ToString()); }; 43 | 44 | // Store application path, version, game path and program name 45 | AssemblyName thisasm = Assembly.GetExecutingAssembly().GetName(); 46 | version = thisasm.Version.Major + "." + thisasm.Version.Minor; 47 | appname = Path.GetFileNameWithoutExtension(thisasm.CodeBase); 48 | Uri localpath = new Uri(Path.GetDirectoryName(thisasm.CodeBase)); 49 | 50 | string apppath = Uri.UnescapeDataString(localpath.AbsolutePath); 51 | gamepath = ((e.Args.Length == 1 && Directory.Exists(e.Args[0])) ? e.Args[0] : apppath); 52 | inipath = Path.Combine(gamepath, appname + ".ini"); 53 | 54 | random = new Random(); 55 | 56 | if(!GameHandler.Create(gamepath)) 57 | { 58 | MessageBox.Show("No supported game files detected in the game directory (" + gamepath 59 | + ")\n\nMake sure you are running this program from your " + GameHandler.SupportedGames + " directory!", ErrorMessageTitle); 60 | Application.Current.Shutdown(); 61 | return; 62 | } 63 | 64 | Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture; // Set CultureInfo 65 | 66 | var mainwindow = new MainWindow(); 67 | mainwindow.Show(); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /SQL2/Items/AbstractItem.cs: -------------------------------------------------------------------------------- 1 | #region ================= Namespaces 2 | 3 | using System.Windows; 4 | using System.Windows.Media; 5 | using mxd.SQL2.Games; 6 | 7 | #endregion 8 | 9 | namespace mxd.SQL2.Items 10 | { 11 | public abstract class AbstractItem 12 | { 13 | #region ================= Constants 14 | 15 | protected const string NAME_RANDOM = "[Random]"; 16 | protected const string NAME_DEFAULT = "[Default]"; 17 | protected const string NAME_NONE = "[None]"; 18 | 19 | #endregion 20 | 21 | #region ================= Variables 22 | 23 | protected string value; // e1m1 24 | protected string argument; // +map "e1 m1" 25 | protected string argumentpreview; // +map ??? 26 | protected string title; // e1m1 | The Underhalls 27 | protected string param; // +map {0} 28 | 29 | protected bool israndom; 30 | protected bool isdefault; 31 | 32 | protected Brush foreground; 33 | 34 | protected abstract ItemType type { get; } 35 | 36 | #endregion 37 | 38 | #region ================= Properties 39 | 40 | public virtual string Value => value; // e1m1 41 | public virtual string Argument => (israndom ? GetArgument(GameHandler.Current.GetRandomItem(type)) : argument); // +map "e1 m1" 42 | public virtual string ArgumentPreview => argumentpreview; // +map ??? 43 | public virtual string Title => title; // e1m1 | The Underhalls 44 | 45 | public bool IsRandom => israndom; 46 | public bool IsDefault => isdefault; 47 | 48 | public Brush Foreground => foreground; 49 | 50 | #endregion 51 | 52 | #region ================= Constructor 53 | 54 | protected AbstractItem(string title, string value) 55 | { 56 | this.israndom = (title == NAME_RANDOM); 57 | this.isdefault = (title == NAME_DEFAULT || title == NAME_NONE); 58 | 59 | this.title = title; 60 | this.value = GetSafeValue(value.ToLowerInvariant()); 61 | this.param = GameHandler.Current.LaunchParameters[type]; 62 | this.argument = GetArgument(this.value); 63 | this.argumentpreview = GetArgument(israndom ? "???" : this.value); 64 | 65 | this.foreground = (israndom || isdefault ? SystemColors.InactiveCaptionTextBrush : SystemColors.ActiveCaptionTextBrush); 66 | } 67 | 68 | #endregion 69 | 70 | #region ================= Methods 71 | 72 | protected virtual string GetArgument(string val) 73 | { 74 | return (!string.IsNullOrEmpty(param) ? string.Format(param, GetSafeValue(val)) : val); 75 | } 76 | 77 | protected static string GetSafeValue(string val) 78 | { 79 | return (val.Contains(" ") ? "\"" + val + "\"" : val); 80 | } 81 | 82 | public override string ToString() 83 | { 84 | return title; 85 | } 86 | 87 | #endregion 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /SQL2/Games/Quake2/Quake2BSPReader.cs: -------------------------------------------------------------------------------- 1 | #region ================= Namespaces 2 | 3 | using System; 4 | using System.IO; 5 | using mxd.SQL2.Data; 6 | using mxd.SQL2.DataReaders; 7 | using mxd.SQL2.Items; 8 | using mxd.SQL2.Tools; 9 | 10 | #endregion 11 | 12 | namespace mxd.SQL2.Games.Quake2 13 | { 14 | public static class Quake2BSPReader 15 | { 16 | #region ================= Constants 17 | 18 | private const string BSP_MAGIC = "IBSP"; 19 | private const int BSP_VERSION = 38; 20 | 21 | #endregion 22 | 23 | #region ================= GetMapInfo 24 | 25 | public static MapItem GetMapInfo(string name, BinaryReader reader, ResourceType restype) 26 | { 27 | long offset = reader.BaseStream.Position; 28 | 29 | // Check header 30 | string magic = reader.ReadStringExactLength(4); 31 | int version = reader.ReadInt32(); 32 | 33 | if(magic != BSP_MAGIC || version != BSP_VERSION) 34 | return new MapItem(name, restype); 35 | 36 | // Next is lump directory. We are interested in the first one 37 | long entdatastart = reader.ReadUInt32() + offset; 38 | long entdataend = entdatastart + reader.ReadUInt32(); 39 | 40 | if(entdatastart >= reader.BaseStream.Length || entdataend >= reader.BaseStream.Length) 41 | return new MapItem(name, restype); 42 | 43 | // Get entities data. Worldspawn should be the first entry 44 | reader.BaseStream.Position = entdatastart + 1; // Skip the first "{" 45 | string data = reader.ReadString(' '); 46 | 47 | while(!data.EndsWith("\"message\"", StringComparison.OrdinalIgnoreCase) && !data.Contains("}") && reader.BaseStream.Position < entdataend) 48 | { 49 | data = reader.ReadString(' '); 50 | } 51 | 52 | // Next quoted string is map name 53 | string title = string.Empty; 54 | if(data.EndsWith("\"message\"", StringComparison.OrdinalIgnoreCase)) 55 | { 56 | byte b = reader.ReadByte(); 57 | 58 | // Skip opening quote... 59 | while((char)b != '\"') b = reader.ReadByte(); 60 | 61 | // Continue till closing quote... 62 | b = 0; 63 | byte prevchar = b; 64 | while(true) 65 | { 66 | b = reader.ReadByte(); 67 | 68 | // Stop on closing quote, EOF or closing brace... 69 | if((char)b == '\"' || (char)b == '\0' || (char)b == '}') break; 70 | 71 | // Replace newline with space 72 | if(b == 'n' && prevchar == '\\') 73 | { 74 | prevchar = b; 75 | title = title.Remove(title.Length - 1, 1) + ' '; 76 | continue; 77 | } 78 | 79 | // Trim extra spaces... 80 | if(!(prevchar == 32 && prevchar == b)) title += Quake2Font.CharMap[b]; 81 | prevchar = b; 82 | } 83 | } 84 | 85 | // Return MapItem with title, if we have one 86 | title = GameHandler.Current.CheckMapTitle(title); 87 | return (!string.IsNullOrEmpty(title) ? new MapItem(title, name, restype) : new MapItem(name, restype)); 88 | } 89 | 90 | #endregion 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /SQL2/Games/Quake2/Quake2DemoReader.cs: -------------------------------------------------------------------------------- 1 | #region ================= Namespaces 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | using System.IO; 6 | using mxd.SQL2.Data; 7 | using mxd.SQL2.DataReaders; 8 | using mxd.SQL2.Items; 9 | using mxd.SQL2.Tools; 10 | 11 | #endregion 12 | 13 | namespace mxd.SQL2.Games.Quake2 14 | { 15 | public static class Quake2DemoReader 16 | { 17 | #region ================= Constants 18 | 19 | private const int PROTOCOL_KMQ = 56; 20 | private const int PROTOCOL_R1Q2 = 35; 21 | private static readonly HashSet ProtocolsQ2 = new HashSet { 25, 26, 27, 28, 30, 31, 32, 33, 34 }; // The many Q2 PROTOCOL_VERSIONs... 22 | 23 | private const int SERVERINFO = 12; // 0x0C 24 | private const int CONFIGSTRING = 13; // 0x0D 25 | 26 | #endregion 27 | 28 | #region ================= GetDemoInfo 29 | 30 | // https://www.quakewiki.net/archives/demospecs/dm2/dm2.html 31 | public static DemoItem GetDemoInfo(string demoname, BinaryReader reader, ResourceType restype) 32 | { 33 | // uint blocklength 34 | // SERVERINFO 35 | // CONFIGSTRINGS, one of them is .bsp path 36 | 37 | int blocklength = reader.ReadInt32(); 38 | if(reader.BaseStream.Position + blocklength >= reader.BaseStream.Length) return null; 39 | long blockend = blocklength + reader.BaseStream.Position; 40 | 41 | int messagetype = reader.ReadByte(); 42 | if(messagetype != SERVERINFO) return null; 43 | 44 | // Read ServerInfo 45 | int serverversion = reader.ReadInt32(); 46 | if(serverversion != PROTOCOL_KMQ && serverversion != PROTOCOL_R1Q2 && !ProtocolsQ2.Contains(serverversion)) return null; 47 | int key = reader.ReadInt32(); 48 | if(reader.ReadByte() != 1) return null; // Not a RECORD_CLIENT demo... 49 | string gamedir = reader.ReadString('\0'); // Game directory (may be empty, which means "baseq2"). 50 | int playernum = reader.ReadInt16(); 51 | string maptitle = reader.ReadMapTitle(blocklength, Quake2Font.CharMap); 52 | 53 | // Read configstrings 54 | string mapfilepath = string.Empty; 55 | while(reader.BaseStream.Position < blockend) 56 | { 57 | messagetype = reader.ReadByte(); 58 | if(messagetype != CONFIGSTRING) return null; 59 | 60 | int configstringtype = reader.ReadInt16(); 61 | string data = reader.ReadString('\0'); 62 | 63 | if(data.EndsWith(".bsp", StringComparison.OrdinalIgnoreCase)) 64 | { 65 | mapfilepath = data; 66 | break; 67 | } 68 | 69 | // Block end reached?.. 70 | if(reader.BaseStream.Position == blockend) 71 | { 72 | blockend = reader.ReadUInt32() + reader.BaseStream.Position; 73 | if(blockend >= reader.BaseStream.Length) return null; 74 | } 75 | } 76 | 77 | if(!string.IsNullOrEmpty(maptitle) && !string.IsNullOrEmpty(mapfilepath)) 78 | return new DemoItem(demoname, mapfilepath, maptitle, restype); 79 | 80 | // No dice... 81 | return null; 82 | } 83 | 84 | #endregion 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /SQL2/Games/Quake/QuakeBSPReader.cs: -------------------------------------------------------------------------------- 1 | #region ================= Namespaces 2 | 3 | using System; 4 | using System.IO; 5 | using mxd.SQL2.Data; 6 | using mxd.SQL2.DataReaders; 7 | using mxd.SQL2.Items; 8 | using mxd.SQL2.Tools; 9 | 10 | #endregion 11 | 12 | namespace mxd.SQL2.Games.Quake 13 | { 14 | // Quake BSP reader 15 | public static class QuakeBSPReader 16 | { 17 | #region ================= Constants 18 | 19 | private const int BSPVERSION = 29; 20 | private const int BSP2VERSION_2PSB = (('B' << 24) | ('S' << 16) | ('P' << 8) | '2'); 21 | private const int BSP2VERSION_BSP2 = (('B' << 0) | ('S' << 8) | ('P' << 16) | ('2' << 24)); 22 | 23 | #endregion 24 | 25 | #region ================= GetMapInfo 26 | 27 | public static MapItem GetMapInfo(string name, BinaryReader reader, ResourceType restype) 28 | { 29 | long offset = reader.BaseStream.Position; 30 | 31 | // Get version and offset to entities 32 | int version = reader.ReadInt32(); 33 | long entdatastart = reader.ReadInt32() + offset; 34 | long entdataend = entdatastart + reader.ReadInt32(); 35 | 36 | // Time to bail out? 37 | if((version != BSPVERSION && version != BSP2VERSION_BSP2 && version != BSP2VERSION_2PSB) 38 | || entdatastart >= reader.BaseStream.Length || entdataend >= reader.BaseStream.Length) 39 | return new MapItem(name, restype); 40 | 41 | // Get entities data. Worldspawn should be the first entry 42 | reader.BaseStream.Position = entdatastart + 1; // Skip the first "{" 43 | string data = reader.ReadString(' '); 44 | 45 | while(!IsMessage(data) && !data.Contains("}") && reader.BaseStream.Position < entdataend) 46 | { 47 | data = reader.ReadString(' '); 48 | } 49 | 50 | // Next quoted string is map name 51 | string title = string.Empty; 52 | if(IsMessage(data)) 53 | { 54 | byte b = reader.ReadByte(); 55 | 56 | // Skip opening quote... 57 | while((char)b != '\"') b = reader.ReadByte(); 58 | 59 | // Continue till closing quote... 60 | b = 0; 61 | byte prevchar = b; 62 | while(true) 63 | { 64 | b = reader.ReadByte(); 65 | 66 | // Stop on closing quote, EOF or closing brace... 67 | if((char)b == '\"' || (char)b == '\0' || (char)b == '}') break; 68 | 69 | // Replace newline with space 70 | if(b == 'n' && prevchar == '\\') 71 | { 72 | prevchar = b; 73 | title = title.Remove(title.Length - 1, 1) + ' '; 74 | continue; 75 | } 76 | 77 | // Trim extra spaces... 78 | if(!(prevchar == 32 && prevchar == b)) title += QuakeFont.CharMap[b]; 79 | prevchar = b; 80 | } 81 | } 82 | 83 | // Return MapItem with title, if we have one 84 | title = GameHandler.Current.CheckMapTitle(title); 85 | return (!string.IsNullOrEmpty(title) ? new MapItem(title, name, restype) : new MapItem(name, restype)); 86 | } 87 | 88 | private static bool IsMessage(string data) 89 | { 90 | return data.EndsWith("\"message\"", StringComparison.OrdinalIgnoreCase) || data.EndsWith("\"netname\"", StringComparison.OrdinalIgnoreCase); 91 | } 92 | 93 | #endregion 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /SQL2/Tools/BinaryReaderEx.cs: -------------------------------------------------------------------------------- 1 | #region ================= Namespaces 2 | 3 | using System.IO; 4 | using System.Text; 5 | 6 | #endregion 7 | 8 | namespace mxd.SQL2.Tools 9 | { 10 | // Extension methods for binary reader 11 | public static class BinaryReaderEx 12 | { 13 | #region ================= String reading 14 | 15 | // Reads given length of bytes as a string 16 | public static string ReadStringExactLength(this BinaryReader br, int len) 17 | { 18 | char[] arr = new char[len]; 19 | int i; 20 | 21 | for(i = 0; i < len; ++i) 22 | { 23 | var c = br.ReadChar(); 24 | if(c == '\0') break; 25 | arr[i] = c; 26 | } 27 | 28 | if(i < len) br.BaseStream.Position += (len - i - 1); 29 | return new string(arr, 0, i); 30 | } 31 | 32 | // Reads a string until either maxlength chars are read or terminator char is encountered 33 | public static string ReadString(this BinaryReader reader, int maxlength, char terminator = '\0') 34 | { 35 | char[] arr = new char[maxlength]; 36 | int i; 37 | 38 | for(i = 0; i < maxlength; i++) 39 | { 40 | var c = reader.ReadChar(); 41 | if(c == terminator) break; 42 | arr[i] = c; 43 | } 44 | 45 | return new string(arr, 0, i); 46 | } 47 | 48 | // Reads bytes as a string until given char, null or EOF is encountered 49 | public static string ReadString(this BinaryReader br, char stopper) 50 | { 51 | var sb = new StringBuilder(); 52 | 53 | if(stopper == '\0') 54 | { 55 | while(br.BaseStream.Position < br.BaseStream.Length) 56 | { 57 | var c = br.ReadChar(); 58 | if(c == '\0') break; 59 | sb.Append(c); 60 | } 61 | } 62 | else 63 | { 64 | while(br.BaseStream.Position < br.BaseStream.Length) 65 | { 66 | var c = br.ReadChar(); 67 | if(c == '\0' || c == stopper) break; 68 | sb.Append(c); 69 | } 70 | } 71 | 72 | return sb.ToString(); 73 | } 74 | 75 | public static bool SkipString(this BinaryReader reader, int maxlength, char terminator = '\0') 76 | { 77 | char c = '0'; 78 | for(int i = 0; i < maxlength; i++) 79 | { 80 | c = reader.ReadChar(); 81 | if(c == terminator) break; 82 | } 83 | 84 | return (c == terminator); 85 | } 86 | 87 | #endregion 88 | 89 | #region ================= Special string reading 90 | 91 | public static string ReadMapTitle(this BinaryReader reader, int maxlength, string[] charmap) 92 | { 93 | string result = string.Empty; 94 | 95 | byte prevchar = 0; 96 | for(int i = 0; i < maxlength; i++) 97 | { 98 | var b = reader.ReadByte(); 99 | 100 | // Stop on null char 101 | if(b == 0) break; 102 | 103 | // Replace newline with space 104 | if(b == 'n' && prevchar == '\\') 105 | { 106 | prevchar = b; 107 | result = result.Remove(result.Length - 1, 1) + ' '; 108 | continue; 109 | } 110 | 111 | // Trim extra spaces... 112 | if(!(prevchar == 32 && prevchar == b)) result += charmap[b]; 113 | prevchar = b; 114 | } 115 | 116 | return result; 117 | } 118 | 119 | #endregion 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /SQL2/Games/Hexen2/Hexen2DemoReader.cs: -------------------------------------------------------------------------------- 1 | #region ================= Namespaces 2 | 3 | using System.IO; 4 | using mxd.SQL2.Data; 5 | using mxd.SQL2.DataReaders; 6 | using mxd.SQL2.Items; 7 | using mxd.SQL2.Tools; 8 | 9 | #endregion 10 | 11 | namespace mxd.SQL2.Games.Hexen2 12 | { 13 | public static class Hexen2DemoReader 14 | { 15 | #region ================= Constants 16 | 17 | private const int PROTOCOL_HEXEN2 = 19; 18 | 19 | private const int GAME_COOP = 0; 20 | private const int GAME_DEATHMATCH = 1; 21 | 22 | private const int SVC_PRINT = 8; 23 | private const int SVC_SERVERINFO = 11; 24 | 25 | #endregion 26 | 27 | #region ================= GetDemoInfo 28 | 29 | public static DemoItem GetDemoInfo(string demoname, BinaryReader reader, ResourceType restype) 30 | { 31 | // CD track (string terminated by '\n' (0x0A in ASCII)) 32 | if(!reader.SkipString(13, '\n')) return null; 33 | 34 | string maptitle = string.Empty; 35 | string mapfilepath = string.Empty; 36 | int protocol = 0; 37 | bool alldatafound = false; 38 | 39 | // Read blocks... 40 | while(reader.BaseStream.Position < reader.BaseStream.Length) 41 | { 42 | if(alldatafound) break; 43 | 44 | // Block header: 45 | // Block size (int32) 46 | // Camera angles (int32 x 3) 47 | 48 | int blocklength = reader.ReadInt32(); 49 | long blockend = reader.BaseStream.Position + blocklength; 50 | if(blockend >= reader.BaseStream.Length) return null; 51 | reader.BaseStream.Position += 12; // Skip camera angles 52 | 53 | // Read messages... 54 | while(reader.BaseStream.Position < blockend) 55 | { 56 | if(alldatafound) break; 57 | 58 | int message = reader.ReadByte(); 59 | switch(message) 60 | { 61 | // SVC_SERVERINFO (byte, 0x0B) 62 | // protocol (int32) -> 19 63 | // maxclients (byte) should be in 1 .. 16 range 64 | // gametype (byte) - 0 -> coop, 1 -> deathmatch 65 | // map title (null-terminated string) 66 | // map filename (null-terminated string) "maps/mymap.bsp" 67 | 68 | case SVC_SERVERINFO: 69 | protocol = reader.ReadInt32(); 70 | if(protocol != PROTOCOL_HEXEN2) return null; 71 | 72 | int maxclients = reader.ReadByte(); 73 | if(maxclients < 1 || maxclients > 16) return null; 74 | 75 | int gametype = reader.ReadByte(); 76 | if(gametype != GAME_COOP && gametype != GAME_DEATHMATCH) return null; 77 | 78 | maptitle = reader.ReadMapTitle(blocklength, QuakeFont.CharMap); // Map title can contain bogus chars... 79 | mapfilepath = reader.ReadString(blocklength); 80 | if(string.IsNullOrEmpty(mapfilepath)) return null; 81 | if(string.IsNullOrEmpty(maptitle)) maptitle = Path.GetFileName(mapfilepath); 82 | alldatafound = true; 83 | break; 84 | 85 | case SVC_PRINT: 86 | // The string reading stops at '\0' or after 0x7FF bytes. The internal buffer has only 0x800 bytes available. 87 | if(!reader.SkipString(2048)) return null; 88 | break; 89 | 90 | default: 91 | return null; 92 | } 93 | } 94 | } 95 | 96 | // Done 97 | return (alldatafound ? new DemoItem(demoname, mapfilepath, maptitle, restype) : null); 98 | } 99 | 100 | #endregion 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /SQL2/Items/DemoItem.cs: -------------------------------------------------------------------------------- 1 | #region ================= Namespaces 2 | 3 | using System; 4 | using System.IO; 5 | using System.Windows; 6 | using System.Windows.Media; 7 | using mxd.SQL2.DataReaders; 8 | 9 | #endregion 10 | 11 | namespace mxd.SQL2.Items 12 | { 13 | public class DemoItem : AbstractItem 14 | { 15 | #region ================= Default items 16 | 17 | public static readonly DemoItem None = new DemoItem(NAME_NONE); 18 | 19 | #endregion 20 | 21 | #region ================= Variables 22 | 23 | private readonly string modname; // Stored only on QWD demos, so can be empty... 24 | private readonly string mapfilepath; 25 | private readonly string maptitle; 26 | private readonly bool isinvalid; 27 | private readonly ResourceType restype; 28 | 29 | protected override ItemType type => ItemType.DEMO; 30 | 31 | #endregion 32 | 33 | #region ================= Properties 34 | 35 | // Value: demos\somedemo.dem 36 | // Title: demos\somedemo.dem | map: Benis Devastation 37 | public string ModName => modname; // xatrix / id1 etc. 38 | public string MapFilePath => mapfilepath; // maps/somemap.bsp 39 | public string MapTitle => maptitle; // Benis Devastation 40 | public bool IsInvalid => isinvalid; 41 | public ResourceType ResourceType => restype; 42 | 43 | private new bool IsRandom; // No random demos 44 | 45 | #endregion 46 | 47 | #region ================= Constructors 48 | 49 | private DemoItem(string name) : base(name, "") 50 | { 51 | this.maptitle = name; 52 | } 53 | 54 | // "demos\dm3_demo.dem", "maps\dm3.bsp", "Whatever Title DM3 Has" 55 | public DemoItem(string filename, string mapfilepath, string maptitle, ResourceType restype) : base(filename + " | map: " + maptitle, filename) 56 | { 57 | this.modname = string.Empty; 58 | this.mapfilepath = mapfilepath; 59 | this.maptitle = maptitle; 60 | this.restype = restype; 61 | SetColor(); 62 | } 63 | 64 | // "qw", "demos\dm3_demo.dem", "maps\dm3.bsp", "Whatever Title DM3 Has" 65 | public DemoItem(string modname, string filename, string mapfilepath, string maptitle, ResourceType restype) : base(filename + " | map: " + maptitle, filename) 66 | { 67 | this.modname = modname; 68 | this.mapfilepath = mapfilepath; 69 | this.maptitle = maptitle; 70 | this.restype = restype; 71 | SetColor(); 72 | } 73 | 74 | public DemoItem(string filename, string message, ResourceType restype) : base((string.IsNullOrEmpty(message) ? filename : filename + " | " + message), filename) 75 | { 76 | this.isinvalid = true; 77 | this.maptitle = Path.GetFileName(filename); 78 | this.restype = restype; 79 | SetColor(); 80 | } 81 | 82 | #endregion 83 | 84 | #region ================= Methods 85 | 86 | private void SetColor() 87 | { 88 | if(isinvalid) 89 | { 90 | foreground = Brushes.DarkRed; 91 | return; 92 | } 93 | 94 | switch(restype) 95 | { 96 | case ResourceType.NONE: break; // Already set in AbstractItem 97 | case ResourceType.FOLDER: foreground = SystemColors.ActiveCaptionTextBrush; break; 98 | case ResourceType.PAK: foreground = Brushes.DarkGreen; break; 99 | case ResourceType.PK3: foreground = Brushes.DarkBlue; break; 100 | default: throw new NotImplementedException("Unknown ResourceType!"); 101 | } 102 | } 103 | 104 | #endregion 105 | } 106 | } -------------------------------------------------------------------------------- /SQL2/DataReaders/DirectoryReader.cs: -------------------------------------------------------------------------------- 1 | #region ================= Namespaces 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | using System.IO; 6 | using System.Text; 7 | using mxd.SQL2.Games; 8 | using mxd.SQL2.Items; 9 | 10 | #endregion 11 | 12 | namespace mxd.SQL2.DataReaders 13 | { 14 | public static class DirectoryReader 15 | { 16 | #region ================= Variables 17 | 18 | private const ResourceType restype = ResourceType.FOLDER; 19 | 20 | #endregion 21 | 22 | #region ================= Maps 23 | 24 | public static void GetMaps(string modpath, Dictionary mapslist, GameHandler.GetMapInfoDelegate getmapinfo) 25 | { 26 | DirectoryInfo mapdir = new DirectoryInfo(Path.Combine(modpath, "maps")); 27 | if(!mapdir.Exists) return; 28 | 29 | // Get the map files 30 | string[] mapnames = Directory.GetFiles(mapdir.FullName, "*.bsp"); 31 | foreach(string file in mapnames) 32 | { 33 | if(!GameHandler.Current.EntryIsMap(file, mapslist)) continue; 34 | string mapname = Path.GetFileNameWithoutExtension(file); 35 | MapItem mapitem; 36 | 37 | if(getmapinfo != null) 38 | { 39 | using(FileStream stream = File.OpenRead(file)) 40 | using(BinaryReader reader = new BinaryReader(stream, Encoding.ASCII)) 41 | mapitem = getmapinfo(mapname, reader, restype); 42 | } 43 | else 44 | { 45 | mapitem = new MapItem(mapname, restype); 46 | } 47 | 48 | // Add to collection 49 | mapslist.Add(mapname, mapitem); 50 | } 51 | } 52 | 53 | public static bool ContainsMaps(string modpath) 54 | { 55 | DirectoryInfo mapdir = new DirectoryInfo(Path.Combine(modpath, "maps")); 56 | if(!mapdir.Exists) return false; 57 | 58 | // Get map files 59 | string prefix = GameHandler.Current.IgnoredMapPrefix; 60 | string[] mapnames = Directory.GetFiles(mapdir.FullName, "*.bsp"); 61 | 62 | foreach(string file in mapnames) 63 | { 64 | if(!file.EndsWith(".bsp", StringComparison.OrdinalIgnoreCase)) continue; 65 | if(string.IsNullOrEmpty(prefix) || !Path.GetFileName(file).StartsWith(prefix)) 66 | return true; 67 | } 68 | 69 | return false; 70 | } 71 | 72 | #endregion 73 | 74 | #region ================= Demos 75 | 76 | public static List GetDemos(string modpath, string demosfolder) 77 | { 78 | var result = new List(); 79 | if(!string.IsNullOrEmpty(demosfolder)) 80 | { 81 | modpath = Path.Combine(modpath, demosfolder); 82 | if(!Directory.Exists(modpath)) return result; 83 | } 84 | 85 | // Get demo files. Can be in subfolders 86 | foreach(string ext in GameHandler.Current.SupportedDemoExtensions) // .dem, etc 87 | { 88 | // Try to get data from demo files... 89 | foreach(string file in Directory.GetFiles(modpath, "*" + ext, SearchOption.AllDirectories)) 90 | { 91 | if(!file.EndsWith(ext, StringComparison.OrdinalIgnoreCase)) continue; // A searchPattern with a file extension (for example *.txt) of exactly three characters returns files 92 | // having an extension of three or more characters, where the first three characters match the file extension specified in the searchPattern. 93 | string relativedemopath = file.Substring(modpath.Length + 1); 94 | 95 | using(var stream = File.OpenRead(file)) 96 | using(var br = new BinaryReader(stream, Encoding.ASCII)) 97 | GameHandler.Current.AddDemoItem(relativedemopath, result, br, restype); 98 | } 99 | } 100 | 101 | return result; 102 | } 103 | 104 | #endregion 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /SQL2/Properties/Resources.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace mxd.SQL2.Properties { 12 | using System; 13 | 14 | 15 | /// 16 | /// A strongly-typed resource class, for looking up localized strings, etc. 17 | /// 18 | // This class was auto-generated by the StronglyTypedResourceBuilder 19 | // class via a tool like ResGen or Visual Studio. 20 | // To add or remove a member, edit your .ResX file then rerun ResGen 21 | // with the /str option, or rebuild your VS project. 22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] 23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 25 | internal class Resources { 26 | 27 | private static global::System.Resources.ResourceManager resourceMan; 28 | 29 | private static global::System.Globalization.CultureInfo resourceCulture; 30 | 31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 32 | internal Resources() { 33 | } 34 | 35 | /// 36 | /// Returns the cached ResourceManager instance used by this class. 37 | /// 38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 39 | internal static global::System.Resources.ResourceManager ResourceManager { 40 | get { 41 | if (object.ReferenceEquals(resourceMan, null)) { 42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("mxd.SQL2.Properties.Resources", typeof(Resources).Assembly); 43 | resourceMan = temp; 44 | } 45 | return resourceMan; 46 | } 47 | } 48 | 49 | /// 50 | /// Overrides the current thread's CurrentUICulture property for all 51 | /// resource lookups using this strongly typed resource class. 52 | /// 53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 54 | internal static global::System.Globalization.CultureInfo Culture { 55 | get { 56 | return resourceCulture; 57 | } 58 | set { 59 | resourceCulture = value; 60 | } 61 | } 62 | 63 | /// 64 | /// Looks up a localized resource of type System.Drawing.Bitmap. 65 | /// 66 | internal static System.Drawing.Bitmap Copy { 67 | get { 68 | object obj = ResourceManager.GetObject("Copy", resourceCulture); 69 | return ((System.Drawing.Bitmap)(obj)); 70 | } 71 | } 72 | 73 | /// 74 | /// Looks up a localized resource of type System.Drawing.Bitmap. 75 | /// 76 | internal static System.Drawing.Bitmap Delete { 77 | get { 78 | object obj = ResourceManager.GetObject("Delete", resourceCulture); 79 | return ((System.Drawing.Bitmap)(obj)); 80 | } 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /SQL2/Tools/DisplayTools.cs: -------------------------------------------------------------------------------- 1 | #region ================= Namespaces 2 | 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Runtime.InteropServices; 6 | using System.Windows.Forms; 7 | using mxd.SQL2.Data; 8 | using mxd.SQL2.Items; 9 | 10 | #endregion 11 | 12 | namespace mxd.SQL2.Tools 13 | { 14 | internal static class DisplayTools 15 | { 16 | #region ================= Imports/consts 17 | 18 | [DllImport("user32.dll")] 19 | private static extern bool EnumDisplaySettings(string deviceName, int modeNum, ref DeviceMode devMode); 20 | 21 | #endregion 22 | 23 | #region ================= Structs 24 | 25 | [StructLayout(LayoutKind.Sequential)] 26 | private struct DeviceMode 27 | { 28 | private const int CCHDEVICENAME = 0x20; 29 | private const int CCHFORMNAME = 0x20; 30 | [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 0x20)] 31 | public string dmDeviceName; 32 | public short dmSpecVersion; 33 | public short dmDriverVersion; 34 | public short dmSize; 35 | public short dmDriverExtra; 36 | public int dmFields; 37 | public int dmPositionX; 38 | public int dmPositionY; 39 | public ScreenOrientation dmDisplayOrientation; 40 | public int dmDisplayFixedOutput; 41 | public short dmColor; 42 | public short dmDuplex; 43 | public short dmYResolution; 44 | public short dmTTOption; 45 | public short dmCollate; 46 | [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 0x20)] 47 | public string dmFormName; 48 | public short dmLogPixels; 49 | public int dmBitsPerPel; 50 | public int dmPelsWidth; 51 | public int dmPelsHeight; 52 | public int dmDisplayFlags; 53 | public int dmDisplayFrequency; 54 | public int dmICMMethod; 55 | public int dmICMIntent; 56 | public int dmMediaType; 57 | public int dmDitherType; 58 | public int dmReserved1; 59 | public int dmReserved2; 60 | public int dmPanningWidth; 61 | public int dmPanningHeight; 62 | } 63 | 64 | #endregion 65 | 66 | #region ================= Methods 67 | 68 | public static List GetVideoModes() 69 | { 70 | var dm = new DeviceMode(); 71 | var modes = new Dictionary(1); 72 | var screenarea = Screen.PrimaryScreen.WorkingArea; 73 | var screenres = Screen.PrimaryScreen.Bounds; 74 | int i = 0; 75 | 76 | while(EnumDisplaySettings(null, i++, ref dm)) 77 | { 78 | string key = dm.dmPelsWidth + "x" + dm.dmPelsHeight; 79 | if(!modes.ContainsKey(key)) 80 | { 81 | if(dm.dmPelsWidth < screenarea.Width && dm.dmPelsHeight < screenarea.Height) 82 | modes.Add(key, new ResolutionItem(dm.dmPelsWidth, dm.dmPelsHeight)); 83 | else if(dm.dmPelsWidth == screenres.Width && dm.dmPelsHeight == screenres.Height) 84 | modes.Add(key, new ResolutionItem(dm.dmPelsWidth, dm.dmPelsHeight, -1, true)); 85 | } 86 | } 87 | 88 | // Sort in descending order... 89 | var result = modes.Values.ToList(); 90 | result.Sort((i1, i2) => (i1.Width == i2.Width ? i1.Height.CompareTo(i2.Height) : i1.Width.CompareTo(i2.Width)) * -1); 91 | return result; 92 | } 93 | 94 | public static List GetFixedVideoModes(List rmodes) 95 | { 96 | var screenarea = Screen.PrimaryScreen.WorkingArea; 97 | var screenres = Screen.PrimaryScreen.Bounds; 98 | var result = new List(); 99 | 100 | // Pick all the modes smaller than screenarea or equal to screenres... 101 | foreach(var vmi in rmodes) 102 | { 103 | if(vmi.Width < screenarea.Width && vmi.Height < screenarea.Height) 104 | result.Add(new ResolutionItem(vmi.Width, vmi.Height, vmi.Index)); 105 | else if(vmi.Width == screenres.Width && vmi.Height == screenres.Height) 106 | result.Add(new ResolutionItem(vmi.Width, vmi.Height, vmi.Index, true)); 107 | } 108 | 109 | // Sort in descending order... 110 | result.Sort((i1, i2) => (i1.Width == i2.Width ? i1.Height.CompareTo(i2.Height) : i1.Width.CompareTo(i2.Width)) * -1); 111 | return result; 112 | } 113 | 114 | #endregion 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /SQL2/Games/Quake/QuakeHandler.cs: -------------------------------------------------------------------------------- 1 | #region ================= Namespaces 2 | 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using mxd.SQL2.DataReaders; 6 | using mxd.SQL2.Items; 7 | 8 | #endregion 9 | 10 | namespace mxd.SQL2.Games.Quake 11 | { 12 | public class QuakeHandler : GameHandler 13 | { 14 | #region ================= Properties 15 | 16 | public override string GameTitle => "Quake"; 17 | 18 | #endregion 19 | 20 | #region ================= Setup 21 | 22 | // Valid Quake path if "id1\pak0.pak" exists, I guess... 23 | protected override bool CanHandle(string gamepath) 24 | { 25 | return File.Exists(Path.Combine(gamepath, "id1\\pak0.pak")); 26 | } 27 | 28 | // Data initialization order matters (horrible, I know...)! 29 | protected override void Setup(string gamepath) 30 | { 31 | // Default mod path 32 | defaultmodpath = Path.Combine(gamepath, "ID1").ToLowerInvariant(); 33 | 34 | // Ignore props 35 | ignoredmapprefix = "b_"; 36 | 37 | // Demo extensions 38 | supporteddemoextensions.Add(".dem"); 39 | supporteddemoextensions.Add(".mvd"); // net Quake only? 40 | supporteddemoextensions.Add(".qwd"); // net Quake only? 41 | 42 | // Setup map delegates 43 | getfoldermaps = DirectoryReader.GetMaps; 44 | getpakmaps = PAKReader.GetMaps; 45 | getpk3maps = PK3Reader.GetMaps; 46 | 47 | foldercontainsmaps = DirectoryReader.ContainsMaps; 48 | pakscontainmaps = PAKReader.ContainsMaps; 49 | pk3scontainmaps = PK3Reader.ContainsMaps; 50 | 51 | getmapinfo = QuakeBSPReader.GetMapInfo; 52 | 53 | // Setup demo delegates 54 | getfolderdemos = DirectoryReader.GetDemos; 55 | getpakdemos = PAKReader.GetDemos; 56 | getpk3demos = PK3Reader.GetDemos; 57 | 58 | getdemoinfo = QuakeDemoReader.GetDemoInfo; 59 | 60 | // Setup file checking delegates 61 | pakscontainfile = PAKReader.ContainsFile; 62 | pk3scontainfile = PK3Reader.ContainsFile; 63 | 64 | // Setup fullscreen args... 65 | fullscreenarg[true] = string.Empty; 66 | fullscreenarg[false] = "-window "; 67 | 68 | // Setup launch params 69 | launchparams[ItemType.ENGINE] = string.Empty; 70 | launchparams[ItemType.RESOLUTION] = "{2}-width {0} -height {1}"; 71 | launchparams[ItemType.GAME] = string.Empty; 72 | launchparams[ItemType.MOD] = "-game {0}"; 73 | launchparams[ItemType.MAP] = "+map {0}"; 74 | launchparams[ItemType.SKILL] = "+skill {0}"; 75 | launchparams[ItemType.CLASS] = string.Empty; 76 | launchparams[ItemType.DEMO] = "+playdemo {0}"; 77 | 78 | // Setup skills (requires launchparams) 79 | skills.AddRange(new[] 80 | { 81 | new SkillItem("Easy", "0"), 82 | new SkillItem("Normal", "1", true), 83 | new SkillItem("Hard", "2"), 84 | new SkillItem("Nightmare!", "3", false, true) 85 | }); 86 | 87 | // Setup basegames (requires defaultmodpath) 88 | basegames["ID1"] = new GameItem("Quake", "id1", ""); 89 | basegames["QUOTH"] = new GameItem("Quoth", "quoth", "-quoth"); 90 | basegames["NEHAHRA"] = new GameItem("Nehahra", "nehahra", "-nehahra"); 91 | basegames["HIPNOTIC"] = new GameItem("MP1: Scourge of Armagon", "hipnotic", "-hipnotic"); 92 | basegames["ROGUE"] = new GameItem("MP2: Dissolution of Eternity", "rogue", "-rogue"); 93 | 94 | // Pass on to base... 95 | base.Setup(gamepath); 96 | } 97 | 98 | #endregion 99 | 100 | #region ================= Methods 101 | 102 | public override List GetMods() 103 | { 104 | var result = new List(); 105 | GetMods(gamepath, result); 106 | return result; 107 | } 108 | 109 | private void GetMods(string path, ICollection result) 110 | { 111 | foreach(string folder in Directory.GetDirectories(path)) 112 | { 113 | if(!Directory.Exists(folder)) continue; 114 | 115 | string name = folder.Substring(gamepath.Length + 1); 116 | if(basegames.ContainsKey(name)) 117 | { 118 | result.Add(new ModItem(name, folder, true)); 119 | continue; 120 | } 121 | 122 | // Count folder as a mod when it contains "progs.dat"... 123 | if(File.Exists(Path.Combine(folder, "progs.dat")) || pakscontainfile(folder, "progs.dat") || pk3scontainfile(folder, "progs.dat")) 124 | { 125 | result.Add(new ModItem(name, folder)); 126 | continue; 127 | } 128 | 129 | // If current folder has no maps, try subfolders... 130 | if(!foldercontainsmaps(folder) && !pakscontainmaps(folder) && !pk3scontainmaps(folder)) 131 | { 132 | GetMods(folder, result); 133 | continue; 134 | } 135 | 136 | result.Add(new ModItem(name, folder)); 137 | } 138 | } 139 | 140 | #endregion 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /SQL2/Data/Configuration.cs: -------------------------------------------------------------------------------- 1 | #region ================= Namespaces 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | using System.IO; 6 | using System.Text; 7 | using mxd.SQL2.Items; 8 | 9 | #endregion 10 | 11 | namespace mxd.SQL2.Data 12 | { 13 | static class Configuration 14 | { 15 | #region ================= Properties 16 | 17 | public static string Engine = string.Empty; 18 | public static string WindowSize = string.Empty; 19 | public static string Mod = string.Empty; 20 | public static string Map = string.Empty; 21 | public static string Demo = string.Empty; 22 | public static string Skill = string.Empty; 23 | public static string Class = string.Empty; // [Hexen 2] 24 | public static int Game; 25 | public static Dictionary ExtraArguments = new Dictionary(); // LaunchParameterType, custom arg 26 | 27 | #endregion 28 | 29 | #region ================= Variables 30 | 31 | // Local stuff 32 | private static string configpath; 33 | private static readonly string[] separator = { " = " }; 34 | private static readonly string[] extraargsseparator = { "|" }; 35 | 36 | #endregion 37 | 38 | #region ================= Save/Load 39 | 40 | public static void Load(string path) 41 | { 42 | configpath = path; 43 | if(!File.Exists(configpath)) return; 44 | 45 | // Read values 46 | string[] lines = File.ReadAllLines(configpath); 47 | foreach (string line in lines) 48 | { 49 | string[] bits = line.Split(separator, StringSplitOptions.RemoveEmptyEntries); 50 | if(bits.Length != 2) continue; 51 | 52 | string key = bits[0].ToLower().Trim(); 53 | string value = bits[1].Trim(); 54 | if(string.IsNullOrEmpty(key) || string.IsNullOrEmpty(value)) continue; 55 | 56 | switch(key) 57 | { 58 | case "extraargs": 59 | ExtraArguments = new Dictionary(); 60 | string[] args = value.Split(extraargsseparator, StringSplitOptions.RemoveEmptyEntries); 61 | foreach(string arg in args) 62 | { 63 | string typestr = arg.Substring(0, 2); 64 | ItemType type = ItemTypes.Markers.ContainsKey(typestr) ? ItemTypes.Markers[typestr] : ItemType.ENGINE; 65 | string trimmedarg = arg.Substring(2).Trim(); 66 | if(string.IsNullOrEmpty(trimmedarg)) continue; 67 | 68 | if(ExtraArguments.ContainsKey(type)) 69 | ExtraArguments[type] += " " + trimmedarg; 70 | else 71 | ExtraArguments[type] = trimmedarg; 72 | } 73 | break; 74 | 75 | case "resolution": WindowSize = value; break; 76 | case "engine": Engine = value; break; 77 | case "game": Mod = value; break; 78 | case "map": Map = value; break; 79 | case "demo": Demo = value; break; 80 | case "skill": Skill = value; break; 81 | case "class": Class = value; break; 82 | case "basegame": int.TryParse(value, out Game); break; 83 | 84 | default: 85 | System.Windows.MessageBox.Show("Got unknown configuration parameter:\n'" + line + "'", App.ErrorMessageTitle); 86 | break; 87 | } 88 | } 89 | } 90 | 91 | public static void Save() 92 | { 93 | var sb = new StringBuilder(100); 94 | 95 | if(!string.IsNullOrEmpty(Engine)) sb.AppendLine("engine" + separator[0] + Engine); 96 | if(!string.IsNullOrEmpty(WindowSize)) sb.AppendLine("resolution" + separator[0] + WindowSize); 97 | if(!string.IsNullOrEmpty(Mod)) sb.AppendLine("game" + separator[0] + Mod); 98 | if(!string.IsNullOrEmpty(Map)) sb.AppendLine("map" + separator[0] + Map); 99 | if(!string.IsNullOrEmpty(Demo)) sb.AppendLine("demo" + separator[0] + Demo); 100 | if(!string.IsNullOrEmpty(Skill) && Skill != SkillItem.Default.Value) sb.AppendLine("skill" + separator[0] + Skill); 101 | if(!string.IsNullOrEmpty(Class) && Class != ClassItem.Default.Value) sb.AppendLine("class" + separator[0] + Class); 102 | if(Game > 0) sb.AppendLine("basegame" + separator[0] + Game); 103 | if(ExtraArguments.Count > 0) 104 | { 105 | var args = new List(); 106 | foreach(var group in ExtraArguments) 107 | { 108 | if(!string.IsNullOrEmpty(group.Value)) 109 | args.Add(ItemTypes.Types[group.Key] + " " + group.Value); 110 | } 111 | 112 | if(args.Count > 0) sb.AppendLine("extraargs" + separator[0] + string.Join(extraargsseparator[0], args.ToArray())); 113 | } 114 | 115 | try 116 | { 117 | using(StreamWriter writer = File.CreateText(configpath)) writer.Write(sb); 118 | } 119 | catch(Exception ex) 120 | { 121 | System.Windows.MessageBox.Show("Unable to save configuration file '" + configpath + "'!\n" + ex.GetType().Name + ": " + ex.Message, App.ErrorMessageTitle); 122 | } 123 | } 124 | 125 | #endregion 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /SQL2/Games/Hexen2/Hexen2Handler.cs: -------------------------------------------------------------------------------- 1 | #region ================= Namespaces 2 | 3 | using System.Collections.Generic; 4 | using System.Globalization; 5 | using System.IO; 6 | using mxd.SQL2.DataReaders; 7 | using mxd.SQL2.Games.Quake; 8 | using mxd.SQL2.Items; 9 | 10 | #endregion 11 | 12 | namespace mxd.SQL2.Games.Hexen2 13 | { 14 | public class Hexen2Handler : GameHandler 15 | { 16 | #region ================= Variables 17 | 18 | private List strings; // Contents of strings.txt in the current moddir 19 | 20 | #endregion 21 | 22 | #region ================= Properties 23 | 24 | public override string GameTitle => "Hexen II"; 25 | 26 | #endregion 27 | 28 | #region ================= Setup 29 | 30 | // Valid Hexen 2 path if "data1\pak0.pak" and "data1\pak1.pak" exist, I guess... 31 | protected override bool CanHandle(string gamepath) 32 | { 33 | foreach(var p in new[] { "data1\\pak0.pak", "data1\\pak1.pak" }) 34 | if(!File.Exists(Path.Combine(gamepath, p))) return false; 35 | 36 | return true; 37 | } 38 | 39 | // Data initialization order matters (horrible, I know...)! 40 | protected override void Setup(string gamepath) 41 | { 42 | // Default mod path 43 | defaultmodpath = Path.Combine(gamepath, "DATA1").ToLowerInvariant(); 44 | 45 | // Nothing to ignore 46 | ignoredmapprefix = string.Empty; 47 | 48 | // Demo extensions 49 | supporteddemoextensions.Add(".dem"); 50 | 51 | // Setup map delegates 52 | getfoldermaps = DirectoryReader.GetMaps; 53 | getpakmaps = PAKReader.GetMaps; 54 | getpk3maps = PK3Reader.GetMaps; 55 | 56 | foldercontainsmaps = DirectoryReader.ContainsMaps; 57 | pakscontainmaps = PAKReader.ContainsMaps; 58 | pk3scontainmaps = PK3Reader.ContainsMaps; 59 | 60 | getmapinfo = QuakeBSPReader.GetMapInfo; 61 | 62 | // Setup demo delegates 63 | getfolderdemos = DirectoryReader.GetDemos; 64 | getpakdemos = PAKReader.GetDemos; 65 | getpk3demos = PK3Reader.GetDemos; 66 | 67 | getdemoinfo = Hexen2DemoReader.GetDemoInfo; 68 | 69 | // Setup file checking delegates 70 | pakscontainfile = PAKReader.ContainsFile; 71 | pk3scontainfile = PK3Reader.ContainsFile; 72 | 73 | // Setup fullscreen args... 74 | fullscreenarg[true] = string.Empty; 75 | fullscreenarg[false] = "-window "; 76 | 77 | // Setup launch params 78 | launchparams[ItemType.ENGINE] = string.Empty; 79 | launchparams[ItemType.RESOLUTION] = "{2}-width {0} -height {1}"; 80 | launchparams[ItemType.GAME] = string.Empty; 81 | launchparams[ItemType.MOD] = "-game {0}"; 82 | launchparams[ItemType.MAP] = "+map {0}"; 83 | launchparams[ItemType.SKILL] = "+skill {0}"; 84 | launchparams[ItemType.CLASS] = "+playerclass {0}"; 85 | launchparams[ItemType.DEMO] = "+playdemo {0}"; 86 | 87 | // Setup skills (requires launchparams) 88 | skills.AddRange(new[] 89 | { 90 | new SkillItem("Easy", "0"), 91 | new SkillItem("Medium", "1", true), 92 | new SkillItem("Hard", "2"), 93 | new SkillItem("Very Hard", "3", false, true) 94 | }); 95 | 96 | // Setup classes (requires launchparams) 97 | classes.AddRange(new[] 98 | { 99 | // Hexen 2 stores last used playerclass, so no defaults here... 100 | new ClassItem("Paladin", "1"), 101 | new ClassItem("Crusader", "2"), 102 | new ClassItem("Necromancer", "3"), 103 | new ClassItem("Assassin", "4"), 104 | new ClassItem("Demoness", "5") 105 | }); 106 | 107 | // Setup basegames (requires defaultmodpath) 108 | basegames["DATA1"] = new GameItem("Hexen II", "data1", ""); 109 | basegames["PORTALS"] = new GameItem("H2MP: Portal of Praevus", "portals", "-portals"); 110 | 111 | // Initialize collections 112 | strings = new List(); 113 | 114 | // Pass on to base... 115 | base.Setup(gamepath); 116 | } 117 | 118 | #endregion 119 | 120 | #region ================= Methods 121 | 122 | public override List GetMods() 123 | { 124 | var result = new List(); 125 | 126 | foreach(string folder in Directory.GetDirectories(gamepath)) 127 | { 128 | if(!Directory.Exists(folder)) continue; 129 | 130 | string name = folder.Substring(gamepath.Length + 1); 131 | if(basegames.ContainsKey(name)) 132 | { 133 | result.Add(new ModItem(name, folder, true)); 134 | continue; 135 | } 136 | 137 | // Count folder as a mod when it contains "progs.dat"... 138 | if(File.Exists(Path.Combine(folder, "progs.dat")) || pakscontainfile(folder, "progs.dat") || pk3scontainfile(folder, "progs.dat")) 139 | { 140 | result.Add(new ModItem(name, folder)); 141 | continue; 142 | } 143 | 144 | // Skip folder if it has no maps 145 | if(!foldercontainsmaps(folder) && !pakscontainmaps(folder) && !pk3scontainmaps(folder)) 146 | continue; 147 | 148 | result.Add(new ModItem(name, folder)); 149 | } 150 | 151 | return result; 152 | } 153 | 154 | public override List GetMaps(string modpath) 155 | { 156 | // Safety foist... 157 | if(!Directory.Exists(modpath)) return new List(); 158 | 159 | // Get contents of strings.txt... 160 | strings = new List(); // Actually faster than Clear() 161 | var stringspath = Path.Combine(modpath, "strings.txt"); 162 | if(File.Exists(stringspath)) strings.AddRange(File.ReadAllLines(stringspath)); 163 | 164 | // Pass on to base... 165 | return base.GetMaps(modpath); 166 | } 167 | 168 | public override string CheckMapTitle(string title) 169 | { 170 | // Title is a number? 171 | int index; 172 | if(int.TryParse(title, NumberStyles.Integer, CultureInfo.InvariantCulture, out index)) 173 | { 174 | index--; // Make it 0-based... 175 | if(index >= strings.Count || index < 0) 176 | return "[Invalid string index: " + index + "]"; 177 | 178 | if(string.IsNullOrEmpty(strings[index])) 179 | return "[Empty string index: " + index + "]"; 180 | 181 | title = strings[index]; 182 | if(title.Length > 64) title = title.Substring(0, 64); // What's the actual limit, BTW?.. 183 | } 184 | 185 | return base.CheckMapTitle(title); 186 | } 187 | 188 | #endregion 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /SQL2/DataReaders/PK3Reader.cs: -------------------------------------------------------------------------------- 1 | #region ================= Namespaces 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | using System.IO; 6 | using System.IO.Compression; 7 | using System.Text; 8 | using mxd.SQL2.Games; 9 | using mxd.SQL2.Items; 10 | using mxd.SQL2.Tools; 11 | 12 | #endregion 13 | 14 | namespace mxd.SQL2.DataReaders 15 | { 16 | public static class PK3Reader 17 | { 18 | #region ================= Variables 19 | 20 | private const ResourceType restype = ResourceType.PK3; 21 | 22 | #endregion 23 | 24 | #region ================= Maps 25 | 26 | public static void GetMaps(string modpath, Dictionary mapslist, GameHandler.GetMapInfoDelegate getmapinfo) 27 | { 28 | string[] zipfiles = Directory.GetFiles(modpath, "*.pk3"); 29 | foreach(string file in zipfiles) 30 | { 31 | if(!file.EndsWith(".pk3", StringComparison.OrdinalIgnoreCase)) continue; 32 | using(var arc = ZipFile.OpenRead(file)) 33 | { 34 | foreach(var e in arc.Entries) 35 | { 36 | if(!GameHandler.Current.EntryIsMap(e.FullName, mapslist)) continue; 37 | string mapname = Path.GetFileNameWithoutExtension(e.Name); 38 | MapItem mapitem; 39 | 40 | if(getmapinfo != null) 41 | { 42 | using(var stream = e.Open()) 43 | { 44 | var wrapper = new DeflateStreamWrapper((DeflateStream)stream, e.Length); 45 | using(var reader = new BinaryReader(wrapper, Encoding.ASCII)) 46 | mapitem = getmapinfo(mapname, reader, restype); 47 | } 48 | } 49 | else 50 | { 51 | mapitem = new MapItem(mapname, restype); 52 | } 53 | 54 | // Add to collection 55 | mapslist.Add(mapname, mapitem); 56 | } 57 | } 58 | } 59 | } 60 | 61 | public static bool ContainsMaps(string modpath) 62 | { 63 | string[] zipfiles = Directory.GetFiles(modpath, "*.pk3"); 64 | string prefix = GameHandler.Current.IgnoredMapPrefix; 65 | 66 | foreach(string file in zipfiles) 67 | { 68 | if(!file.EndsWith(".pk3", StringComparison.OrdinalIgnoreCase)) continue; 69 | using(FileStream stream = File.OpenRead(file)) 70 | { 71 | using(BinaryReader reader = new BinaryReader(stream, Encoding.ASCII)) 72 | { 73 | // Traverse file entries 74 | while(reader.ReadStringExactLength(4) == "PK\x03\x04") 75 | { 76 | reader.BaseStream.Position += 14; 77 | int compressedsize = reader.ReadInt32(); 78 | reader.BaseStream.Position += 4; 79 | short filenamelength = reader.ReadInt16(); 80 | short extralength = reader.ReadInt16(); 81 | string entry = reader.ReadStringExactLength(filenamelength); 82 | 83 | if(Path.GetDirectoryName(entry.ToLower()) == "maps" && Path.GetExtension(entry).ToLower() == ".bsp" 84 | && (string.IsNullOrEmpty(prefix) || !Path.GetFileName(entry).StartsWith(prefix)) ) 85 | { 86 | return true; 87 | } 88 | 89 | reader.BaseStream.Position += extralength + compressedsize; 90 | } 91 | } 92 | } 93 | } 94 | 95 | // 2 SLOW 4 US 96 | /*foreach(string file in zipfiles) 97 | { 98 | using(var arc = ZipFile.OpenRead(file)) 99 | { 100 | foreach(var entry in arc.Entries) 101 | { 102 | if(Path.GetDirectoryName(entry.FullName.ToLower()) == "maps" && Path.GetExtension(entry.FullName).ToLower() == ".bsp") 103 | { 104 | string mapname = Path.GetFileNameWithoutExtension(entry.Name); 105 | if(string.IsNullOrEmpty(prefix) || !mapname.StartsWith(prefix)) 106 | return true; 107 | } 108 | } 109 | } 110 | }*/ 111 | 112 | return false; 113 | } 114 | 115 | #endregion 116 | 117 | #region ================= Demos 118 | 119 | public static List GetDemos(string modpath, string demosfolder) 120 | { 121 | string[] zipfiles = Directory.GetFiles(modpath, "*.pk3"); 122 | var result = new List(); 123 | 124 | foreach(string file in zipfiles) 125 | { 126 | if(!file.EndsWith(".pk3", StringComparison.OrdinalIgnoreCase)) continue; 127 | using(var arc = ZipFile.OpenRead(file)) 128 | { 129 | foreach(var e in arc.Entries) 130 | { 131 | string entry = e.FullName; 132 | 133 | // Skip unrelated files... 134 | if(!GameHandler.Current.SupportedDemoExtensions.Contains(Path.GetExtension(entry))) 135 | continue; 136 | 137 | // If demosfolder is given, skip items not within said folder... 138 | if(!string.IsNullOrEmpty(demosfolder)) 139 | { 140 | // If demosfolder is given, skip items not within said folder... 141 | if(!entry.StartsWith(demosfolder, StringComparison.OrdinalIgnoreCase)) 142 | continue; 143 | 144 | // Strip "demos" from the entry name (Q2 expects path relative to "demos" folder) 145 | entry = entry.Substring(demosfolder.Length + 1); 146 | } 147 | 148 | using(var stream = e.Open()) 149 | { 150 | var wrapper = new DeflateStreamWrapper((DeflateStream)stream, e.Length); 151 | using(var reader = new BinaryReader(wrapper)) 152 | GameHandler.Current.AddDemoItem(entry, result, reader, restype); 153 | } 154 | } 155 | } 156 | } 157 | 158 | return result; 159 | } 160 | 161 | #endregion 162 | 163 | #region ================= Files 164 | 165 | public static bool ContainsFile(string modpath, string filename) 166 | { 167 | string[] zipfiles = Directory.GetFiles(modpath, "*.pk3"); 168 | 169 | foreach (string file in zipfiles) 170 | { 171 | if (!file.EndsWith(".pk3", StringComparison.OrdinalIgnoreCase)) continue; 172 | using (FileStream stream = File.OpenRead(file)) 173 | { 174 | using (BinaryReader reader = new BinaryReader(stream, Encoding.ASCII)) 175 | { 176 | // Traverse file entries 177 | while (reader.ReadStringExactLength(4) == "PK\x03\x04") 178 | { 179 | reader.BaseStream.Position += 14; 180 | int compressedsize = reader.ReadInt32(); 181 | reader.BaseStream.Position += 4; 182 | short filenamelength = reader.ReadInt16(); 183 | short extralength = reader.ReadInt16(); 184 | string entry = reader.ReadStringExactLength(filenamelength); 185 | 186 | if (string.CompareOrdinal(entry, filename) == 0) 187 | return true; 188 | 189 | reader.BaseStream.Position += extralength + compressedsize; 190 | } 191 | } 192 | } 193 | } 194 | 195 | return false; 196 | } 197 | 198 | #endregion 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /SQL2/Games/HalfLife/HalfLifeHandler.cs: -------------------------------------------------------------------------------- 1 | #region ================= Namespaces 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | using System.IO; 6 | using mxd.SQL2.Data; 7 | using mxd.SQL2.DataReaders; 8 | using mxd.SQL2.Items; 9 | using mxd.SQL2.Tools; 10 | 11 | #endregion 12 | 13 | namespace mxd.SQL2.Games.HalfLife 14 | { 15 | public class HalfLifeHandler : GameHandler 16 | { 17 | #region ================= Variables 18 | 19 | private Dictionary knowngamefolders; // Folder names and titles for official expansions, 20 | private List rmodes; 21 | private HashSet nonengines; // HL comes with a lot of unrelated exes... 22 | 23 | #endregion 24 | 25 | #region ================= Properties 26 | 27 | public override string GameTitle => "Half-Life"; 28 | 29 | #endregion 30 | 31 | #region ================= Setup 32 | 33 | // Valid Half-Life path if "valve\pak0.pak" exists, I guess... 34 | protected override bool CanHandle(string gamepath) 35 | { 36 | return File.Exists(Path.Combine(gamepath, "valve\\pak0.pak")) // HL Classic 37 | || File.Exists(Path.Combine(gamepath, "valve\\maps\\c0a0.bsp")); // HL GoldSource 38 | } 39 | 40 | // Data initialization order matters (horrible, I know...)! 41 | protected override void Setup(string gamepath) 42 | { 43 | // Default mod path 44 | defaultmodpath = Path.Combine(gamepath, "valve").ToLowerInvariant(); 45 | 46 | // Nothing to ignore 47 | ignoredmapprefix = string.Empty; 48 | 49 | // Demo extensions 50 | supporteddemoextensions.Add(".dem"); 51 | 52 | // Setup map delegates 53 | getfoldermaps = DirectoryReader.GetMaps; 54 | getpakmaps = PAKReader.GetMaps; 55 | getpk3maps = null; // No PK3 support in HL 56 | 57 | foldercontainsmaps = DirectoryReader.ContainsMaps; 58 | pakscontainmaps = PAKReader.ContainsMaps; 59 | pk3scontainmaps = null; // No PK3 support in HL 60 | 61 | getmapinfo = null; // HL maps contain no useful data 62 | 63 | // Setup demo delegates 64 | getfolderdemos = DirectoryReader.GetDemos; 65 | getpakdemos = PAKReader.GetDemos; 66 | getpk3demos = null; // No PK3 support in HL 67 | 68 | getdemoinfo = HalfLifeDemoReader.GetDemoInfo; 69 | 70 | // Setup fullscreen args... 71 | fullscreenarg[true] = "1"; 72 | fullscreenarg[false] = "0"; 73 | 74 | // Setup launch params 75 | launchparams[ItemType.ENGINE] = string.Empty; 76 | launchparams[ItemType.RESOLUTION] = "+fullscreen {1} +vid_mode {0}"; // "-sw -w {0} -h {1}" 77 | launchparams[ItemType.GAME] = string.Empty; 78 | launchparams[ItemType.MOD] = "-game {0}"; 79 | launchparams[ItemType.MAP] = "+map {0}"; 80 | launchparams[ItemType.SKILL] = "+skill {0}"; 81 | launchparams[ItemType.CLASS] = string.Empty; 82 | launchparams[ItemType.DEMO] = "+playdemo {0}"; 83 | 84 | // Setup skills (requires launchparams) 85 | skills.AddRange(new[] 86 | { 87 | new SkillItem("Easy", "0"), 88 | new SkillItem("Medium", "1", true), 89 | new SkillItem("Difficult", "2"), 90 | }); 91 | 92 | // Setup known folders 93 | knowngamefolders = new Dictionary(StringComparer.OrdinalIgnoreCase) 94 | { 95 | { "bshift", "Blue Shift" }, 96 | { "dmc", "Deathmatch Classic" }, 97 | { "gearbox", "Opposing Forces" }, 98 | { "ricochet", "Ricochet" }, 99 | { "tfc", "Team Fortress Classic" }, 100 | }; 101 | 102 | // Setup fixed r_modes... Taken from engine\client\gl_vidnt.c (xash) 103 | int c = 0; 104 | rmodes = new List 105 | { 106 | new VideoModeInfo(640, 480, c++), 107 | new VideoModeInfo(800, 600, c++), 108 | new VideoModeInfo(960, 720, c++), 109 | new VideoModeInfo(1024, 768, c++), 110 | new VideoModeInfo(1152, 864, c++), 111 | new VideoModeInfo(1280, 960, c++), 112 | new VideoModeInfo(1280, 1024, c++), 113 | new VideoModeInfo(1600, 1200, c++), 114 | new VideoModeInfo(2048, 1536, c++), 115 | 116 | new VideoModeInfo(800, 480, c++), 117 | new VideoModeInfo(856, 480, c++), 118 | new VideoModeInfo(960, 540, c++), 119 | new VideoModeInfo(1024, 576, c++), 120 | new VideoModeInfo(1024, 600, c++), 121 | new VideoModeInfo(1280, 720, c++), 122 | new VideoModeInfo(1360, 768, c++), 123 | new VideoModeInfo(1366, 768, c++), 124 | new VideoModeInfo(1440, 900, c++), 125 | new VideoModeInfo(1680, 1050, c++), 126 | new VideoModeInfo(1920, 1080, c++), 127 | new VideoModeInfo(1920, 1200, c++), 128 | new VideoModeInfo(2560, 1440, c++), 129 | new VideoModeInfo(2560, 1600, c++), 130 | new VideoModeInfo(1600, 900, c++), 131 | new VideoModeInfo(3840, 2160, c++), 132 | }; 133 | 134 | // Setup non-engines... 135 | nonengines = new HashSet(StringComparer.OrdinalIgnoreCase) 136 | { 137 | "hlupdate", 138 | "opforup", 139 | "SierraUp", 140 | "upd", 141 | "UtDel32", 142 | "voice_tweak", 143 | }; 144 | 145 | // Pass on to base... 146 | base.Setup(gamepath); 147 | } 148 | 149 | #endregion 150 | 151 | #region ================= Methods 152 | 153 | public override List GetVideoModes() 154 | { 155 | return DisplayTools.GetFixedVideoModes(rmodes); 156 | } 157 | 158 | public override List GetMods() 159 | { 160 | var result = new List(); 161 | 162 | foreach(string folder in Directory.GetDirectories(gamepath)) 163 | { 164 | // Skip folder if it has no maps or client.dll 165 | if(!foldercontainsmaps(folder) && !pakscontainmaps(folder) && !File.Exists(Path.Combine(folder, "cl_dlls\\client.dll"))) continue; 166 | 167 | string name = folder.Substring(gamepath.Length + 1); 168 | bool isbuiltin = (string.Compare(folder, defaultmodpath, StringComparison.OrdinalIgnoreCase) == 0); 169 | string title = (knowngamefolders.ContainsKey(name) ? knowngamefolders[name] : name); 170 | 171 | result.Add(new ModItem(title, name, folder, isbuiltin)); 172 | } 173 | 174 | // Push known mods above regular ones 175 | result.Sort((i1, i2) => 176 | { 177 | bool firstknown = (i1.Title != i1.Value); 178 | bool secondknown = (i2.Title != i2.Value); 179 | 180 | if(firstknown == secondknown) return string.Compare(i1.Title, i2.Title, StringComparison.Ordinal); 181 | return (firstknown ? -1 : 1); 182 | }); 183 | 184 | return result; 185 | } 186 | 187 | protected override bool IsEngine(string filename) 188 | { 189 | return !nonengines.Contains(Path.GetFileNameWithoutExtension(filename)) && base.IsEngine(filename); 190 | } 191 | 192 | #endregion 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /SQL2/Properties/Resources.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | 122 | ..\Resources\Copy.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a 123 | 124 | 125 | ..\Resources\Delete.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a 126 | 127 | -------------------------------------------------------------------------------- /SQL2/DataReaders/PAKReader.cs: -------------------------------------------------------------------------------- 1 | #region ================= Namespaces 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | using System.IO; 6 | using System.Text; 7 | using mxd.SQL2.Games; 8 | using mxd.SQL2.Items; 9 | using mxd.SQL2.Tools; 10 | 11 | #endregion 12 | 13 | namespace mxd.SQL2.DataReaders 14 | { 15 | public static class PAKReader 16 | { 17 | #region ================= Variables 18 | 19 | private const ResourceType restype = ResourceType.PAK; 20 | 21 | #endregion 22 | 23 | #region ================= Maps 24 | 25 | public static void GetMaps(string modpath, Dictionary mapslist, GameHandler.GetMapInfoDelegate getmapinfo) 26 | { 27 | string[] pakfiles = Directory.GetFiles(modpath, "*.pak"); 28 | foreach(string file in pakfiles) 29 | { 30 | if(!file.EndsWith(".pak", StringComparison.OrdinalIgnoreCase)) continue; 31 | using(FileStream stream = File.OpenRead(file)) 32 | { 33 | using(BinaryReader reader = new BinaryReader(stream, Encoding.ASCII)) 34 | { 35 | // Read header 36 | string id = reader.ReadStringExactLength(4); 37 | if(id != "PACK") continue; 38 | 39 | int ftoffset = reader.ReadInt32(); 40 | int ftsize = reader.ReadInt32() / 64; 41 | 42 | // Read file table 43 | reader.BaseStream.Position = ftoffset; 44 | for(int i = 0; i < ftsize; i++) 45 | { 46 | string entry = reader.ReadStringExactLength(56).Trim(); // Read entry name 47 | int offset = reader.ReadInt32(); 48 | reader.BaseStream.Position += 4; // Skip unrelated stuff 49 | 50 | if(!GameHandler.Current.EntryIsMap(entry, mapslist)) continue; 51 | string mapname = Path.GetFileNameWithoutExtension(entry); 52 | MapItem mapitem; 53 | 54 | if(getmapinfo != null) 55 | { 56 | // Store position 57 | long curpos = reader.BaseStream.Position; 58 | 59 | // Go to data location 60 | reader.BaseStream.Position = offset; 61 | mapitem = getmapinfo(mapname, reader, restype); 62 | 63 | // Restore position 64 | reader.BaseStream.Position = curpos; 65 | } 66 | else 67 | { 68 | mapitem = new MapItem(mapname, restype); 69 | } 70 | 71 | // Add to collection 72 | mapslist.Add(mapname, mapitem); 73 | } 74 | } 75 | } 76 | } 77 | } 78 | 79 | public static bool ContainsMaps(string modpath) 80 | { 81 | string[] pakfiles = Directory.GetFiles(modpath, "*.pak"); 82 | string prefix = GameHandler.Current.IgnoredMapPrefix; 83 | foreach(string file in pakfiles) 84 | { 85 | if(!file.EndsWith(".pak", StringComparison.OrdinalIgnoreCase)) continue; 86 | using(FileStream stream = File.OpenRead(file)) 87 | { 88 | using(BinaryReader reader = new BinaryReader(stream, Encoding.ASCII)) 89 | { 90 | // Read header 91 | string id = reader.ReadStringExactLength(4); 92 | if(id != "PACK") continue; 93 | 94 | int ftoffset = reader.ReadInt32(); 95 | int ftsize = reader.ReadInt32() / 64; 96 | 97 | // Read file table 98 | reader.BaseStream.Position = ftoffset; 99 | for(int i = 0; i < ftsize; i++) 100 | { 101 | string entry = reader.ReadStringExactLength(56).Trim(); // Read entry name 102 | reader.BaseStream.Position += 8; // Skip unrelated stuff 103 | 104 | if(Path.GetDirectoryName(entry.ToLower()) == "maps" && Path.GetExtension(entry).ToLower() == ".bsp" 105 | && (string.IsNullOrEmpty(prefix) || !Path.GetFileName(entry).StartsWith(prefix)) ) 106 | { 107 | return true; 108 | } 109 | } 110 | } 111 | } 112 | } 113 | 114 | return false; 115 | } 116 | 117 | #endregion 118 | 119 | #region ================= Demos 120 | 121 | public static List GetDemos(string modpath, string demosfolder) 122 | { 123 | string[] pakfiles = Directory.GetFiles(modpath, "*.pak"); 124 | var result = new List(); 125 | 126 | // Get demo files 127 | foreach(string file in pakfiles) 128 | { 129 | if(!file.EndsWith(".pak", StringComparison.OrdinalIgnoreCase)) continue; 130 | using(FileStream stream = File.OpenRead(file)) 131 | { 132 | using(BinaryReader reader = new BinaryReader(stream, Encoding.ASCII)) 133 | { 134 | // Read header 135 | string id = reader.ReadStringExactLength(4); 136 | if(id != "PACK") continue; 137 | 138 | int ftoffset = reader.ReadInt32(); 139 | int ftsize = reader.ReadInt32() / 64; 140 | 141 | // Read file table 142 | reader.BaseStream.Position = ftoffset; 143 | for(int i = 0; i < ftsize; i++) 144 | { 145 | string entry = reader.ReadStringExactLength(56).Trim(); // Read entry name 146 | int offset = reader.ReadInt32(); 147 | reader.BaseStream.Position += 4; //skip unrelated stuff 148 | 149 | // Skip unrelated files... 150 | if(!GameHandler.Current.SupportedDemoExtensions.Contains(Path.GetExtension(entry))) 151 | continue; 152 | 153 | if(!string.IsNullOrEmpty(demosfolder)) 154 | { 155 | // If demosfolder is given, skip items not within said folder... 156 | if(!entry.StartsWith(demosfolder, StringComparison.OrdinalIgnoreCase)) 157 | continue; 158 | 159 | // Strip "demos" from the entry name (Q2 expects path relative to "demos" folder) 160 | entry = entry.Substring(demosfolder.Length + 1); 161 | } 162 | 163 | // Store position 164 | long curpos = reader.BaseStream.Position; 165 | 166 | // Go to data location 167 | reader.BaseStream.Position = offset; 168 | 169 | // Add demo data 170 | GameHandler.Current.AddDemoItem(entry, result, reader, restype); 171 | 172 | // Restore position 173 | reader.BaseStream.Position = curpos; 174 | } 175 | } 176 | } 177 | } 178 | 179 | return result; 180 | } 181 | 182 | #endregion 183 | 184 | #region ================= Files 185 | 186 | public static bool ContainsFile(string modpath, string filename) 187 | { 188 | string[] pakfiles = Directory.GetFiles(modpath, "*.pak"); 189 | 190 | foreach (string file in pakfiles) 191 | { 192 | if (!file.EndsWith(".pak", StringComparison.OrdinalIgnoreCase)) continue; 193 | using (FileStream stream = File.OpenRead(file)) 194 | { 195 | using (BinaryReader reader = new BinaryReader(stream, Encoding.ASCII)) 196 | { 197 | // Read header 198 | string id = reader.ReadStringExactLength(4); 199 | if (id != "PACK") continue; 200 | 201 | int ftoffset = reader.ReadInt32(); 202 | int ftsize = reader.ReadInt32() / 64; 203 | 204 | // Read file table 205 | reader.BaseStream.Position = ftoffset; 206 | for (int i = 0; i < ftsize; i++) 207 | { 208 | string entry = reader.ReadStringExactLength(56).Trim(); // Read entry name 209 | reader.BaseStream.Position += 8; // Skip unrelated stuff 210 | 211 | if (string.CompareOrdinal(entry, filename) == 0) 212 | return true; 213 | } 214 | } 215 | } 216 | } 217 | 218 | return false; 219 | } 220 | 221 | #endregion 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /SQL2/Games/Quake2/Quake2Handler.cs: -------------------------------------------------------------------------------- 1 | #region ================= Namespaces 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | using System.IO; 6 | using mxd.SQL2.Data; 7 | using mxd.SQL2.DataReaders; 8 | using mxd.SQL2.Items; 9 | using mxd.SQL2.Tools; 10 | 11 | #endregion 12 | 13 | namespace mxd.SQL2.Games.Quake2 14 | { 15 | public class Quake2Handler : GameHandler 16 | { 17 | #region ================= Variables 18 | 19 | private Dictionary knowngamefolders; // Folder names and titles for official expansions, 20 | private List rmodes; 21 | 22 | #endregion 23 | 24 | #region ================= Properties 25 | 26 | public override string GameTitle => "Quake II"; 27 | 28 | #endregion 29 | 30 | #region ================= Setup 31 | 32 | // Valid Quake 2 path if "baseq2\pak0.pak" and "baseq2\pak1.pak" exist, I guess... 33 | protected override bool CanHandle(string gamepath) 34 | { 35 | foreach(var p in new[] { "baseq2\\pak0.pak", "baseq2\\pak1.pak" }) 36 | if(!File.Exists(Path.Combine(gamepath, p))) return false; 37 | 38 | return true; 39 | } 40 | 41 | // Data initialization order matters (horrible, I know...)! 42 | protected override void Setup(string gamepath) 43 | { 44 | // Default mod path 45 | defaultmodpath = Path.Combine(gamepath, "baseq2").ToLowerInvariant(); 46 | 47 | // Nothing to ignore 48 | ignoredmapprefix = string.Empty; 49 | 50 | // Demo extensions 51 | supporteddemoextensions.Add(".dm2"); 52 | 53 | // Setup map delegates 54 | getfoldermaps = DirectoryReader.GetMaps; 55 | getpakmaps = PAKReader.GetMaps; 56 | getpk3maps = PK3Reader.GetMaps; 57 | 58 | foldercontainsmaps = DirectoryReader.ContainsMaps; 59 | pakscontainmaps = PAKReader.ContainsMaps; 60 | pk3scontainmaps = PK3Reader.ContainsMaps; 61 | 62 | getmapinfo = Quake2BSPReader.GetMapInfo; 63 | 64 | // Setup demo delegates 65 | getfolderdemos = DirectoryReader.GetDemos; 66 | getpakdemos = PAKReader.GetDemos; 67 | getpk3demos = PK3Reader.GetDemos; 68 | 69 | getdemoinfo = Quake2DemoReader.GetDemoInfo; 70 | 71 | // Setup fullscreen args... 72 | fullscreenarg[true] = "1"; 73 | fullscreenarg[false] = "0"; 74 | 75 | // Setup launch params 76 | launchparams[ItemType.ENGINE] = string.Empty; 77 | launchparams[ItemType.RESOLUTION] = "+vid_fullscreen {1} +set r_mode {0}"; // "+vid_fullscreen 0 +r_customwidth {0} +r_customheight {1}" -> works unreliably in KMQuake2, doesn't work in Q2 v3.24 78 | launchparams[ItemType.GAME] = string.Empty; 79 | launchparams[ItemType.MOD] = "+set game {0}"; 80 | launchparams[ItemType.MAP] = "+map {0}"; 81 | launchparams[ItemType.SKILL] = "+set skill {0}"; 82 | launchparams[ItemType.CLASS] = string.Empty; 83 | launchparams[ItemType.DEMO] = "+map {0}"; 84 | 85 | // Setup skills (requires launchparams) 86 | skills.AddRange(new[] 87 | { 88 | new SkillItem("Easy", "0"), 89 | new SkillItem("Medium", "1", true), 90 | new SkillItem("Hard", "2"), 91 | new SkillItem("Nightmare", "3", false, true) 92 | }); 93 | 94 | // Setup known folders 95 | knowngamefolders = new Dictionary(StringComparer.OrdinalIgnoreCase) 96 | { 97 | { "XATRIX", "MP1: The Reckoning" }, 98 | { "ROGUE", "MP2: Ground Zero" }, 99 | }; 100 | 101 | // Setup fixed r_modes... Taken from qcommon\vid_modes.h (KMQ2) 102 | int c = 3; // The first two r_modes are ignored by KMQ2 103 | rmodes = new List 104 | { 105 | new VideoModeInfo(640, 480, c++), 106 | new VideoModeInfo(800, 600, c++), 107 | new VideoModeInfo(960, 720, c++), 108 | new VideoModeInfo(1024, 768, c++), 109 | new VideoModeInfo(1152, 864, c++), 110 | new VideoModeInfo(1280, 960, c++), 111 | new VideoModeInfo(1280, 1024, c++), 112 | new VideoModeInfo(1400, 1050, c++), 113 | new VideoModeInfo(1600, 1200, c++), 114 | new VideoModeInfo(1920, 1440, c++), 115 | new VideoModeInfo(2048, 1536, c++), 116 | 117 | new VideoModeInfo(800, 480, c++), 118 | new VideoModeInfo(856, 480, c++), 119 | new VideoModeInfo(1024, 600, c++), 120 | new VideoModeInfo(1280, 720, c++), 121 | new VideoModeInfo(1280, 768, c++), 122 | new VideoModeInfo(1280, 800, c++), 123 | new VideoModeInfo(1360, 768, c++), 124 | new VideoModeInfo(1366, 768, c++), 125 | new VideoModeInfo(1440, 900, c++), 126 | new VideoModeInfo(1600, 900, c++), 127 | new VideoModeInfo(1600, 1024, c++), 128 | new VideoModeInfo(1680, 1050, c++), 129 | new VideoModeInfo(1920, 1080, c++), 130 | new VideoModeInfo(1920, 1200, c++), 131 | new VideoModeInfo(2560, 1080, c++), 132 | new VideoModeInfo(2560, 1440, c++), 133 | new VideoModeInfo(2560, 1600, c++), 134 | new VideoModeInfo(3200, 1800, c++), 135 | new VideoModeInfo(3440, 1440, c++), 136 | new VideoModeInfo(3840, 2160, c++), 137 | new VideoModeInfo(3840, 2400, c++), 138 | new VideoModeInfo(5120, 2880, c++), 139 | }; 140 | 141 | // Pass on to base... 142 | base.Setup(gamepath); 143 | } 144 | 145 | #endregion 146 | 147 | #region ================= Methods 148 | 149 | public override List GetVideoModes() 150 | { 151 | return DisplayTools.GetFixedVideoModes(rmodes); 152 | } 153 | 154 | public override List GetDemos(string modpath) 155 | { 156 | return GetDemos(modpath, "DEMOS"); 157 | } 158 | 159 | public override List GetMods() 160 | { 161 | var result = new List(); 162 | 163 | foreach(string folder in Directory.GetDirectories(gamepath)) 164 | { 165 | // Skip folder if it has no maps or a variant of "gamex86.dll" 166 | if(!foldercontainsmaps(folder) && !pakscontainmaps(folder) && !pk3scontainmaps(folder) && !ContainsGameDll(folder)) 167 | continue; 168 | 169 | string name = folder.Substring(gamepath.Length + 1); 170 | bool isbuiltin = (string.Compare(folder, defaultmodpath, StringComparison.OrdinalIgnoreCase) == 0); 171 | string title = (knowngamefolders.ContainsKey(name) ? knowngamefolders[name] : name); 172 | 173 | result.Add(new ModItem(title, name, folder, isbuiltin)); 174 | } 175 | 176 | // Push known mods above regular ones 177 | result.Sort((i1, i2) => 178 | { 179 | bool firstknown = (i1.Title != i1.Value); 180 | bool secondknown = (i2.Title != i2.Value); 181 | 182 | if(firstknown == secondknown) return string.Compare(i1.Title, i2.Title, StringComparison.Ordinal); 183 | return (firstknown ? -1 : 1); 184 | }); 185 | 186 | return result; 187 | } 188 | 189 | // Check for a variant of "gamex86.dll". Can be named differently depending on source port... 190 | // Vanilla, UQE Quake2: gamex86.dll 191 | // KMQuake2: kmq2gamex86.dll 192 | // Yamagi Quake2: game.dll 193 | // Quake 2 Evolved: q2e_gamex86.dll 194 | // Quake 2 XP: gamex86xp.dll 195 | private bool ContainsGameDll(string folder) 196 | { 197 | //TODO? Ideally, we should check for game.dll specific to selected game engine, but that would require too much work, including updating mod list when game engine selection changes. 198 | //TODO? So, for now just look for anything resembling game.dll... 199 | var dlls = Directory.GetFiles(folder, "*.dll"); 200 | foreach (var dll in dlls) 201 | if (Path.GetFileName(dll).Contains("game")) 202 | return true; 203 | 204 | return false; 205 | } 206 | 207 | #endregion 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /SQL2/SQL2.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {4B99AA35-9D15-47FA-BEEE-225402E7D77E} 8 | WinExe 9 | Properties 10 | mxd.SQL2 11 | SQLauncher2 12 | v4.5 13 | 512 14 | {60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} 15 | 4 16 | 17 | 18 | 19 | 20 | AnyCPU 21 | true 22 | full 23 | false 24 | bin\Debug\ 25 | DEBUG;TRACE 26 | prompt 27 | 4 28 | false 29 | 30 | 31 | AnyCPU 32 | pdbonly 33 | true 34 | bin\Release\ 35 | TRACE 36 | prompt 37 | 4 38 | false 39 | 40 | 41 | icon.ico 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 4.0 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | MSBuild:Compile 62 | Designer 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | MSBuild:Compile 90 | Designer 91 | 92 | 93 | App.xaml 94 | Code 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | MainWindow.xaml 109 | Code 110 | 111 | 112 | 113 | 114 | Code 115 | 116 | 117 | True 118 | True 119 | Resources.resx 120 | 121 | 122 | True 123 | Settings.settings 124 | True 125 | 126 | 127 | ResXFileCodeGenerator 128 | Resources.Designer.cs 129 | 130 | 131 | SettingsSingleFileGenerator 132 | Settings.Designer.cs 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | {F935DC20-1CF0-11D0-ADB9-00C04FD58A0B} 154 | 1 155 | 0 156 | 0 157 | tlbimp 158 | False 159 | True 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 175 | -------------------------------------------------------------------------------- /SQL2/MainWindow.xaml: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 134 | 135 | -------------------------------------------------------------------------------- /SQL2/Controls/PreviewTextBox.cs: -------------------------------------------------------------------------------- 1 | #region ================= Namespaces 2 | 3 | using System.Collections.Generic; 4 | using System.Windows; 5 | using System.Windows.Controls; 6 | using System.Windows.Documents; 7 | using System.Windows.Input; 8 | using mxd.SQL2.Data; 9 | using mxd.SQL2.Items; 10 | 11 | #endregion 12 | 13 | namespace mxd.SQL2.Controls 14 | { 15 | public class PreviewTextBox : RichTextBox 16 | { 17 | #region ================= Variables 18 | 19 | private const string spacer = " "; 20 | private readonly Paragraph paragraph; 21 | 22 | #endregion 23 | 24 | #region ================= Constructor 25 | 26 | public PreviewTextBox() 27 | { 28 | paragraph = (Paragraph)this.Document.Blocks.FirstBlock; 29 | 30 | // Bind events 31 | DataObject.AddPastingHandler(this, OnPaste); 32 | this.PreviewKeyDown += OnPreviewKeyDown; 33 | this.KeyUp += OnKeyUp; 34 | this.SelectionChanged += OnSelectionChanged; 35 | this.MouseUp += OnMouseUp; 36 | } 37 | 38 | #endregion 39 | 40 | #region ================= Arguments handling 41 | 42 | public void SetArguments(Dictionary args, bool clearcustomargs = false) 43 | { 44 | // Store and reset data 45 | if(clearcustomargs) 46 | Configuration.ExtraArguments.Clear(); 47 | else 48 | StoreCustomArguments(); 49 | 50 | // Create runs 51 | var inlines = new List(); 52 | foreach(var group in args) 53 | { 54 | var arg = group.Value; 55 | var argtype = group.Key; 56 | 57 | // Add arg? 58 | if(arg != null && !arg.IsDefault) 59 | inlines.Add(new PreviewRun(arg.ArgumentPreview, arg, argtype, false)); 60 | 61 | // Add custom arg? Add when extraarg is not empty 62 | if(Configuration.ExtraArguments.ContainsKey(argtype)) 63 | inlines.Add(new PreviewRun(" " + Configuration.ExtraArguments[argtype] + " ", arg, argtype, true)); 64 | } 65 | 66 | // No data? (unlikely to happen) 67 | if(inlines.Count == 0) 68 | { 69 | paragraph.Inlines.Clear(); 70 | return; 71 | } 72 | 73 | // Add spacers... 74 | PreviewRun previous = null; 75 | var inlineswithspacers = new List(); 76 | foreach(var current in inlines) 77 | { 78 | // Insert spacer between non - editable runs 79 | if(!current.IsEditable && previous != null && !previous.IsEditable) 80 | inlineswithspacers.Add(new PreviewRun(spacer, previous.Item, previous.ItemType, true)); 81 | 82 | // Add current arg 83 | inlineswithspacers.Add(current); 84 | 85 | // Store previous arg 86 | previous = current; 87 | } 88 | 89 | // Add spacer after the last arg? 90 | var lastarg = inlines[inlines.Count - 1]; 91 | if(!lastarg.IsEditable) inlineswithspacers.Add(new PreviewRun(spacer, lastarg.Item, lastarg.ItemType, true)); 92 | 93 | // Clear current text 94 | paragraph.Inlines.Clear(); 95 | 96 | // Set new text 97 | paragraph.Inlines.AddRange(inlineswithspacers); 98 | } 99 | 100 | // Command line without engine name 101 | public string GetCommandLine() 102 | { 103 | if(paragraph.Inlines.Count == 0) return string.Empty; 104 | 105 | StoreCustomArguments(); 106 | 107 | var result = new List(); 108 | foreach(PreviewRun run in paragraph.Inlines) 109 | { 110 | // Custom args 111 | if(run.IsEditable) 112 | { 113 | string text = run.Text.Trim(); 114 | if(!string.IsNullOrEmpty(text)) result.Add(text); 115 | } 116 | else // Pre-generated args 117 | { 118 | // Skip engine arg 119 | if(run.ItemType != ItemType.ENGINE) result.Add(run.Item.Argument); 120 | } 121 | } 122 | 123 | return string.Join(" ", result); 124 | } 125 | 126 | private void StoreCustomArguments() 127 | { 128 | if(paragraph.Inlines.Count == 0) return; 129 | 130 | // Get custom args from current text... 131 | foreach(PreviewRun run in paragraph.Inlines) 132 | { 133 | if(run.IsEditable) 134 | { 135 | // Don't store empty strings 136 | string text = run.Text.Trim(); 137 | if(string.IsNullOrEmpty(text)) 138 | Configuration.ExtraArguments.Remove(run.ItemType); 139 | else 140 | Configuration.ExtraArguments[run.ItemType] = text; 141 | } 142 | } 143 | } 144 | 145 | #endregion 146 | 147 | #region ================= Text handling methods 148 | 149 | private List GetSelectedRuns(TextSelection selection) 150 | { 151 | var result = new List(); 152 | if(paragraph.Inlines.Count > 0) 153 | { 154 | var startrun = (PreviewRun)selection.Start.Parent; 155 | result.Add(startrun); 156 | 157 | // Add the rest of the runs, if necessary 158 | if(!selection.IsEmpty) 159 | { 160 | var endrun = selection.End.Parent as PreviewRun; 161 | if(startrun != endrun) 162 | { 163 | var startfound = false; 164 | foreach(PreviewRun run in paragraph.Inlines) 165 | { 166 | // Skip until startrun... 167 | if(!startfound && !run.Equals(startrun)) continue; 168 | 169 | if(!startfound) // First run was already added, so skip it as well... 170 | { 171 | startfound = true; 172 | continue; 173 | } 174 | 175 | result.Add(run); 176 | if(run.Equals(endrun)) break; 177 | } 178 | } 179 | } 180 | } 181 | 182 | return result; 183 | } 184 | 185 | #endregion 186 | 187 | #region ================= Events 188 | 189 | private void OnPreviewKeyDown(object sender, KeyEventArgs e) 190 | { 191 | if(this.IsReadOnly) return; // Can't be edited 192 | 193 | // Delete and Backspace require special handling... 194 | if(e.Key == Key.Delete || e.Key == Key.Back) 195 | { 196 | // Ignore Delete when at the end of editable run, ignore Backspace when at the start of editable run 197 | var currun = (PreviewRun)this.Selection.Start.Parent; 198 | var targetpos = (e.Key == Key.Delete ? currun.ContentEnd : currun.ContentStart); 199 | if(this.Selection.IsEmpty && this.Selection.Start.GetOffsetToPosition(targetpos) == 0) 200 | { 201 | e.Handled = true; 202 | return; 203 | } 204 | 205 | // Don't allow to completely delete editable runs 206 | if(currun.Text.Length < 2) e.Handled = true; 207 | } 208 | } 209 | 210 | private void OnKeyUp(object sender, KeyEventArgs e) 211 | { 212 | var runs = GetSelectedRuns(this.Selection); 213 | if(runs.Count == 1) 214 | { 215 | var currun = (PreviewRun)this.Selection.Start.Parent; 216 | 217 | // Move selection forward when at the end of non-editable run and next run is editable... 218 | if(e.Key == Key.Back && this.Selection.IsEmpty && !currun.IsEditable && currun.NextInline != null 219 | && ((PreviewRun)currun.NextInline).IsEditable && ((PreviewRun)currun.NextInline).Text.Length > 0 220 | && this.Selection.Start.GetOffsetToPosition(currun.ContentEnd) == 0) 221 | { 222 | var nextsel = currun.ContentEnd.GetPositionAtOffset(1, LogicalDirection.Forward); 223 | this.Selection.Select(nextsel, nextsel); 224 | currun = (PreviewRun)currun.NextInline; 225 | } 226 | // Move selection backward when at the start of non-editable run and previous run is editable... 227 | else if(e.Key == Key.Delete && this.Selection.IsEmpty && !currun.IsEditable && currun.PreviousInline != null 228 | && ((PreviewRun)currun.PreviousInline).IsEditable && ((PreviewRun)currun.PreviousInline).Text.Length > 0 229 | && this.Selection.Start.GetOffsetToPosition(currun.ContentStart) == 0) 230 | { 231 | var prevsel = currun.ContentStart.GetPositionAtOffset(-1, LogicalDirection.Backward); 232 | this.Selection.Select(prevsel, prevsel); 233 | currun = (PreviewRun)currun.PreviousInline; 234 | } 235 | 236 | // Add some padding to editable runs... 237 | if(currun.IsEditable) 238 | { 239 | var text = currun.Text; 240 | if(text.Length < 2) 241 | { 242 | currun.Text = " "; 243 | var newsel = currun.ContentStart.GetPositionAtOffset(1, LogicalDirection.Forward); 244 | this.Selection.Select(newsel, newsel); 245 | return; 246 | } 247 | 248 | bool startspaceneeded = !text.StartsWith(" "); 249 | bool endspaceneeded = !text.EndsWith(" "); 250 | if(startspaceneeded || endspaceneeded) 251 | { 252 | int curoffset = currun.ContentStart.GetOffsetToPosition(this.Selection.Start); 253 | if(startspaceneeded) curoffset += 1; 254 | currun.Text = (startspaceneeded ? " " : "") + text + (endspaceneeded ? " " : ""); 255 | var newsel = currun.ContentStart.GetPositionAtOffset(curoffset, LogicalDirection.Forward); 256 | this.Selection.Select(newsel, newsel); 257 | } 258 | } 259 | } 260 | } 261 | 262 | // Clear custom args on MMB click 263 | private void OnMouseUp(object sender, MouseButtonEventArgs e) 264 | { 265 | if(e.ChangedButton != MouseButton.Middle) return; 266 | 267 | // Editable run clicked? 268 | var run = this.GetPositionFromPoint(e.GetPosition(this), true)?.Parent as PreviewRun; 269 | if(run == null || !run.IsEditable || run.Text == spacer) return; 270 | 271 | // Update text 272 | run.Text = spacer; 273 | 274 | // Update selection 275 | var newsel = run.ContentStart.GetPositionAtOffset(1, LogicalDirection.Forward); 276 | this.Selection.Select(newsel, newsel); 277 | 278 | // Update stored args 279 | StoreCustomArguments(); 280 | } 281 | 282 | // Toggle textbox editability based on selection 283 | private void OnSelectionChanged(object sender, RoutedEventArgs e) 284 | { 285 | var runs = GetSelectedRuns(this.Selection); 286 | 287 | // Prevent editing when non-editable runs are selected... 288 | foreach(var run in runs) 289 | { 290 | if(!run.IsEditable) 291 | { 292 | this.IsReadOnly = true; 293 | return; 294 | } 295 | } 296 | 297 | this.IsReadOnly = false; 298 | } 299 | 300 | private void OnPaste(object sender, DataObjectPastingEventArgs e) 301 | { 302 | // Native implementation creates a copy of PreviewRun using parameterless constructor, without needed properties, so cancel that... 303 | e.CancelCommand(); 304 | 305 | var runs = GetSelectedRuns(this.Selection); 306 | if(runs.Count != 1 || !runs[0].IsEditable) return; 307 | 308 | string pasted = Clipboard.GetText(); 309 | if(!string.IsNullOrEmpty(pasted)) 310 | { 311 | // Delete selected text 312 | if(!this.Selection.IsEmpty) 313 | this.Selection.Start.DeleteTextInRun(this.Selection.Start.GetOffsetToPosition(this.Selection.End)); 314 | 315 | // Instert text manually... 316 | this.Selection.Start.InsertTextInRun(pasted); 317 | } 318 | } 319 | 320 | #endregion 321 | } 322 | } 323 | -------------------------------------------------------------------------------- /SQL2/Games/Quake/QuakeDemoReader.cs: -------------------------------------------------------------------------------- 1 | #region ================= Namespaces 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | using System.IO; 6 | using mxd.SQL2.Data; 7 | using mxd.SQL2.DataReaders; 8 | using mxd.SQL2.Items; 9 | using mxd.SQL2.Tools; 10 | 11 | #endregion 12 | 13 | namespace mxd.SQL2.Games.Quake 14 | { 15 | public static class QuakeDemoReader 16 | { 17 | #region ================= Constants 18 | 19 | // DEM protocols 20 | private const int PROTOCOL_NETQUAKE = 15; 21 | private const int PROTOCOL_FITZQUAKE = 666; 22 | private const int PROTOCOL_RMQ = 999; 23 | 24 | // "Special" protocols... 25 | private const int PROTOCOL_FTE = ('F' << 0) + ('T' << 8) + ('E' << 16) + ('X' << 24); 26 | private const int PROTOCOL_FTE2 = ('F' << 0) + ('T' << 8) + ('E' << 16) + ('2' << 24); 27 | 28 | // QW protocols 29 | private static readonly HashSet ProtocolsQW = new HashSet { 24, 25, 26, 27, 28 }; // The not so many QW PROTOCOL_VERSIONs... 30 | 31 | private const int GAME_COOP = 0; 32 | private const int GAME_DEATHMATCH = 1; 33 | 34 | private const int BLOCK_CLIENT = 0; 35 | private const int BLOCK_SERVER = 1; 36 | private const int BLOCK_FRAME = 2; 37 | 38 | private const int SVC_PRINT = 8; 39 | private const int SVC_STUFFTEXT = 9; 40 | private const int SVC_SERVERINFO = 11; 41 | private const int SVC_CDTRACK = 32; 42 | private const int SVC_MODELLIST = 45; 43 | private const int SVC_SOUNDLIST = 46; 44 | 45 | #endregion 46 | 47 | #region ================= GetDemoInfo 48 | 49 | public static DemoItem GetDemoInfo(string demoname, BinaryReader reader, ResourceType restype) 50 | { 51 | string ext = Path.GetExtension(demoname); 52 | if(string.IsNullOrEmpty(ext)) return null; 53 | 54 | switch(ext.ToUpperInvariant()) 55 | { 56 | case ".DEM": return GetDEMInfo(demoname, reader, restype); 57 | case ".MVD": return GetMVDInfo(demoname, reader, restype); 58 | case ".QWD": return GetQWDInfo(demoname, reader, restype); 59 | default: throw new NotImplementedException("Unsupported demo type: " + ext); 60 | } 61 | } 62 | 63 | // https://www.quakewiki.net/archives/demospecs/dem/dem.html 64 | private static DemoItem GetDEMInfo(string demoname, BinaryReader reader, ResourceType restype) 65 | { 66 | // CD track (string terminated by '\n' (0x0A in ASCII)) 67 | if(!reader.SkipString(13, '\n')) return null; 68 | 69 | string maptitle = string.Empty; 70 | string mapfilepath = string.Empty; 71 | int protocol = 0; 72 | bool alldatafound = false; 73 | 74 | // Read blocks... 75 | while(reader.BaseStream.Position < reader.BaseStream.Length) 76 | { 77 | if(alldatafound) break; 78 | 79 | // Block header: 80 | // Block size (int32) 81 | // Camera angles (int32 x 3) 82 | 83 | int blocklength = reader.ReadInt32(); 84 | long blockend = reader.BaseStream.Position + blocklength; 85 | if(blockend >= reader.BaseStream.Length) return null; 86 | reader.BaseStream.Position += 12; // Skip camera angles 87 | 88 | // Read messages... 89 | while(reader.BaseStream.Position < blockend) 90 | { 91 | if(alldatafound) break; 92 | 93 | int message = reader.ReadByte(); 94 | switch(message) 95 | { 96 | // SVC_SERVERINFO (byte, 0x0B) 97 | // protocol (int32) -> 666, 999 or 15 98 | // protocolflags (int32) - only when protocol == 999 99 | // maxclients (byte) should be in 1 .. 16 range 100 | // gametype (byte) - 0 -> coop, 1 -> deathmatch 101 | // map title (null-terminated string) 102 | // map filename (null-terminated string) "maps/mymap.bsp" 103 | 104 | case SVC_SERVERINFO: 105 | protocol = reader.ReadInt32(); 106 | 107 | // FTE2 shenanigans... 108 | if(protocol == PROTOCOL_FTE || protocol == PROTOCOL_FTE2) 109 | { 110 | reader.BaseStream.Position += 4; // Skip fteprotocolextensions or fteprotocolextensions2 (?) 111 | protocol = reader.ReadInt32(); 112 | 113 | if(protocol == PROTOCOL_FTE2) 114 | { 115 | reader.BaseStream.Position += 4; // Skip fteprotocolextensions2 (?) 116 | protocol = reader.ReadInt32(); 117 | } 118 | 119 | reader.SkipString(1024); // Skip mod folder... 120 | } 121 | 122 | if(protocol != PROTOCOL_NETQUAKE && protocol != PROTOCOL_FITZQUAKE && protocol != PROTOCOL_RMQ) return null; 123 | if(protocol == PROTOCOL_RMQ) reader.BaseStream.Position += 4; // Skip RMQ protocolflags (int32) 124 | 125 | int maxclients = reader.ReadByte(); 126 | if(maxclients < 1 || maxclients > 16) return null; 127 | 128 | int gametype = reader.ReadByte(); 129 | if(gametype != GAME_COOP && gametype != GAME_DEATHMATCH) return null; 130 | 131 | maptitle = reader.ReadMapTitle(blocklength, QuakeFont.CharMap); // Map title can contain bogus chars... 132 | mapfilepath = reader.ReadString(blocklength); 133 | if(string.IsNullOrEmpty(mapfilepath)) return null; 134 | if(string.IsNullOrEmpty(maptitle)) maptitle = Path.GetFileName(mapfilepath); 135 | alldatafound = true; 136 | break; 137 | 138 | case SVC_PRINT: 139 | // The string reading stops at '\0' or after 0x7FF bytes. The internal buffer has only 0x800 bytes available. 140 | if(!reader.SkipString(2048)) return null; 141 | break; 142 | 143 | default: 144 | return null; 145 | } 146 | } 147 | } 148 | 149 | // Done 150 | return (alldatafound ? new DemoItem(demoname, mapfilepath, maptitle, restype) : null); 151 | } 152 | 153 | // https://www.quakewiki.net/archives/demospecs/qwd/qwd.html 154 | private static DemoItem GetQWDInfo(string demoname, BinaryReader reader, ResourceType restype) 155 | { 156 | // Block header: 157 | // float time; 158 | // char code; // QWDBlockType.SERVER for server block 159 | 160 | // Server block: 161 | // long blocksize; 162 | // unsigned long seq_rel_1; // (!= 0xFFFFFFFF) for Game block 163 | 164 | // Game block: 165 | // unsigned long seq_rel_2; 166 | // char messages[blocksize - 8]; 167 | 168 | string game = string.Empty; 169 | string maptitle = string.Empty; 170 | string mapfilepath = string.Empty; 171 | int protocol = 0; 172 | bool alldatafound = false; 173 | 174 | // Read blocks... 175 | while(reader.BaseStream.Position < reader.BaseStream.Length) 176 | { 177 | if(alldatafound) break; 178 | 179 | // Read block header... 180 | reader.BaseStream.Position += 4; // Skip time 181 | int code = reader.ReadByte(); 182 | if(code != BLOCK_SERVER) return null; 183 | 184 | // Read as Server block... 185 | int blocklength = reader.ReadInt32(); 186 | long blockend = reader.BaseStream.Position + blocklength; 187 | if(blockend >= reader.BaseStream.Length) return null; 188 | uint serverblocktype = reader.ReadUInt32(); // 13 // connectionless block (== 0xFFFFFFFF) or a game block (!= 0xFFFFFFFF). 189 | if(serverblocktype == uint.MaxValue) return null; 190 | 191 | // Read as Game block... 192 | reader.BaseStream.Position += 4; // Skip seq_rel_2 193 | 194 | while(reader.BaseStream.Position < blockend) 195 | { 196 | if(alldatafound) break; 197 | 198 | // Read messages... 199 | int message = reader.ReadByte(); 200 | switch(message) 201 | { 202 | // SVC_SERVERINFO 203 | // long serverversion; // the protocol version coming from the server. 204 | // long age; // the number of levels analysed since the existence of the server process. Starts with 1. 205 | // char* game; // the QuakeWorld game directory. It has usually the value "qw"; 206 | // byte client; // the client id. 207 | // char* mapname; // the name of the level. 208 | // 10 unrelated floats 209 | 210 | case SVC_SERVERINFO: 211 | protocol = reader.ReadInt32(); 212 | if(!ProtocolsQW.Contains(protocol)) return null; 213 | reader.BaseStream.Position += 4; // Skip age 214 | game = reader.ReadString('\0'); 215 | reader.BaseStream.Position += 1; // Skip client 216 | maptitle = reader.ReadMapTitle(blocklength, QuakeFont.CharMap); // Map title can contain bogus chars... 217 | if(protocol > 24) reader.BaseStream.Position += 40; // Skip 10 floats... 218 | break; 219 | 220 | case SVC_CDTRACK: 221 | reader.BaseStream.Position += 1; // Skip CD track number 222 | break; 223 | 224 | case SVC_STUFFTEXT: 225 | reader.SkipString(2048); 226 | break; 227 | 228 | case SVC_MODELLIST: // First model should be the map name 229 | if(protocol > 25) reader.BaseStream.Position += 1; // Skip first model index... 230 | for(int i = 0; i < 256; i++) 231 | { 232 | string mdlname = reader.ReadString('\0'); 233 | if(string.IsNullOrEmpty(mdlname)) break; 234 | if(mdlname.EndsWith(".bsp", StringComparison.OrdinalIgnoreCase)) 235 | { 236 | mapfilepath = mdlname; 237 | alldatafound = true; 238 | break; 239 | } 240 | } 241 | if(protocol > 25) reader.BaseStream.Position += 1; // Skip next model index... 242 | break; 243 | 244 | case SVC_SOUNDLIST: 245 | if(protocol > 25) reader.BaseStream.Position += 1; // Skip first sound index... 246 | for(int i = 0; i < 256; i++) 247 | { 248 | string sndname = reader.ReadString('\0'); 249 | if(string.IsNullOrEmpty(sndname)) break; 250 | } 251 | if(protocol > 25) reader.BaseStream.Position += 1; // Skip next sound index... 252 | break; 253 | 254 | default: 255 | return null; 256 | } 257 | } 258 | } 259 | 260 | // Done 261 | return (alldatafound ? new DemoItem(game, demoname, mapfilepath, maptitle, restype) : null); 262 | } 263 | 264 | //TODO: Hacked in, needs more testing or proper format spec... 265 | private static DemoItem GetMVDInfo(string demoname, BinaryReader reader, ResourceType restype) 266 | { 267 | string game = string.Empty; 268 | string maptitle = string.Empty; 269 | string mapfilepath = string.Empty; 270 | int protocol = 0; 271 | bool alldatafound = false; 272 | 273 | // Read blocks... 274 | while(reader.BaseStream.Position < reader.BaseStream.Length) 275 | { 276 | if(alldatafound) break; 277 | 278 | // Read block header... 279 | reader.BaseStream.Position += 2; // Skip ??? (0x00 0x01 or 0x00 0x06) 280 | int blocklength = reader.ReadInt32(); 281 | long blockend = reader.BaseStream.Position + blocklength; 282 | if(blockend >= reader.BaseStream.Length) return null; 283 | 284 | while(reader.BaseStream.Position < blockend) 285 | { 286 | if(alldatafound) break; 287 | 288 | // Read messages... 289 | int message = reader.ReadByte(); 290 | switch(message) 291 | { 292 | // SVC_SERVERINFO 293 | // long serverversion; // the protocol version coming from the server. 294 | // long age; // the number of levels analysed since the existence of the server process. Starts with 1. 295 | // char* game; // the QuakeWorld game directory. It has usually the value "qw"; 296 | // long client; // the client id. 297 | // char* mapname; // the name of the level. 298 | // 10 unrelated floats 299 | 300 | case SVC_SERVERINFO: 301 | protocol = reader.ReadInt32(); 302 | if(!ProtocolsQW.Contains(protocol)) return null; 303 | reader.BaseStream.Position += 4; // Skip age 304 | game = reader.ReadString('\0'); 305 | reader.BaseStream.Position += 4; // Skip ??? 306 | maptitle = reader.ReadMapTitle(blocklength, QuakeFont.CharMap); // Map title can contain bogus chars... 307 | if(protocol > 24) reader.BaseStream.Position += 40; // Skip 10 floats... 308 | break; 309 | 310 | case SVC_CDTRACK: 311 | reader.BaseStream.Position += 1; // Skip CD track number 312 | break; 313 | 314 | case SVC_STUFFTEXT: 315 | reader.SkipString(2048); 316 | break; 317 | 318 | case SVC_MODELLIST: // First model should be the map name 319 | if(protocol > 25) reader.BaseStream.Position += 1; // Skip first model index... 320 | for(int i = 0; i < 256; i++) 321 | { 322 | string mdlname = reader.ReadString('\0'); 323 | if(string.IsNullOrEmpty(mdlname)) break; 324 | if(mdlname.EndsWith(".bsp", StringComparison.OrdinalIgnoreCase)) 325 | { 326 | mapfilepath = mdlname; 327 | alldatafound = true; 328 | break; 329 | } 330 | } 331 | if(protocol > 25) reader.BaseStream.Position += 1; // Skip next model index... 332 | break; 333 | 334 | case SVC_SOUNDLIST: 335 | if(protocol > 25) reader.BaseStream.Position += 1; // Skip first sound index... 336 | for(int i = 0; i < 256; i++) 337 | { 338 | string sndname = reader.ReadString('\0'); 339 | if(string.IsNullOrEmpty(sndname)) break; 340 | } 341 | if(protocol > 25) reader.BaseStream.Position += 1; // Skip next sound index... 342 | break; 343 | 344 | default: 345 | return null; 346 | } 347 | } 348 | } 349 | 350 | // Done 351 | return (alldatafound ? new DemoItem(game, demoname, mapfilepath, maptitle, restype) : null); 352 | } 353 | 354 | #endregion 355 | } 356 | } 357 | -------------------------------------------------------------------------------- /SQL2/Games/GameHandler.cs: -------------------------------------------------------------------------------- 1 | #region ================= Namespaces 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Drawing; 6 | using System.IO; 7 | using System.Linq; 8 | using System.Reflection; 9 | using System.Windows; 10 | using System.Windows.Interop; 11 | using System.Windows.Media; 12 | using System.Windows.Media.Imaging; 13 | using mxd.SQL2.DataReaders; 14 | using mxd.SQL2.Items; 15 | using mxd.SQL2.Tools; 16 | 17 | #endregion 18 | 19 | namespace mxd.SQL2.Games 20 | { 21 | public abstract class GameHandler 22 | { 23 | #region ================= Variables 24 | 25 | protected string defaultmodpath; // id1 / baseq2 / data1 etc. 26 | protected string gamepath; // c:\games\Quake, c:\games\Quake2 etc. 27 | protected string modname; // xatrix / Arcane Dimensions / yoursupermod etc. 28 | protected string ignoredmapprefix; // map filenames starting with this will be ignored 29 | protected HashSet supporteddemoextensions; // .dem, .mvd, .qvd etc. 30 | protected Dictionary basegames; // Quake-specific game flags; 31 | protected List skills; // Easy, Medium, Hard, Nightmare! 32 | protected List classes; // Cleric, Paladin, Necromancer [default], Assassin, Demoness [Hexen 2 only] 33 | protected Dictionary fullscreenarg; // true: arg to run the game in fullscreen, false: arg to run the game windowed 34 | 35 | private HashSet mapnames; 36 | private HashSet defaultmapnames; // Map names from defaultmodpath. Needed to properly check demos MapPath data... 37 | private Dictionary defaultmaplist; // Maps from defaultmodpath. Needed to populate "maps" dropdown for mods without maps... 38 | private List skillnames; 39 | private List classnames; // [Hexen 2 only] 40 | 41 | private static string supportedgames; // "Quake / Quake II / Hexen 2 / Half-Life" 42 | private static GameHandler current; 43 | 44 | // Command line 45 | protected Dictionary launchparams; 46 | 47 | #endregion 48 | 49 | #region ================= Properties 50 | 51 | public abstract string GameTitle { get; } // Quake / Quake II / Hexen 2 / Half-Life etc. 52 | public Dictionary LaunchParameters => launchparams; 53 | public string DefaultModPath => defaultmodpath; // c:\games\Quake\ID1, c:\games\Quake2\baseq2 etc. 54 | public string GamePath => gamepath; 55 | public string IgnoredMapPrefix => ignoredmapprefix; 56 | public HashSet SupportedDemoExtensions => supporteddemoextensions; 57 | public ICollection BaseGames => basegames.Values; 58 | public List Skills => skills; 59 | public List Classes => classes; 60 | public Dictionary FullScreenArg => fullscreenarg; 61 | 62 | #endregion 63 | 64 | #region ================= Static properties 65 | 66 | public static string SupportedGames => supportedgames; 67 | public static GameHandler Current => current; 68 | 69 | #endregion 70 | 71 | #region ================= Delegates 72 | 73 | // Map title retrieval 74 | public delegate MapItem GetMapInfoDelegate(string mapname, BinaryReader reader, ResourceType restype); 75 | 76 | // Maps gathering 77 | protected delegate void GetFolderMapsDelegate(string modpath, Dictionary mapslist, GetMapInfoDelegate getmapinfo); 78 | protected delegate void GetPakMapsDelegate(string modpath, Dictionary mapslist, GetMapInfoDelegate getmapinfo); 79 | protected delegate void GetPK3MapsDelegate(string modpath, Dictionary mapslist, GetMapInfoDelegate getmapinfo); 80 | 81 | // Maps checking 82 | protected delegate bool FolderContainsMapsDelegate(string modpath); 83 | protected delegate bool PakContainsMapsDelegate(string modpath); 84 | protected delegate bool PK3ContainsMapsDelegate(string modpath); 85 | 86 | // File checking 87 | protected delegate bool PakContainsFileDelegate(string modpath, string filename); 88 | protected delegate bool PK3ContainsFileDelegate(string modpath, string filename); 89 | 90 | // Demo info retrieval 91 | protected delegate DemoItem GetDemoInfoDelegate(string demoname, BinaryReader reader, ResourceType restype); 92 | 93 | // Demos gathering 94 | protected delegate List GetFolderDemosDelegate(string modpath, string demosfolder); 95 | protected delegate List GetPakDemosDelegate(string modpath, string demosfolder); 96 | protected delegate List GetPK3DemosDelegate(string modpath, string demosfolder); 97 | 98 | // Map title retrieval instance 99 | protected GetMapInfoDelegate getmapinfo; 100 | 101 | // Maps gathering instances 102 | protected GetFolderMapsDelegate getfoldermaps; 103 | protected GetPakMapsDelegate getpakmaps; 104 | protected GetPK3MapsDelegate getpk3maps; 105 | 106 | // Maps checking instances 107 | protected FolderContainsMapsDelegate foldercontainsmaps; 108 | protected PakContainsMapsDelegate pakscontainmaps; 109 | protected PK3ContainsMapsDelegate pk3scontainmaps; 110 | 111 | // File checking instances 112 | protected PakContainsFileDelegate pakscontainfile; 113 | protected PK3ContainsFileDelegate pk3scontainfile; 114 | 115 | // Demo info retrieval instance 116 | protected GetDemoInfoDelegate getdemoinfo; 117 | 118 | // Demo gathering instances 119 | protected GetFolderDemosDelegate getfolderdemos; 120 | protected GetPakDemosDelegate getpakdemos; 121 | protected GetPK3DemosDelegate getpk3demos; 122 | 123 | #endregion 124 | 125 | #region ================= Constructor / Setup 126 | 127 | protected GameHandler() 128 | { 129 | launchparams = new Dictionary(); 130 | basegames = new Dictionary(StringComparer.OrdinalIgnoreCase); 131 | mapnames = new HashSet(StringComparer.OrdinalIgnoreCase); 132 | defaultmapnames = new HashSet(StringComparer.OrdinalIgnoreCase); 133 | skills = new List(); 134 | classes = new List(); 135 | skillnames = new List(); 136 | classnames = new List(); 137 | supporteddemoextensions = new HashSet(StringComparer.OrdinalIgnoreCase); 138 | fullscreenarg = new Dictionary(); 139 | } 140 | 141 | protected abstract bool CanHandle(string gamepath); 142 | 143 | protected virtual void Setup(string gamepath) // c:\games\Quake 144 | { 145 | this.gamepath = gamepath; 146 | 147 | // Add random skill and class 148 | if(skills.Count > 1) skills.Insert(0, SkillItem.Random); 149 | if(skills.Count > 0) skills.Insert(0, SkillItem.Default); 150 | 151 | if(classes.Count > 1) classes.Insert(0, ClassItem.Random); 152 | if(classes.Count > 0) classes.Insert(0, ClassItem.Default); 153 | 154 | // Store base game maps... 155 | defaultmaplist = new Dictionary(StringComparer.OrdinalIgnoreCase); 156 | var path = Path.Combine(gamepath, defaultmodpath); 157 | 158 | if (Directory.Exists(path)) 159 | { 160 | getfoldermaps?.Invoke(path, defaultmaplist, getmapinfo); 161 | getpakmaps?.Invoke(path, defaultmaplist, getmapinfo); 162 | getpk3maps?.Invoke(path, defaultmaplist, getmapinfo); 163 | } 164 | } 165 | 166 | #endregion 167 | 168 | #region ================= Data gathering 169 | 170 | // Quakespasm can play demos made for both ID1 maps and currently selected official MP from any folder... 171 | public void UpdateDefaultMapNames(string modpath) 172 | { 173 | // Store maps from default mod... 174 | var maplist = new Dictionary(StringComparer.OrdinalIgnoreCase); 175 | 176 | // Get maps from all supported sources 177 | if(!string.IsNullOrEmpty(modpath) && Directory.Exists(modpath)) 178 | { 179 | getfoldermaps?.Invoke(modpath, maplist, null); 180 | getpakmaps?.Invoke(modpath, maplist, null); 181 | getpk3maps?.Invoke(modpath, maplist, null); 182 | } 183 | 184 | // Store map names... 185 | defaultmapnames = new HashSet(maplist.Keys, StringComparer.OrdinalIgnoreCase); 186 | 187 | // Add map names from base game... 188 | defaultmapnames.UnionWith(defaultmaplist.Keys); 189 | } 190 | 191 | // Because some engines have hardcoded resolution lists... 192 | public virtual List GetVideoModes() 193 | { 194 | return DisplayTools.GetVideoModes(); 195 | } 196 | 197 | public abstract List GetMods(); 198 | 199 | public virtual List GetDemos(string modpath) // c:\Quake\MyMod 200 | { 201 | return GetDemos(modpath, string.Empty); 202 | } 203 | 204 | protected virtual List GetDemos(string modpath, string modfolder) 205 | { 206 | var demos = new List(); 207 | if(!Directory.Exists(modpath)) return demos; 208 | 209 | // Get demos from all supported sources 210 | var nameshash = new HashSet(); 211 | if(getfolderdemos != null) AddDemos(demos, getfolderdemos(modpath, modfolder), nameshash); 212 | if(getpakdemos != null) AddDemos(demos, getpakdemos(modpath, modfolder), nameshash); 213 | if(getpk3demos != null) AddDemos(demos, getpk3demos(modpath, modfolder), nameshash); 214 | 215 | // Sort and return the List 216 | demos.Sort((s1, s2) => 217 | { 218 | if(s1.ResourceType != s2.ResourceType) return (int)s1.ResourceType > (int)s2.ResourceType ? 1 : -1; // Sort by ResourceType 219 | return s1.Value.CompareNatural(s2.Value); 220 | }); 221 | return demos; 222 | } 223 | 224 | protected virtual void AddDemos(List demos, List newdemos, HashSet nameshash) 225 | { 226 | foreach(DemoItem di in newdemos) 227 | { 228 | string hash = Path.GetFileName(di.MapFilePath) + di.Title; 229 | if(!nameshash.Contains(hash)) 230 | { 231 | nameshash.Add(hash); 232 | demos.Add(di); 233 | } 234 | } 235 | } 236 | 237 | public virtual List GetMaps(string modpath) // c:\Quake\MyMod 238 | { 239 | modname = modpath.Substring(gamepath.Length + 1); 240 | if(!Directory.Exists(modpath)) return new List(); 241 | var maplist = new Dictionary(StringComparer.OrdinalIgnoreCase); 242 | 243 | // Get maps from all supported sources 244 | getfoldermaps?.Invoke(modpath, maplist, getmapinfo); 245 | getpakmaps?.Invoke(modpath, maplist, getmapinfo); 246 | getpk3maps?.Invoke(modpath, maplist, getmapinfo); 247 | 248 | // Store map names... 249 | mapnames = new HashSet(maplist.Keys, StringComparer.OrdinalIgnoreCase); 250 | mapnames.UnionWith(defaultmapnames); // Add default maps... 251 | 252 | // When mod has no maps, use maps from base game... 253 | if (maplist.Count == 0) 254 | maplist = defaultmaplist; 255 | 256 | // Sort and return the List 257 | var mapitems = new List(maplist.Values.Count); 258 | foreach(MapItem mi in maplist.Values) mapitems.Add(mi); 259 | mapitems.Sort((s1, s2) => 260 | { 261 | if(s1.ResourceType != s2.ResourceType) return (int)s1.ResourceType > (int)s2.ResourceType ? 1 : -1; // Sort by ResourceType 262 | return s1.Value.CompareNatural(s2.Value); 263 | }); 264 | return mapitems; 265 | } 266 | 267 | public virtual List GetEngines() 268 | { 269 | string[] enginenames = Directory.GetFiles(gamepath, "*.exe"); 270 | var result = new List(); 271 | foreach(string engine in enginenames) 272 | { 273 | if(!IsEngine(Path.GetFileName(engine))) continue; 274 | 275 | ImageSource img = null; 276 | using(var i = Icon.ExtractAssociatedIcon(engine)) 277 | { 278 | if(i != null) 279 | img = Imaging.CreateBitmapSourceFromHIcon(i.Handle, new Int32Rect(0, 0, i.Width, i.Height), BitmapSizeOptions.FromEmptyOptions()); 280 | } 281 | 282 | result.Add(new EngineItem(img, engine)); 283 | } 284 | 285 | return result; 286 | } 287 | 288 | public virtual string GetRandomItem(ItemType type) 289 | { 290 | switch(type) 291 | { 292 | case ItemType.CLASS: return (classnames.Count > 0 ? classnames[App.Random.Next(0, classnames.Count)] : "0"); 293 | case ItemType.SKILL: return (skillnames.Count > 0 ? skillnames[App.Random.Next(0, skillnames.Count)] : "0"); 294 | case ItemType.MAP: return mapnames.ElementAt(App.Random.Next(0, mapnames.Count)); 295 | default: throw new Exception("GetRandomItem: unsupported ItemType!"); 296 | } 297 | } 298 | 299 | // Here mainly because of Hexen 2... 300 | public virtual string CheckMapTitle(string title) 301 | { 302 | return title.Trim(); 303 | } 304 | 305 | #endregion 306 | 307 | #region ================= Utility methods 308 | 309 | // "maps/mymap4.bsp", 310 | public virtual bool EntryIsMap(string path, Dictionary mapslist) 311 | { 312 | path = path.ToLowerInvariant(); 313 | if(Path.GetDirectoryName(path).EndsWith("maps") && Path.GetExtension(path) == ".bsp") 314 | { 315 | string mapname = Path.GetFileNameWithoutExtension(path); 316 | if((string.IsNullOrEmpty(ignoredmapprefix) || !mapname.StartsWith(ignoredmapprefix)) && !mapslist.ContainsKey(mapname)) 317 | return true; 318 | } 319 | 320 | return false; 321 | } 322 | 323 | public virtual void AddDemoItem(string relativedemopath, List demos, BinaryReader reader, ResourceType restype) 324 | { 325 | relativedemopath = relativedemopath.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar); 326 | DemoItem di = getdemoinfo(relativedemopath, reader, restype); 327 | if(di != null) 328 | { 329 | if(di.IsInvalid) 330 | demos.Add(di); 331 | else if(!mapnames.Contains(Path.GetFileNameWithoutExtension(di.MapFilePath))) // Check if we have a matching map... 332 | demos.Add(new DemoItem(relativedemopath, "Missing map file: '" + di.MapFilePath + "'", di.ResourceType)); // Add anyway, but with a warning... 333 | else if(!string.IsNullOrEmpty(di.ModName) && string.Compare(modname, di.ModName, StringComparison.OrdinalIgnoreCase) != 0) 334 | demos.Add(new DemoItem(relativedemopath, "Incorrect location: expected to be in '" + di.ModName + "' folder", di.ResourceType)); // Add anyway, but with a warning... 335 | else 336 | demos.Add(di); 337 | } 338 | else 339 | { 340 | // Add anyway, I guess... 341 | demos.Add(new DemoItem(relativedemopath, "Unknown demo format", restype)); 342 | } 343 | } 344 | 345 | protected virtual bool IsEngine(string filename) 346 | { 347 | return (filename.EndsWith(".exe", StringComparison.OrdinalIgnoreCase) 348 | && Path.GetFileNameWithoutExtension(filename) != App.AppName 349 | && !filename.StartsWith("unins", StringComparison.OrdinalIgnoreCase) 350 | && !filename.StartsWith("unwise", StringComparison.OrdinalIgnoreCase)); 351 | } 352 | 353 | #endregion 354 | 355 | #region ================= Instancing 356 | 357 | public static bool Create(string gamepath) // c:\games\Quake 358 | { 359 | // Try to get appropriate game handler 360 | List gametitles = new List(); 361 | foreach(Type type in Assembly.GetAssembly(typeof(GameHandler)).GetTypes() 362 | .Where(t => t.IsClass && !t.IsAbstract && t.IsSubclassOf(typeof(GameHandler)))) 363 | { 364 | var gh = (GameHandler)Activator.CreateInstance(type); 365 | gametitles.Add(gh.GameTitle); 366 | if(current == null && gh.CanHandle(gamepath)) 367 | { 368 | current = gh; 369 | gh.Setup(gamepath); // GameItems created in Setup() reference GameHandler.Current... 370 | } 371 | } 372 | 373 | // Store all titles 374 | supportedgames = string.Join(" / ", gametitles.ToArray()); 375 | 376 | return current != null; 377 | } 378 | 379 | #endregion 380 | } 381 | } 382 | -------------------------------------------------------------------------------- /SQL2/MainWindow.xaml.cs: -------------------------------------------------------------------------------- 1 | #region ================= Namespaces 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Diagnostics; 6 | using System.IO; 7 | using System.Media; 8 | using System.Windows; 9 | using System.Windows.Controls; 10 | using IWshRuntimeLibrary; 11 | using mxd.SQL2.Data; 12 | using mxd.SQL2.Games; 13 | using mxd.SQL2.Items; 14 | using mxd.SQL2.Tools; 15 | using File = System.IO.File; 16 | 17 | #endregion 18 | 19 | namespace mxd.SQL2 20 | { 21 | 22 | public partial class MainWindow : Window 23 | { 24 | #region ================= Enums 25 | 26 | private enum FocusState 27 | { 28 | DEFAULT, 29 | FOCUSED, 30 | UNFOCUSED 31 | } 32 | 33 | #endregion 34 | 35 | #region ================= Variables 36 | 37 | private int windowx; 38 | private int windowy; 39 | private bool enginelaunched; 40 | private bool blockupdate; 41 | private List allmods; 42 | private FocusState focusstate; 43 | 44 | #endregion 45 | 46 | #region ================= Constructor / Setup 47 | 48 | public MainWindow() 49 | { 50 | InitializeComponent(); 51 | allmods = new List(); 52 | createshortcut.ContextMenu.PlacementTarget = createshortcut; 53 | } 54 | 55 | private void Setup() 56 | { 57 | blockupdate = true; 58 | 59 | // Set title and version 60 | this.Title = "Simple " + GameHandler.Current.GameTitle + " Launcher"; 61 | labelversion.Content = "v" + App.Version; 62 | 63 | // Load configuration 64 | Configuration.Load(App.IniPath); 65 | 66 | // Setup Engines 67 | UpdateEngines(); 68 | 69 | // Fill video modes list 70 | var videomodes = GameHandler.Current.GetVideoModes(); 71 | if(videomodes.Count > 0) 72 | { 73 | resolutions.Items.Add(ResolutionItem.Default); 74 | foreach(ResolutionItem mode in videomodes) 75 | { 76 | resolutions.Items.Add(mode); 77 | if(mode.ToString() == Configuration.WindowSize) 78 | resolutions.SelectedIndex = resolutions.Items.Count - 1; 79 | } 80 | 81 | if(resolutions.SelectedIndex == -1) resolutions.SelectedIndex = 0; 82 | } 83 | else 84 | { 85 | // Um... go on regardless, I guess... 86 | rowresolution.Height = new GridLength(0); 87 | } 88 | 89 | // Setup base game 90 | if(GameHandler.Current.BaseGames.Count > 0) 91 | { 92 | foreach(var bgi in GameHandler.Current.BaseGames) 93 | games.Items.Add(bgi); 94 | 95 | if(Configuration.Game > -1 && Configuration.Game < GameHandler.Current.BaseGames.Count) 96 | games.SelectedIndex = Configuration.Game; 97 | else 98 | games.SelectedIndex = 0; 99 | } 100 | else 101 | { 102 | // No base game support 103 | rowgame.Height = new GridLength(0); 104 | } 105 | 106 | // Setup mod folders 107 | UpdateModsList(); 108 | 109 | // Update default map names 110 | UpdateDefaultMapNames(); 111 | 112 | // Setup maps 113 | UpdateMapsList(); 114 | 115 | // Setup demos list 116 | UpdateDemosList(); 117 | 118 | // Setup skills 119 | foreach(var skill in GameHandler.Current.Skills) 120 | { 121 | skills.Items.Add(skill); 122 | if(skill.Value == Configuration.Skill) 123 | skills.SelectedItem = skill; 124 | } 125 | 126 | // Safety measures... 127 | if(skills.SelectedIndex == -1) 128 | { 129 | if(skills.Items.Count > 0) skills.SelectedIndex = 0; 130 | else rowskill.Height = new GridLength(0); 131 | } 132 | 133 | // Setup classes 134 | foreach(var pclass in GameHandler.Current.Classes) 135 | { 136 | classes.Items.Add(pclass); 137 | if(pclass.Value == Configuration.Class) 138 | classes.SelectedItem = pclass; 139 | } 140 | 141 | // Safety measures... 142 | if(classes.SelectedIndex == -1) 143 | { 144 | if(classes.Items.Count > 0) classes.SelectedIndex = 0; 145 | else rowclass.Height = new GridLength(0); 146 | } 147 | 148 | // Update UI and preview 149 | UpdateInterface(); 150 | UpdateCommandLinePreview(); 151 | 152 | blockupdate = false; 153 | } 154 | 155 | #endregion 156 | 157 | #region ================= Utility 158 | 159 | private void UpdateAll() 160 | { 161 | blockupdate = true; 162 | 163 | UpdateModsList(); 164 | UpdateDefaultMapNames(); 165 | UpdateMapsList(); 166 | UpdateDemosList(); 167 | UpdateInterface(); 168 | UpdateCommandLinePreview(); 169 | 170 | blockupdate = false; 171 | } 172 | 173 | private void UpdateModsList() 174 | { 175 | mods.Items.Clear(); 176 | mods.Items.Add(ModItem.Default); 177 | 178 | // Select stored item? 179 | allmods = GameHandler.Current.GetMods(); 180 | foreach(ModItem mi in allmods) 181 | { 182 | if(mi.IsBuiltIn) continue; // Skip mods enabled by cmdline param 183 | mods.Items.Add(mi); 184 | if(mi.Value == Configuration.Mod) 185 | mods.SelectedIndex = mods.Items.Count - 1; 186 | } 187 | 188 | // Select the Default item... 189 | if(mods.SelectedIndex == -1) mods.SelectedIndex = 0; 190 | } 191 | 192 | // Quakespasm can play demos made for both ID1 maps and currently selected official MP from any folder... 193 | private void UpdateDefaultMapNames() 194 | { 195 | var game = (games.IsVisible && games.IsEnabled) ? (GameItem)games.SelectedItem : null; 196 | GameHandler.Current.UpdateDefaultMapNames((game != null && !game.IsDefault) ? game.ModFolder : string.Empty); 197 | } 198 | 199 | private void UpdateMapsList() 200 | { 201 | maps.Items.Clear(); 202 | maps.Items.Add(MapItem.Default); 203 | 204 | ModItem curmod = GetCurrentMod((ModItem)mods.SelectedItem); 205 | List mapslist = GameHandler.Current.GetMaps(curmod.ModPath); 206 | foreach(MapItem mi in mapslist) maps.Items.Add(mi); 207 | 208 | //Add "[Random]" item 209 | if(maps.Items.Count > 2) maps.Items.Insert(1, MapItem.Random); 210 | 211 | // Select map when both mod name and map name match 212 | if(!string.IsNullOrEmpty(Configuration.Map)) 213 | { 214 | foreach(MapItem mi in maps.Items) 215 | { 216 | // Select stored map? 217 | if(mi.Value == Configuration.Map) 218 | { 219 | maps.SelectedItem = mi; 220 | break; 221 | } 222 | } 223 | } 224 | 225 | //Select start map? 226 | if(maps.SelectedIndex == -1) 227 | { 228 | foreach(MapItem mi in mapslist) 229 | { 230 | if(mi.Value.Contains("start")) 231 | { 232 | maps.SelectedItem = mi; 233 | break; 234 | } 235 | } 236 | } 237 | 238 | //Select the first map if no "start"/stored map was found 239 | if(maps.SelectedIndex == -1) 240 | { 241 | foreach(MapItem mi in mapslist) 242 | { 243 | if(!mi.IsDefault && !mi.IsRandom) 244 | { 245 | maps.SelectedItem = mi; 246 | break; 247 | } 248 | } 249 | } 250 | 251 | // No maps. Select the Default item... 252 | if(maps.SelectedIndex == -1) maps.SelectedIndex = 0; 253 | } 254 | 255 | // c:\quake\mymod 256 | private void UpdateDemosList() 257 | { 258 | demos.Items.Clear(); 259 | ModItem curmod = GetCurrentMod((ModItem)mods.SelectedItem); 260 | 261 | #if DEBUG 262 | if(!Directory.Exists(curmod.ModPath)) 263 | throw new InvalidOperationException("Expected existing absolute path!"); 264 | #endif 265 | 266 | var demoitems = GameHandler.Current.GetDemos(curmod.ModPath); 267 | if(demoitems.Count == 0) return; 268 | 269 | demos.Items.Add(DemoItem.None); 270 | 271 | foreach(var di in demoitems) 272 | { 273 | demos.Items.Add(di); 274 | if(di.Value == Configuration.Demo) demos.SelectedItem = di; 275 | } 276 | 277 | // Select the first item... 278 | if(demos.SelectedIndex == -1) demos.SelectedIndex = 0; 279 | } 280 | 281 | private void UpdateEngines() 282 | { 283 | engines.Items.Clear(); 284 | 285 | // Store current engine... 286 | string currentengine = (engines.SelectedItem != null ? ((EngineItem)engines.SelectedItem).Title : string.Empty); 287 | var engineitems = GameHandler.Current.GetEngines(); 288 | if(engineitems.Count == 0) 289 | { 290 | MessageBox.Show(this, "No executable files detected in the game directory (" + GameHandler.Current.GamePath 291 | + ")\n\nMake sure you are running this program from your " + GameHandler.SupportedGames + " directory!", App.ErrorMessageTitle); 292 | Application.Current.Shutdown(); 293 | return; 294 | } 295 | 296 | // Refill the list... 297 | foreach(EngineItem ei in engineitems) engines.Items.Add(ei); 298 | 299 | // Select last used engine 300 | if(!string.IsNullOrEmpty(currentengine)) 301 | { 302 | foreach(EngineItem ei in engines.Items) 303 | { 304 | if(ei.Title != currentengine) continue; 305 | engines.SelectedItem = ei; 306 | break; 307 | } 308 | } 309 | 310 | // Select last stored engine 311 | if(engines.SelectedIndex == -1) 312 | { 313 | foreach(EngineItem ei in engines.Items) 314 | { 315 | if(ei.Title != Configuration.Engine) continue; 316 | engines.SelectedItem = ei; 317 | break; 318 | } 319 | } 320 | 321 | // Select... something 322 | if(engines.SelectedIndex == -1) engines.SelectedIndex = 0; 323 | 324 | // Set engine icon 325 | engineicon.Source = ((EngineItem)engines.SelectedItem).Icon; 326 | } 327 | 328 | private void UpdateCommandLinePreview(bool clearcustomargs = false) 329 | { 330 | var lp = GetLaunchParams(); 331 | 332 | // Update the shortcut button... 333 | createshortcut.IsEnabled = (lp[ItemType.CLASS] == null || !lp[ItemType.CLASS].IsRandom) 334 | && (lp[ItemType.MAP] == null || !lp[ItemType.MAP].IsRandom) 335 | && (lp[ItemType.SKILL] == null || !lp[ItemType.SKILL].IsRandom); 336 | 337 | // Update command line 338 | cmdline.SetArguments(lp, clearcustomargs); 339 | } 340 | 341 | // Enable/disable controls based on currently selected items 342 | private void UpdateInterface() 343 | { 344 | // Disable map, skill and class dropdowns when a demo is selected... 345 | var demo = (DemoItem)demos.SelectedItem; 346 | bool enable = (demo == null || demo.IsDefault); 347 | foreach(var c in new Control[]{ maps, labelmaps, skills, labelskills, classes, labelclasses }) 348 | c.IsEnabled = enable; 349 | 350 | // Disable demo controls if no demos were found 351 | bool havedemos = (demos.Items.Count > 0); 352 | demos.IsEnabled = havedemos; 353 | labeldemos.IsEnabled = havedemos; 354 | } 355 | 356 | private ModItem GetCurrentMod(ModItem mi) 357 | { 358 | var gi = (GameItem)games.SelectedItem; 359 | 360 | // When Default ModItem and non-default GameItem is selected, return ModItem to GameItem location 361 | if(!mi.IsDefault || !games.IsEnabled || gi == null || gi.IsDefault) 362 | return mi; 363 | 364 | foreach(ModItem mod in allmods) 365 | if(string.Equals(mod.ModPath, gi.ModFolder, StringComparison.InvariantCultureIgnoreCase)) return mod; 366 | 367 | // GameItem without corresponding game data selected 368 | return mi; 369 | } 370 | 371 | private Dictionary GetLaunchParams() 372 | { 373 | var result = new Dictionary(8); 374 | var demo = ((demos.IsVisible && demos.IsEnabled) ? (DemoItem)demos.SelectedItem : null); 375 | 376 | // Creation order should match args display order 377 | result[ItemType.ENGINE] = (EngineItem)engines.SelectedItem; 378 | result[ItemType.RESOLUTION] = (ResolutionItem)resolutions.SelectedItem; 379 | result[ItemType.GAME] = ((games.IsVisible && games.IsEnabled) ? (GameItem)games.SelectedItem : null); 380 | result[ItemType.MOD] = ((mods.IsVisible && mods.IsEnabled) ? (ModItem)mods.SelectedItem : null); 381 | result[ItemType.SKILL] = ((skills.IsVisible && skills.IsEnabled) ? (SkillItem)skills.SelectedItem : null); 382 | result[ItemType.CLASS] = ((classes.IsVisible && classes.IsEnabled) ? (ClassItem)classes.SelectedItem : null); 383 | result[ItemType.MAP] = ((maps.IsVisible && maps.IsEnabled && (demo == null || demo.IsDefault)) ? (MapItem)maps.SelectedItem : null); 384 | result[ItemType.DEMO] = demo; 385 | 386 | return result; 387 | } 388 | 389 | // Doesn't handle random items! 390 | private void CreateShortcut(string shortcutpath) 391 | { 392 | var lp = GetLaunchParams(); 393 | 394 | // Determine shortcut name 395 | string shortcutname; 396 | if(lp[ItemType.DEMO] != null && !lp[ItemType.DEMO].IsDefault) 397 | { 398 | var demo = (DemoItem)lp[ItemType.DEMO]; 399 | string map = RemoveInvalidFilenameChars(demo.MapTitle); 400 | if(string.IsNullOrEmpty(map)) map = Path.GetFileName(demo.MapFilePath); 401 | shortcutname = "Watch '" + map.UppercaseFirst() + "' Demo"; 402 | } 403 | else 404 | { 405 | // Determine game/map title 406 | string map; 407 | if(lp[ItemType.MAP] != null && !lp[ItemType.MAP].IsDefault) 408 | { 409 | var mi = (MapItem)lp[ItemType.MAP]; 410 | map = RemoveInvalidFilenameChars(mi.MapTitle); 411 | if(string.IsNullOrEmpty(map)) map = mi.Value; 412 | } 413 | else if(lp[ItemType.MOD] != null && !lp[ItemType.MOD].IsDefault) 414 | { 415 | map = Path.GetFileName(lp[ItemType.MOD].Title); 416 | } 417 | else if(lp[ItemType.GAME] != null && !lp[ItemType.GAME].IsDefault) 418 | { 419 | map = RemoveInvalidFilenameChars(lp[ItemType.GAME].Title); 420 | } 421 | else 422 | { 423 | map = GameHandler.Current.GameTitle; 424 | } 425 | 426 | shortcutname = "Play '" + map.UppercaseFirst() + "'"; 427 | 428 | // Add class/skill if available/non-default 429 | var extrainfo = new List(); 430 | if(lp[ItemType.CLASS] != null && !lp[ItemType.CLASS].IsDefault) 431 | extrainfo.Add(lp[ItemType.CLASS].Title); 432 | 433 | if(lp[ItemType.SKILL] != null && !lp[ItemType.SKILL].IsDefault) 434 | extrainfo.Add(lp[ItemType.SKILL].Title); 435 | 436 | if(extrainfo.Count > 0) 437 | shortcutname += " (" + string.Join(", ", extrainfo) + ")"; 438 | } 439 | 440 | // Assemble shortcut path 441 | shortcutpath = Path.Combine(shortcutpath, shortcutname + ".lnk"); 442 | 443 | // Check if we already have a shortcut with that name... 444 | if(File.Exists(shortcutpath) && MessageBox.Show("Shortcut '" + shortcutname + "' already exists." + Environment.NewLine 445 | + "Do you want to replace it?", "Serious Question", MessageBoxButton.OKCancel) == MessageBoxResult.Cancel) 446 | return; 447 | 448 | // Create shortcut 449 | string enginepath = ((EngineItem)lp[ItemType.ENGINE]).FileName; 450 | var shell = new WshShell(); 451 | var shortcut = (IWshShortcut)shell.CreateShortcut(shortcutpath); 452 | shortcut.TargetPath = enginepath; 453 | shortcut.WorkingDirectory = Path.GetDirectoryName(enginepath); 454 | shortcut.Arguments = cmdline.GetCommandLine(); 455 | shortcut.Save(); 456 | } 457 | 458 | private static string RemoveInvalidFilenameChars(string filename) 459 | { 460 | foreach(char c in Path.GetInvalidFileNameChars()) 461 | { 462 | if(filename.Contains(c.ToString())) 463 | filename = filename.Replace(c.ToString(), (c == ':' ? " - " : "")); 464 | } 465 | 466 | return filename; 467 | } 468 | 469 | #endregion 470 | 471 | #region ================= Events 472 | 473 | private void launch_Click(object sender, EventArgs e) 474 | { 475 | var lp = GetLaunchParams(); 476 | var engine = (EngineItem)lp[ItemType.ENGINE]; 477 | var mod = (ModItem)lp[ItemType.MOD]; 478 | string argsstr = cmdline.GetCommandLine(); 479 | 480 | // Some sanity checks 481 | bool reloadengines = false; 482 | bool reloadmods = false; 483 | List reasons = new List(); 484 | 485 | if(!File.Exists(engine.FileName)) 486 | { 487 | reasons.Add("- Selected game engine does not exist!"); 488 | reloadengines = true; 489 | } 490 | 491 | if(mod != null && !Directory.Exists(mod.ModPath)) 492 | { 493 | reasons.Add("- Selected mod folder not exist!"); 494 | reloadmods = true; 495 | } 496 | 497 | if(reasons.Count > 0) 498 | { 499 | MessageBox.Show(this, "Unable to launch:\n" + string.Join("\n", reasons.ToArray()) + "\n\nAffected data will be updated.", App.ErrorMessageTitle); 500 | if(reloadengines) UpdateEngines(); 501 | if(reloadmods) UpdateAll(); 502 | return; 503 | } 504 | 505 | // Don't mess with window position when launching in windowed mode 506 | var res = (ResolutionItem)lp[ItemType.RESOLUTION]; 507 | windowx = (res.IsDefault ? (int)this.Left : int.MaxValue); 508 | windowy = (res.IsDefault ? (int)this.Top : int.MaxValue); 509 | 510 | // Proceed with launch 511 | enginelaunched = true; 512 | 513 | #if DEBUG 514 | string result = engine.FileName + " " + argsstr; 515 | if(MessageBox.Show(this, "Launch parameters:\n" + result + "\n\nProceed?", "Launch Preview", MessageBoxButton.YesNo) == MessageBoxResult.No) return; 516 | #endif 517 | 518 | // Setup process info 519 | ProcessStartInfo processinfo = new ProcessStartInfo 520 | { 521 | Arguments = argsstr, 522 | FileName = engine.FileName, 523 | CreateNoWindow = false, 524 | ErrorDialog = false, 525 | UseShellExecute = true, 526 | WindowStyle = ProcessWindowStyle.Normal, 527 | }; 528 | 529 | processinfo.WorkingDirectory = Path.GetDirectoryName(processinfo.FileName); 530 | 531 | try 532 | { 533 | // Start the program 534 | var process = Process.Start(processinfo); 535 | if(process != null) 536 | { 537 | process.EnableRaisingEvents = true; 538 | process.Exited += ProcessOnExited; 539 | } 540 | } 541 | catch(Exception ex) 542 | { 543 | // Unable to start the program 544 | MessageBox.Show(this, "Unable to start game engine, " + ex.GetType().Name + ": " + ex.Message, App.ErrorMessageTitle); 545 | } 546 | } 547 | 548 | private void engines_SelectionChanged(object sender, SelectionChangedEventArgs e) 549 | { 550 | if(blockupdate || engines.Items.Count == 0) return; 551 | var ei = (EngineItem)engines.SelectedItem; 552 | Configuration.Engine = ei.Title; 553 | engineicon.Source = ei.Icon; 554 | 555 | UpdateCommandLinePreview(); 556 | } 557 | 558 | private void resolutions_SelectionChanged(object sender, SelectionChangedEventArgs e) 559 | { 560 | if(blockupdate || resolutions.Items.Count == 0) return; 561 | Configuration.WindowSize = (resolutions.SelectedIndex > 0 ? ((ResolutionItem)resolutions.SelectedItem).ToString() : string.Empty); 562 | 563 | UpdateCommandLinePreview(); 564 | } 565 | 566 | private void games_SelectionChanged(object sender, SelectionChangedEventArgs e) 567 | { 568 | if(blockupdate || games.Items.Count == 0) return; 569 | Configuration.Game = games.SelectedIndex; 570 | var mod = GetCurrentMod((ModItem)mods.SelectedItem); 571 | Configuration.Mod = (mod != null && !mod.IsDefault ? mod.Value : string.Empty); 572 | 573 | UpdateDefaultMapNames(); 574 | UpdateMapsList(); 575 | UpdateDemosList(); 576 | UpdateCommandLinePreview(); 577 | } 578 | 579 | private void mods_SelectionChanged(object sender, SelectionChangedEventArgs e) 580 | { 581 | if(blockupdate || mods.Items.Count == 0) return; 582 | var mod = (ModItem)mods.SelectedItem; 583 | Configuration.Mod = (mod != null ? mod.Value : string.Empty); 584 | 585 | UpdateMapsList(); 586 | UpdateDemosList(); 587 | UpdateInterface(); 588 | UpdateCommandLinePreview(); 589 | } 590 | 591 | private void maps_SelectionChanged(object sender, SelectionChangedEventArgs e) 592 | { 593 | if(blockupdate || maps.Items.Count == 0) return; 594 | var map = (MapItem)maps.SelectedItem; 595 | Configuration.Map = (map != null ? map.Value : string.Empty); 596 | 597 | UpdateCommandLinePreview(); 598 | } 599 | 600 | private void skills_SelectionChanged(object sender, SelectionChangedEventArgs e) 601 | { 602 | if(blockupdate || skills.Items.Count == 0) return; 603 | var skill = (SkillItem)skills.SelectedItem; 604 | Configuration.Skill = skill.Value; 605 | 606 | UpdateCommandLinePreview(); 607 | } 608 | 609 | private void classes_SelectionChanged(object sender, SelectionChangedEventArgs e) 610 | { 611 | if(blockupdate || classes.Items.Count == 0) return; 612 | var pclass = (ClassItem)classes.SelectedItem; 613 | Configuration.Class = pclass.Value; 614 | 615 | UpdateCommandLinePreview(); 616 | } 617 | 618 | private void demos_SelectionChanged(object sender, SelectionChangedEventArgs e) 619 | { 620 | if(blockupdate || demos.Items.Count == 0) return; 621 | var demo = (DemoItem)demos.SelectedItem; 622 | Configuration.Demo = (demo != null ? demo.Value : string.Empty); 623 | 624 | UpdateInterface(); 625 | UpdateCommandLinePreview(); 626 | } 627 | 628 | private void clearcustomargs_Click(object sender, RoutedEventArgs e) 629 | { 630 | UpdateCommandLinePreview(true); 631 | } 632 | 633 | private void copyargs_Click(object sender, RoutedEventArgs e) 634 | { 635 | // Clipboard.SetText() is borked on many levels... 636 | Clipboard.SetDataObject(cmdline.GetCommandLine()); 637 | SystemSounds.Asterisk.Play(); 638 | } 639 | 640 | private void createshortcut_OnClick(object sender, RoutedEventArgs e) 641 | { 642 | createshortcut.ContextMenu.IsOpen = true; 643 | } 644 | 645 | private void createdesktopshortcut_OnClick(object sender, RoutedEventArgs e) 646 | { 647 | CreateShortcut(Environment.GetFolderPath(Environment.SpecialFolder.Desktop)); 648 | } 649 | 650 | private void createfoldershortcut_OnClick(object sender, RoutedEventArgs e) 651 | { 652 | var engine = (EngineItem)engines.SelectedItem; 653 | CreateShortcut(Path.GetDirectoryName(engine.FileName)); 654 | } 655 | 656 | // Restore window location (fitzquake messes this up when launching in fullscreen) 657 | private void ProcessOnExited(object sender, EventArgs e) 658 | { 659 | if(windowx == int.MaxValue || windowy == int.MaxValue) return; 660 | 661 | // Cross-thread call required... 662 | this.Dispatcher.Invoke(() => 663 | { 664 | // Restore window position 665 | this.Left = windowx; 666 | this.Top = windowy; 667 | }); 668 | } 669 | 670 | private void MainWindow_Loaded(object sender, RoutedEventArgs e) 671 | { 672 | Setup(); 673 | } 674 | 675 | private void MainWindow_Closing(object sender, System.ComponentModel.CancelEventArgs e) 676 | { 677 | cmdline.GetCommandLine(); // Force TextBox to store custom args... 678 | #if DEBUG 679 | Configuration.Save(); 680 | #else 681 | if(enginelaunched) Configuration.Save(); 682 | #endif 683 | } 684 | 685 | private void MainWindow_Activated(object sender, EventArgs e) 686 | { 687 | #if !DEBUG 688 | // Reload data after regaining focus 689 | if(focusstate == FocusState.UNFOCUSED) 690 | { 691 | focusstate = FocusState.FOCUSED; 692 | UpdateAll(); 693 | } 694 | #endif 695 | } 696 | 697 | private void MainWindow_Deactivated(object sender, EventArgs e) 698 | { 699 | focusstate = FocusState.UNFOCUSED; 700 | } 701 | 702 | #endregion 703 | } 704 | } 705 | --------------------------------------------------------------------------------