├── .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 | 
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 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
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 | Some
88 | text areas
89 | are editable.Middle-click an
90 | editable text area
91 | to clear it
92 |
93 |
94 |
95 |
96 |
100 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
132 |
133 |
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 |
--------------------------------------------------------------------------------