├── packages └── Newtonsoft.Json.12.0.3 │ ├── .signature.p7s │ ├── packageIcon.png │ ├── Newtonsoft.Json.12.0.3.nupkg │ ├── lib │ ├── net20 │ │ └── Newtonsoft.Json.dll │ ├── net35 │ │ └── Newtonsoft.Json.dll │ ├── net40 │ │ └── Newtonsoft.Json.dll │ ├── net45 │ │ └── Newtonsoft.Json.dll │ ├── netstandard1.0 │ │ └── Newtonsoft.Json.dll │ ├── netstandard1.3 │ │ └── Newtonsoft.Json.dll │ ├── netstandard2.0 │ │ └── Newtonsoft.Json.dll │ ├── portable-net45+win8+wp8+wpa81 │ │ └── Newtonsoft.Json.dll │ └── portable-net40+sl5+win8+wp8+wpa81 │ │ └── Newtonsoft.Json.dll │ └── LICENSE.md ├── SHCLiveStatReader ├── packages.config ├── App.config ├── memory │ ├── core.json │ ├── buildings.json │ ├── weights.json │ ├── greatestlord.json │ ├── names.json │ └── player.json ├── State.cs ├── SHCNotFoundException.cs ├── PlayerFactory.cs ├── Util.cs ├── Properties │ └── AssemblyInfo.cs ├── Program.cs ├── Constants.cs ├── SHCLiveStatReader.csproj ├── Reader.cs ├── StateMachine.cs ├── GreatestLord.cs └── Player.cs ├── SHCLiveStatReader.sln ├── readme.md └── .github └── workflows └── dotnet-core-desktop.yml /packages/Newtonsoft.Json.12.0.3/.signature.p7s: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/patel-nikhil/SHCLiveStatReader/HEAD/packages/Newtonsoft.Json.12.0.3/.signature.p7s -------------------------------------------------------------------------------- /packages/Newtonsoft.Json.12.0.3/packageIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/patel-nikhil/SHCLiveStatReader/HEAD/packages/Newtonsoft.Json.12.0.3/packageIcon.png -------------------------------------------------------------------------------- /SHCLiveStatReader/packages.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/Newtonsoft.Json.12.0.3/Newtonsoft.Json.12.0.3.nupkg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/patel-nikhil/SHCLiveStatReader/HEAD/packages/Newtonsoft.Json.12.0.3/Newtonsoft.Json.12.0.3.nupkg -------------------------------------------------------------------------------- /packages/Newtonsoft.Json.12.0.3/lib/net20/Newtonsoft.Json.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/patel-nikhil/SHCLiveStatReader/HEAD/packages/Newtonsoft.Json.12.0.3/lib/net20/Newtonsoft.Json.dll -------------------------------------------------------------------------------- /packages/Newtonsoft.Json.12.0.3/lib/net35/Newtonsoft.Json.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/patel-nikhil/SHCLiveStatReader/HEAD/packages/Newtonsoft.Json.12.0.3/lib/net35/Newtonsoft.Json.dll -------------------------------------------------------------------------------- /packages/Newtonsoft.Json.12.0.3/lib/net40/Newtonsoft.Json.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/patel-nikhil/SHCLiveStatReader/HEAD/packages/Newtonsoft.Json.12.0.3/lib/net40/Newtonsoft.Json.dll -------------------------------------------------------------------------------- /packages/Newtonsoft.Json.12.0.3/lib/net45/Newtonsoft.Json.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/patel-nikhil/SHCLiveStatReader/HEAD/packages/Newtonsoft.Json.12.0.3/lib/net45/Newtonsoft.Json.dll -------------------------------------------------------------------------------- /packages/Newtonsoft.Json.12.0.3/lib/netstandard1.0/Newtonsoft.Json.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/patel-nikhil/SHCLiveStatReader/HEAD/packages/Newtonsoft.Json.12.0.3/lib/netstandard1.0/Newtonsoft.Json.dll -------------------------------------------------------------------------------- /packages/Newtonsoft.Json.12.0.3/lib/netstandard1.3/Newtonsoft.Json.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/patel-nikhil/SHCLiveStatReader/HEAD/packages/Newtonsoft.Json.12.0.3/lib/netstandard1.3/Newtonsoft.Json.dll -------------------------------------------------------------------------------- /packages/Newtonsoft.Json.12.0.3/lib/netstandard2.0/Newtonsoft.Json.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/patel-nikhil/SHCLiveStatReader/HEAD/packages/Newtonsoft.Json.12.0.3/lib/netstandard2.0/Newtonsoft.Json.dll -------------------------------------------------------------------------------- /SHCLiveStatReader/App.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /packages/Newtonsoft.Json.12.0.3/lib/portable-net45+win8+wp8+wpa81/Newtonsoft.Json.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/patel-nikhil/SHCLiveStatReader/HEAD/packages/Newtonsoft.Json.12.0.3/lib/portable-net45+win8+wp8+wpa81/Newtonsoft.Json.dll -------------------------------------------------------------------------------- /packages/Newtonsoft.Json.12.0.3/lib/portable-net40+sl5+win8+wp8+wpa81/Newtonsoft.Json.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/patel-nikhil/SHCLiveStatReader/HEAD/packages/Newtonsoft.Json.12.0.3/lib/portable-net40+sl5+win8+wp8+wpa81/Newtonsoft.Json.dll -------------------------------------------------------------------------------- /SHCLiveStatReader/memory/core.json: -------------------------------------------------------------------------------- 1 | { 2 | "MapStartYear": "0x24BA938", 3 | "BackgroundPathAddress": "0x1311607", 4 | "LobbyBackgroundString": "shc_back.tgx", 5 | "PlayerDeathTime": "0x24BA918", 6 | "SHCProcessName": "Stronghold_Crusader_Extreme" 7 | } -------------------------------------------------------------------------------- /SHCLiveStatReader/memory/buildings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Buildings": { 3 | "address": "0x00F98DB2", 4 | "type": "integer", 5 | "category": "building", 6 | "owneroffset": "0x4", 7 | "workersneededoffset": "0xC6", 8 | "workersworkingoffset": "0xC8", 9 | "workersmissingoffset": "0xCA", 10 | "snoozedoffset": "0x1C4", 11 | "offset": "0x32C", 12 | "total": "0x00F989A0" 13 | } 14 | } -------------------------------------------------------------------------------- /SHCLiveStatReader/State.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace SHC 4 | { 5 | class State 6 | { 7 | readonly string name; 8 | readonly Func testActive; 9 | 10 | public State(string state, Func isActive) 11 | { 12 | this.name = state; 13 | this.testActive = isActive; 14 | } 15 | 16 | public bool IsActive() 17 | { 18 | return testActive(); 19 | } 20 | 21 | public override string ToString() 22 | { 23 | return this.name; 24 | } 25 | 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /SHCLiveStatReader/SHCNotFoundException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.Serialization; 3 | 4 | namespace SHC 5 | { 6 | [Serializable] 7 | class SHCNotFoundException : Exception 8 | { 9 | public SHCNotFoundException() : base() 10 | { 11 | } 12 | 13 | public SHCNotFoundException(string message) : base(message) 14 | { 15 | } 16 | 17 | public SHCNotFoundException(string message, Exception innerException) : base(message, innerException) 18 | { 19 | } 20 | 21 | protected SHCNotFoundException(SerializationInfo info, StreamingContext context) : base(info, context) 22 | { 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /SHCLiveStatReader/PlayerFactory.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using static SHC.Constants; 5 | 6 | namespace SHC 7 | { 8 | class PlayerFactory 9 | { 10 | public static List PlayerList { get; } 11 | 12 | static PlayerFactory() 13 | { 14 | Player.Data = 15 | JsonConvert.DeserializeObject>>(File.ReadAllText("memory/player.json")); 16 | 17 | PlayerList = new List(); 18 | 19 | for (int i = 0; i < MAX_PLAYERS; i++) 20 | { 21 | PlayerList.Add(new Player(i + 1)); 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /SHCLiveStatReader/Util.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Text; 4 | using static SHC.Constants; 5 | 6 | namespace SHC 7 | { 8 | public class Util 9 | { 10 | 11 | static readonly Random gen = new Random(); 12 | 13 | public static int GetNextRandom() 14 | { 15 | return gen.Next(); 16 | } 17 | 18 | public static string GetFilename(string prefix) 19 | { 20 | string newFileName = prefix + gen.Next().ToString() + DATA_FILE_SUFFIX; 21 | while (File.Exists(newFileName)) 22 | { 23 | newFileName = prefix + gen.Next().ToString() + DATA_FILE_SUFFIX; 24 | } 25 | return newFileName; 26 | } 27 | 28 | public static void WriteData(string filename, object data) 29 | { 30 | File.WriteAllText(filename, Newtonsoft.Json.JsonConvert.SerializeObject(data, Newtonsoft.Json.Formatting.Indented), Encoding.BigEndianUnicode); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/Newtonsoft.Json.12.0.3/LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2007 James Newton-King 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /SHCLiveStatReader.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.28010.2048 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SHCLiveStatReader", "SHCLiveStatReader\SHCLiveStatReader.csproj", "{CC049475-77CC-4FCE-9B59-649494A66141}" 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 | {CC049475-77CC-4FCE-9B59-649494A66141}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {CC049475-77CC-4FCE-9B59-649494A66141}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {CC049475-77CC-4FCE-9B59-649494A66141}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {CC049475-77CC-4FCE-9B59-649494A66141}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {84A23AE1-E24A-441B-AE13-5EF0EF164527} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /SHCLiveStatReader/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 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("SHCLiveStatReader")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("SHCLiveStatReader")] 13 | [assembly: AssemblyCopyright("Copyright © 2020")] 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 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("cc049475-77cc-4fce-9b59-649494a66141")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /SHCLiveStatReader/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using static SHC.Constants; 4 | using static SHC.Util; 5 | 6 | namespace SHC 7 | { 8 | class Program 9 | { 10 | public static void Main() 11 | { 12 | BackupExistingFile(PLAYERDATA_FILENAME, GREATEST_LORD_FILENAME); 13 | 14 | while(true){ 15 | try 16 | { 17 | StateMachine.Update(); 18 | } 19 | catch (SHCNotFoundException) { } 20 | catch (Exception e) 21 | { 22 | Console.WriteLine(e.Message + "\n" + e.StackTrace); 23 | File.WriteAllText(PLAYERDATA_FILENAME, string.Empty); 24 | } 25 | } 26 | } 27 | 28 | private static void BackupExistingFile(string playerDataFilename, string greatestLordDataFilename) 29 | { 30 | if (File.Exists(playerDataFilename) || File.Exists(greatestLordDataFilename)) 31 | { 32 | string suffix = GetNextRandom().ToString(); 33 | while (File.Exists(PLAYERDATA_FILE_PREFIX + suffix + DATA_FILE_SUFFIX) || File.Exists(GREATEST_LORD_FILE_PREFIX + suffix + DATA_FILE_SUFFIX)) 34 | { 35 | suffix = GetNextRandom().ToString(); 36 | } 37 | if (File.Exists(playerDataFilename)) 38 | { 39 | File.Move(playerDataFilename, PLAYERDATA_FILE_PREFIX + suffix + DATA_FILE_SUFFIX); 40 | } 41 | if (File.Exists(greatestLordDataFilename)) 42 | { 43 | File.Move(greatestLordDataFilename, GREATEST_LORD_FILE_PREFIX + suffix + DATA_FILE_SUFFIX); 44 | } 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # What it is 2 | 3 | SHCLiveStatReader is a program designed to read and output game statistics from the game [Stronghold Crusader Extreme](https://fireflyworlds.com/games/strongholdcrusader/) made by Firefly Studios. 4 | 5 | # How it works 6 | 7 | SHCLiveStatReader reads the computer memory owned by the game's process and writes out relevant player and match data to two text files: `SHCPlayerData.json` and `GreatestLord.json`. 8 | 9 | The `SHCPlayerData.json` file contains statistics such as unit count, building count, resource counts for each player active in the current game. The `GreatestLord.json` contains cumulative statistics for the current match such as total gold (and other goods) produced, units killed and lost, and more. 10 | 11 | 12 | # What do you need to use it 13 | 14 | You will need the executable `SHCLiveStatReader.exe`, the dll `Newtonsoft.Json.dll`, and the `memory` folder and all the json files inside. Simply start the executable, and then start Stronghold Crusader Extreme. When you start a match (skirmish, multiplayer, campaign) it will show `Switched to state: game` in the window and the stats will be read. 15 | 16 | # I want new data, or to change the data being read 17 | 18 | These json files can be modified to change what data is being read by SHCLiveStatReader so long as you follow the same format. Deleting the files may cause the executable to break. Remove fields at your own risk - certain fields such as `Active` in `player.json ` are how SHCLiveStatReader knows what player numbers are participating in a game. 19 | 20 | **`core.json`** should NOT be modified unless you are using a non 1.41 version or a mod that is not the Unofficial Crusader Patch and SHCLiveStatReader is not working. 21 | 22 | # Compatibility 23 | 24 | `SHCLiveStatReader` is targeted to work with the 1.41 English version of Stronghold Crusader Extreme. When launching the game you can check this by looking at the bottom left corner by the exit door - you should see the text V1.41-E or V1.41.1-E (this indicates game version number, and `E` indicates it is `Stronghold Crusader Extreme`, not regular `Stronghold Crusader`) 25 | 26 | # Disclaimer 27 | 28 | Stronghold Crusader is the legal property of Firefly Studios. This project has no affiliation and is not endorsed or supported by Firefly Studios. This content provided in this project does not modify the executable or other game files in any way. -------------------------------------------------------------------------------- /SHCLiveStatReader/memory/weights.json: -------------------------------------------------------------------------------- 1 | { 2 | "Buildings": { 3 | 4 | "1": 1, 5 | "2": 1, 6 | "3": 1, 7 | "4": 1, 8 | "5": 3, 9 | "6": 3, 10 | "7": 1, 11 | "8": 3, 12 | "9": 3, 13 | "10": 0, 14 | "11": 10, 15 | "12": 5, 16 | "13": 5, 17 | "14": 5, 18 | "15": 5, 19 | "16": 5, 20 | "17": 3, 21 | "18": 3, 22 | "19": 10, 23 | "20": 3, 24 | "21": 0, 25 | "22": 3, 26 | "23": 3, 27 | "24": 5, 28 | "25": 5, 29 | "26": 3, 30 | "27": 3, 31 | "28": 3, 32 | "29": 0, 33 | "30": 1, 34 | "31": 1, 35 | "32": 1, 36 | "33": 1, 37 | "34": 3, 38 | "35": 3, 39 | "36": 3, 40 | "37": 3, 41 | "38": 3, 42 | "39": 0, 43 | "40": 0, 44 | "41": 0, 45 | "42": 0, 46 | "43": 0, 47 | "44": 0, 48 | "45": 5, 49 | "46": 5, 50 | "47": 5, 51 | "48": 5, 52 | "49": 3, 53 | "50": 0, 54 | "51": 0, 55 | "52": 0, 56 | "53": 0, 57 | "54": 0, 58 | "55": 0, 59 | "56": 0, 60 | "57": 0, 61 | "58": 0, 62 | "59": 0, 63 | "60": 5, 64 | "61": 5, 65 | "62": 1, 66 | "63": 1, 67 | "64": 1, 68 | "65": 1, 69 | "66": 1, 70 | "67": 1, 71 | "68": 1, 72 | "69": 0, 73 | "70": 3, 74 | "71": 0, 75 | "72": 0, 76 | "73": 0, 77 | "74": 5, 78 | "75": 5, 79 | "76": 5, 80 | "77": 5, 81 | "78": 5, 82 | "79": 0, 83 | "80": 0, 84 | "81": 0, 85 | "82": 0, 86 | "83": 0, 87 | "84": 0, 88 | "85": 0, 89 | "86": 0, 90 | "87": 0, 91 | "88": 0, 92 | "89": 0, 93 | "90": 0, 94 | "91": 1, 95 | "92": 1, 96 | "93": 1, 97 | "94": 1, 98 | "95": 1, 99 | "96": 1, 100 | "97": 1, 101 | "98": 1, 102 | "99": 1, 103 | "100": 1, 104 | "101": 1, 105 | "102": 1, 106 | "103": 1, 107 | "104": 1, 108 | "105": 1, 109 | "106": 20, 110 | "107": 20, 111 | "108": 0 112 | }, 113 | 114 | "Resources": { 115 | 116 | "Wood": 1, 117 | "Stone": 7, 118 | "Iron": 23, 119 | "Pitch": 15, 120 | "Apples": 4, 121 | "Meat": 4, 122 | "Cheese": 4, 123 | "Bread": 4, 124 | "MetalArmor": 30, 125 | "LeatherArmor": 12, 126 | "Swords": 30, 127 | "Maces": 30, 128 | "Pikes": 18, 129 | "Spears": 10, 130 | "Crossbows": 30, 131 | "Bows": 15, 132 | "Flour": 10, 133 | "Hops": 8, 134 | "Ale": 10, 135 | "Wheat": 8 136 | } 137 | } -------------------------------------------------------------------------------- /.github/workflows/dotnet-core-desktop.yml: -------------------------------------------------------------------------------- 1 | # gynt: This is based on the .NET Core Desktop starter action 2 | 3 | 4 | name: build 5 | 6 | on: 7 | workflow_dispatch: 8 | push: 9 | branches: [ master, v1.1, v1.1-german, dev ] 10 | pull_request: 11 | branches: [ master, v1.1, v1.1-german, dev ] 12 | types: [assigned, opened, edited, ready_for_review, reopened, synchronize] 13 | 14 | jobs: 15 | 16 | build: 17 | 18 | strategy: 19 | matrix: 20 | configuration: [Debug, Release] 21 | nuget: [latest] 22 | 23 | runs-on: windows-latest # For a list of available runner types, refer to 24 | # https://help.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idruns-on 25 | 26 | env: 27 | OutputPath: ..\Output${{ matrix.configuration }} 28 | Solution_Name: SHCLiveStatReader.sln 29 | 30 | steps: 31 | - name: Checkout 32 | uses: actions/checkout@v2 33 | with: 34 | fetch-depth: 0 35 | 36 | - name: Checkout submodules 37 | run: git submodule update --init --recursive --depth 1 38 | 39 | # Install the .NET Core workload 40 | - name: Install .NET Core 41 | uses: actions/setup-dotnet@v1 42 | with: 43 | dotnet-version: '3.1.x' 44 | 45 | # Restore NuGet packages 46 | - name: Setup NuGet.exe 47 | uses: nuget/setup-nuget@v1 48 | with: 49 | nuget-version: ${{ matrix.nuget }} 50 | - name: Restore NuGet packages 51 | run: nuget restore $env:Solution_Name 52 | 53 | # Add MSBuild to the PATH: https://github.com/microsoft/setup-msbuild 54 | - name: Setup MSBuild.exe 55 | uses: microsoft/setup-msbuild@v1.0.2 56 | 57 | # Execute all unit tests in the solution 58 | # - name: Execute unit tests 59 | # run: dotnet test 60 | 61 | # Restore the application to populate the obj folder with RuntimeIdentifiers 62 | - name: Build the application 63 | run: msbuild $env:Solution_Name /p:Configuration=$env:Configuration /p:OutputPath=$env:OutputPath 64 | env: 65 | Configuration: ${{ matrix.configuration }} 66 | 67 | - name: Upload build artifacts 68 | uses: actions/upload-artifact@v2 69 | with: 70 | name: latest-release 71 | path: | 72 | OutputRelease/*.dll 73 | OutputRelease/*.exe 74 | OutputRelease/memory 75 | 76 | - name: Upload build artifacts 77 | uses: actions/upload-artifact@v2 78 | with: 79 | name: latest-debug 80 | path: | 81 | OutputDebug 82 | -------------------------------------------------------------------------------- /SHCLiveStatReader/memory/greatestlord.json: -------------------------------------------------------------------------------- 1 | { 2 | "Map": { 3 | "MapName": { 4 | "address": "0x24B649C", 5 | "type": "string" 6 | }, 7 | "MapStartYear": { 8 | "address": "0x24BA938", 9 | "type": "integer" 10 | }, 11 | "MapStartMonth": { 12 | "address": "0x24BA93C", 13 | "type": "byte" 14 | }, 15 | "MapEndYear": { 16 | "address": "0x24BA940", 17 | "type": "integer" 18 | }, 19 | "MapEndMonth": { 20 | "address": "0x24BA944", 21 | "type": "byte" 22 | }, 23 | "AdvantageSetting": { 24 | "address": "0x24B7F4C", 25 | "type": "byte" 26 | } 27 | }, 28 | "Player": { 29 | "Active": { 30 | "address": "0x24BA557", 31 | "type": "boolean", 32 | "category": "other", 33 | "offset": "0x1" 34 | }, 35 | "Name": { 36 | "address": "0x24BA286", 37 | "type": "string", 38 | "category": "other", 39 | "offset": "0x5A" 40 | }, 41 | "Team": { 42 | "address": "0x24BAAB6", 43 | "type": "byte", 44 | "category": "other", 45 | "offset": "0x1" 46 | }, 47 | "Gold": { 48 | "address": "0x24BA564", 49 | "type": "integer", 50 | "category": "other", 51 | "offset": "0x4" 52 | }, 53 | "Units": { 54 | "address": "0x24BA888", 55 | "type": "integer", 56 | "category": "military", 57 | "offset": "0x4" 58 | }, 59 | "WeightedTroopsKilled": { 60 | "address": "0x24BA840", 61 | "type": "integer", 62 | "category": "military", 63 | "offset": "0x4" 64 | }, 65 | "WeightedBuildingsDestroyed": { 66 | "address": "0x24BA864", 67 | "type": "integer", 68 | "category": "military", 69 | "offset": "0x4" 70 | }, 71 | "LordKills": { 72 | "address": "0x24BA7EA", 73 | "type": "byte", 74 | "category": "military", 75 | "offset": "0x1" 76 | }, 77 | "Food": { 78 | "address": "0x24BA730", 79 | "type": "integer", 80 | "category": "resource", 81 | "offset": "0x4" 82 | }, 83 | "Wood": { 84 | "address": "0x24BA79C", 85 | "type": "integer", 86 | "category": "resource", 87 | "offset": "0x4" 88 | }, 89 | "Stone": { 90 | "address": "0x24BA778", 91 | "type": "integer", 92 | "category": "resource", 93 | "offset": "0x4" 94 | }, 95 | "Weapons": { 96 | "address": "0x24BA7F8", 97 | "type": "integer", 98 | "category": "resource", 99 | "offset": "0x4" 100 | }, 101 | "GoodsSent": { 102 | "address": "0x24BA8D0", 103 | "type": "integer", 104 | "category": "resource", 105 | "offset": "0x4" 106 | }, 107 | "GoodsReceived": { 108 | "address": "0x24BA8AC", 109 | "type": "integer", 110 | "category": "resource", 111 | "offset": "0x4" 112 | } 113 | } 114 | } -------------------------------------------------------------------------------- /SHCLiveStatReader/Constants.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | 6 | namespace SHC 7 | { 8 | public class Constants 9 | { 10 | public static string PLAYERDATA_FILENAME = "SHCPlayerData.json"; 11 | public static string GREATEST_LORD_FILENAME = "GreatestLord.json"; 12 | public static string SHC_PROCESS_NAME = "Stronghold_Crusader_Extreme"; 13 | 14 | public static string PLAYERDATA_FILE_PREFIX = "SHCPlayerData"; 15 | public static string GREATEST_LORD_FILE_PREFIX = "GreatestLord"; 16 | public static string DATA_FILE_SUFFIX = ".json"; 17 | 18 | public static string MSG_UPDATE_STATE = "Switched to state: "; 19 | 20 | public static string GAME = "game"; 21 | public static string LOBBY = "lobby"; 22 | public static string STATS = "stats"; 23 | 24 | public static int MAP_START_YEAR; 25 | public static int BACKGROUND_PATH_ADDRESS; 26 | public static string LOBBY_BACKGROUND_FILE; 27 | public static int PLAYER_DEATH_TIME; 28 | 29 | public static int MAX_PLAYERS = 8; 30 | 31 | static Constants() 32 | { 33 | Dictionary coreVariables; 34 | if (File.Exists("memory/core.json")) 35 | { 36 | coreVariables = JsonConvert.DeserializeObject>(File.ReadAllText("memory/core.json")); 37 | } 38 | else 39 | { 40 | coreVariables = new Dictionary(); 41 | } 42 | 43 | if (coreVariables.TryGetValue("MapStartYear", out string mapStartYear)) 44 | { 45 | MAP_START_YEAR = Convert.ToInt32(mapStartYear, 16); 46 | } else 47 | { 48 | MAP_START_YEAR = Convert.ToInt32("0x24BA938", 16); 49 | } 50 | 51 | if (!coreVariables.TryGetValue("BackgroundPathAddress", out string backgroundPathAddress)) 52 | { 53 | BACKGROUND_PATH_ADDRESS = Convert.ToInt32(backgroundPathAddress, 16); 54 | } 55 | else 56 | { 57 | BACKGROUND_PATH_ADDRESS = Convert.ToInt32("0x1311607", 16); 58 | } 59 | 60 | if (!coreVariables.TryGetValue("LobbyBackgroundString", out LOBBY_BACKGROUND_FILE)) 61 | { 62 | LOBBY_BACKGROUND_FILE = "shc_back.tgx"; 63 | } 64 | 65 | if (!coreVariables.TryGetValue("SHCProcessName", out SHC_PROCESS_NAME)) 66 | { 67 | SHC_PROCESS_NAME = "Stronghold_Crusader_Extreme"; 68 | } 69 | 70 | if (!coreVariables.TryGetValue("PlayerDeathTime", out string playerDeathTime)) 71 | { 72 | PLAYER_DEATH_TIME = Convert.ToInt32(playerDeathTime, 16); 73 | } 74 | else 75 | { 76 | PLAYER_DEATH_TIME = Convert.ToInt32("0x24BA918", 16); 77 | } 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /SHCLiveStatReader/SHCLiveStatReader.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {CC049475-77CC-4FCE-9B59-649494A66141} 8 | Exe 9 | SHC 10 | SHCLiveStatReader 11 | v4.6.1 12 | 512 13 | true 14 | true 15 | 16 | 17 | AnyCPU 18 | true 19 | full 20 | false 21 | bin\Debug\ 22 | DEBUG;TRACE 23 | prompt 24 | 4 25 | 26 | 27 | AnyCPU 28 | pdbonly 29 | true 30 | bin\Release\ 31 | TRACE 32 | prompt 33 | 4 34 | 35 | 36 | 37 | ..\packages\Newtonsoft.Json.12.0.3\lib\net45\Newtonsoft.Json.dll 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 | Always 65 | 66 | 67 | Always 68 | 69 | 70 | Always 71 | 72 | 73 | Always 74 | 75 | 76 | Always 77 | 78 | 79 | Always 80 | 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /SHCLiveStatReader/Reader.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Runtime.InteropServices; 4 | using System.Text; 5 | using static SHC.Constants; 6 | 7 | namespace SHC 8 | { 9 | 10 | public class Reader 11 | { 12 | const int PROCESS_WM_READ = 0x0010; 13 | 14 | [DllImport("kernel32.dll")] 15 | public static extern IntPtr OpenProcess(int dwDesiredAccess, bool bInheritHandle, int dwProcessId); 16 | 17 | [DllImport("kernel32.dll")] 18 | public static extern bool ReadProcessMemory(int hProcess, int lpBaseAddress, byte[] lpBuffer, int dwSize, ref int lpNumberOfBytesRead); 19 | 20 | static IntPtr GetProcessHandle() 21 | { 22 | try 23 | { 24 | Process process = Process.GetProcessesByName(SHC_PROCESS_NAME)[0]; 25 | return OpenProcess(PROCESS_WM_READ, false, process.Id); 26 | } 27 | catch (Exception) 28 | { 29 | throw new SHCNotFoundException(); 30 | } 31 | } 32 | 33 | public static bool TestZero(int addr, int size) => Reader.ReadInt(addr, size) == 0; 34 | public static bool IsStatic(int addr, int size) { 35 | int val = ReadInt(addr, size); 36 | System.Threading.Thread.Sleep(20); 37 | return ReadInt(addr, size) == val; 38 | } 39 | 40 | public static int ReadInt(int addr, int size) 41 | { 42 | IntPtr processHandle = GetProcessHandle(); 43 | int bytesRead = 0; 44 | byte[] buffer = new byte[size]; 45 | ReadProcessMemory((int)processHandle, addr, buffer, buffer.Length, ref bytesRead); 46 | return BitConverter.ToInt32(buffer, 0); 47 | } 48 | 49 | public static int ReadWord(int addr, int size) 50 | { 51 | IntPtr processHandle = GetProcessHandle(); 52 | int bytesRead = 0; 53 | byte[] buffer = new byte[size]; 54 | ReadProcessMemory((int)processHandle, addr, buffer, buffer.Length, ref bytesRead); 55 | return BitConverter.ToInt16(buffer, 0); 56 | } 57 | 58 | public static byte ReadByte(int addr) 59 | { 60 | IntPtr processHandle = GetProcessHandle(); 61 | int bytesRead = 0; 62 | byte[] buffer = new byte[1]; 63 | ReadProcessMemory((int)processHandle, addr, buffer, buffer.Length, ref bytesRead); 64 | return buffer[0]; 65 | } 66 | 67 | public static bool ReadBool(int addr, int size) 68 | { 69 | IntPtr processHandle = GetProcessHandle(); 70 | int bytesRead = 0; 71 | byte[] buffer = new byte[size]; 72 | ReadProcessMemory((int)processHandle, addr, buffer, buffer.Length, ref bytesRead); 73 | return BitConverter.ToBoolean(buffer, 0); 74 | } 75 | 76 | public static string ReadString(int addr) 77 | { 78 | IntPtr processHandle = GetProcessHandle(); 79 | int bytesRead = 0; 80 | byte[] buffer = new byte[90]; 81 | ReadProcessMemory((int)processHandle, addr, buffer, buffer.Length, ref bytesRead); 82 | return Encoding.GetEncoding(1252).GetString(buffer).Split('\0')[0]; 83 | } 84 | 85 | public static byte[] ReadBytes(int addr, int size) 86 | { 87 | IntPtr processHandle = GetProcessHandle(); 88 | int bytesRead = 0; 89 | byte[] buffer = new byte[size]; 90 | ReadProcessMemory((int)processHandle, addr, buffer, buffer.Length, ref bytesRead); 91 | return buffer; 92 | } 93 | 94 | public static object ReadType(int addr, string type) 95 | { 96 | if (type == "integer") 97 | { 98 | return Reader.ReadInt(addr, 4); 99 | } 100 | else if (type == "word") 101 | { 102 | return Reader.ReadWord(addr, 2); 103 | } 104 | else if (type == "byte") 105 | { 106 | return Reader.ReadByte(addr); 107 | } 108 | else if (type == "boolean") 109 | { 110 | return Reader.ReadBool(addr, 1); 111 | } 112 | else if (type == "string") 113 | { 114 | return Reader.ReadString(addr); 115 | } 116 | return 0; 117 | } 118 | } 119 | } -------------------------------------------------------------------------------- /SHCLiveStatReader/memory/names.json: -------------------------------------------------------------------------------- 1 | { 2 | "Buildings": { 3 | 4 | "1": "hovel", 5 | "2": "house", 6 | "3": "woodcuttershut", 7 | "4": "oxtether", 8 | "5": "ironmine", 9 | "6": "pitchrig", 10 | "7": "huntershut", 11 | "8": "mercenarypost", 12 | "9": "barracks", 13 | "10": "stockpile", 14 | "11": "armory", 15 | "12": "fletcher", 16 | "13": "blacksmith", 17 | "14": "poleturner", 18 | "15": "armourer", 19 | "16": "tanner", 20 | "17": "bakery", 21 | "18": "brewery", 22 | "19": "granary", 23 | "20": "quarry", 24 | "21": "quarrypile", 25 | "22": "inn", 26 | "23": "apothecary", 27 | "24": "engineerguild", 28 | "25": "tunnelerguild", 29 | "26": "marketplace", 30 | "27": "well", 31 | "28": "oilsmelter", 32 | "29": "siege_tent", 33 | "30": "wheat_farm", 34 | "31": "hop_farm", 35 | "32": "apple_farm", 36 | "33": "dairy_farm", 37 | "34": "mill", 38 | "35": "stables", 39 | "36": "chapel", 40 | "37": "church", 41 | "38": "cathedral", 42 | "39": "unused", 43 | "40": "keep_one", 44 | "41": "keep_two", 45 | "42": "keep_three", 46 | "43": "keep_four", 47 | "44": "keep_five", 48 | "45": "large_gatehouse", 49 | "46": "small_gatehouse", 50 | "47": "main_wood", 51 | "48": "postern_gate", 52 | "49": "drawbridge", 53 | "50": "tunnel", 54 | "51": "camp_fire", 55 | "52": "signpost", 56 | "53": "parade_ground", 57 | "54": "s_fballista", 58 | "55": "campground", 59 | "56": "parade_ground", 60 | "57": "parade_ground", 61 | "58": "parade_ground", 62 | "59": "parade_ground", 63 | "60": "gatehouse", 64 | "61": "tower", 65 | "62": "gallows", 66 | "63": "stocks", 67 | "64": "witch_hoist", 68 | "65": "maypole", 69 | "66": "garden", 70 | "67": "killingpit", 71 | "68": "pitchditch", 72 | "69": "unused", 73 | "70": "waterpot", 74 | "71": "keepdoor_left", 75 | "72": "keepdoor_right", 76 | "73": "keepdoor", 77 | "74": "tower_one", 78 | "75": "tower_two", 79 | "76": "tower_three", 80 | "77": "tower_four", 81 | "78": "tower_five", 82 | "79": "unused", 83 | "80": "s_catapult", 84 | "81": "s_trebuchet", 85 | "82": "s_batteringram", 86 | "83": "s_siegetower", 87 | "84": "s_shield", 88 | "85": "unused", 89 | "86": "s_mangonel", 90 | "87": "s_balista", 91 | "88": "unused", 92 | "89": "unused", 93 | "90": "unused", 94 | "91": "cesspit", 95 | "92": "burningstake", 96 | "93": "gibbet", 97 | "94": "dungeon", 98 | "95": "stretchingrack", 99 | "96": "rack_flogging", 100 | "97": "choppingblock", 101 | "98": "dunkingstool", 102 | "99": "dogcage", 103 | "100": "statue", 104 | "101": "shrine", 105 | "102": "bee_hive", 106 | "103": "dancingbear", 107 | "104": "pond", 108 | "105": "bear cave", 109 | "106": "outpost", 110 | "107": "outpost" 111 | }, 112 | "Units": { 113 | "1": "peasant", 114 | "2": "burningman", 115 | "3": "woodcutter", 116 | "4": "fletcher", 117 | "5": "tunneler", 118 | "6": "hunter", 119 | "7": "???", 120 | "8": "???", 121 | "9": "???", 122 | "10": "???", 123 | "11": "???", 124 | "12": "???", 125 | "13": "???", 126 | "14": "???", 127 | "15": "child", 128 | "16": "baker", 129 | "17": "woman", 130 | "18": "poleturner", 131 | "19": "smith", 132 | "20": "armorer", 133 | "21": "tanner", 134 | "22": "e_archer", 135 | "23": "e_xbow", 136 | "24": "e_spear", 137 | "25": "e_pike", 138 | "26": "e_mace", 139 | "27": "e_sword", 140 | "28": "e_knight", 141 | "29": "e_ladder", 142 | "30": "e_engineer", 143 | "31": "???", 144 | "32": "???", 145 | "33": "priest", 146 | "34": "???", 147 | "35": "drunk", 148 | "36": "innkeep", 149 | "37": "e_monk", 150 | "38": "???", 151 | "39": "s_catapult", 152 | "40": "s_trebuchet", 153 | "41": "s_mangonel", 154 | "42": "???", 155 | "43": "???", 156 | "44": "antelope", 157 | "45": "lion", 158 | "46": "rabbit", 159 | "47": "camel", 160 | "48": "???", 161 | "49": "???", 162 | "50": "siegetent", 163 | "51": "???", 164 | "52": "???", 165 | "53": "fireman", 166 | "54": "ghost", 167 | "55": "lord", 168 | "56": "lady", 169 | "57": "chester", 170 | "58": "stower", 171 | "59": "s_batteringram", 172 | "60": "s_shield", 173 | "61": "s_ballista", 174 | "62": "chicken", 175 | "63": "???", 176 | "64": "???", 177 | "65": "juggler", 178 | "66": "fireeater", 179 | "67": "dog", 180 | "68": "???", 181 | "69": "???", 182 | "70": "a_archer", 183 | "71": "a_slave", 184 | "72": "a_slinger", 185 | "73": "a_assassin", 186 | "74": "a_harcher", 187 | "75": "a_sword", 188 | "76": "a_firethrower", 189 | "77": "s_fballista", 190 | "78": "???", 191 | "79": "???" 192 | } 193 | } -------------------------------------------------------------------------------- /SHCLiveStatReader/StateMachine.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using static SHC.Constants; 5 | using static SHC.Util; 6 | 7 | namespace SHC 8 | { 9 | class StateMachine 10 | { 11 | static readonly Dictionary stateList = new Dictionary(); 12 | static State currentState; 13 | 14 | static readonly LinkedList> playerStats = new LinkedList>(); 15 | 16 | static StateMachine() 17 | { 18 | stateList[LOBBY] = new State(LOBBY, () => Reader.TestZero(MAP_START_YEAR, 4)); 19 | stateList[GAME] = new State(GAME, () => !Reader.TestZero(MAP_START_YEAR, 4) && !Reader.ReadString(BACKGROUND_PATH_ADDRESS).Equals(LOBBY_BACKGROUND_FILE)); 20 | stateList[STATS] = new State(STATS, () => !Reader.TestZero(MAP_START_YEAR, 4)); 21 | 22 | currentState = stateList[LOBBY]; 23 | } 24 | 25 | public static bool Lobby() => currentState == stateList[LOBBY]; 26 | public static bool Game() => currentState == stateList[GAME]; 27 | public static bool Stats() => currentState == stateList[STATS]; 28 | 29 | public static void Reset() 30 | { 31 | currentState = stateList[LOBBY]; 32 | } 33 | 34 | public static void Update() 35 | { 36 | if (!currentState.IsActive()) 37 | { 38 | State prevState = StateMachine.currentState; 39 | currentState = StateMachine.Next(); 40 | Console.WriteLine(MSG_UPDATE_STATE + currentState.ToString()); 41 | 42 | if (Stats()) 43 | { 44 | WriteData(GREATEST_LORD_FILENAME, GreatestLord.Update(playerStats)); 45 | } else if (Game() && prevState == stateList[LOBBY]) 46 | { 47 | ArchiveGreatestLordStatFile(); 48 | } 49 | } 50 | 51 | if (Game()) 52 | { 53 | UpdatePlayerStats(playerStats); 54 | WriteData(PLAYERDATA_FILENAME, playerStats); 55 | WriteData(GREATEST_LORD_FILENAME, GreatestLord.Update(playerStats)); 56 | } 57 | 58 | if (Lobby()) 59 | { 60 | ResetWeightedStats(); 61 | } 62 | } 63 | 64 | private static void UpdatePlayerStats(LinkedList> playerStats) 65 | { 66 | playerStats.Clear(); 67 | for (int i = 0; i < PlayerFactory.PlayerList.Count; i++) 68 | { 69 | Player player = PlayerFactory.PlayerList[i]; 70 | if (IsPlayerActive(i)) 71 | { 72 | playerStats.AddLast(player.Update()); 73 | } 74 | } 75 | Player.Update(playerStats); 76 | } 77 | 78 | private static void ArchiveGreatestLordStatFile() 79 | { 80 | if (File.Exists(GREATEST_LORD_FILENAME)) 81 | { 82 | string saveFileName = GetFilename(GREATEST_LORD_FILE_PREFIX); 83 | File.Move(GREATEST_LORD_FILENAME, saveFileName); 84 | } 85 | } 86 | 87 | private static bool IsPlayerActive(int playerNumber) 88 | { 89 | int baseAddress = Convert.ToInt32(Player.Data["Active"]["address"].ToString(), 16); 90 | int playerOffset = Convert.ToInt32(Player.Data["Active"]["offset"].ToString(), 16) * playerNumber; 91 | object active = Reader.ReadType(baseAddress + playerOffset, "boolean"); 92 | 93 | if (active.ToString().ToLowerInvariant() == "false") 94 | { 95 | return false; 96 | } 97 | return true; 98 | } 99 | 100 | private static State Next() 101 | { 102 | if (currentState == stateList[LOBBY]) 103 | { 104 | return stateList[GAME]; 105 | } 106 | else if (currentState == stateList[GAME]) 107 | { 108 | if (Reader.TestZero(MAP_START_YEAR, 4)) 109 | { 110 | return stateList[LOBBY]; 111 | } 112 | else 113 | { 114 | return stateList[STATS]; 115 | } 116 | } 117 | else if (currentState == stateList[STATS]) 118 | { 119 | if (Reader.TestZero(MAP_START_YEAR, 4)) 120 | { 121 | return stateList[LOBBY]; 122 | } 123 | else 124 | { 125 | return stateList[GAME]; 126 | } 127 | } 128 | return stateList[LOBBY]; 129 | } 130 | 131 | private static void ResetWeightedStats() 132 | { 133 | foreach (Player player in PlayerFactory.PlayerList) 134 | { 135 | player.ResetWeightedStats(); 136 | } 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /SHCLiveStatReader/GreatestLord.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using static SHC.Constants; 6 | 7 | namespace SHC 8 | { 9 | class GreatestLord 10 | { 11 | readonly static Dictionary>> playerData; 12 | readonly static Dictionary statsDictionary; 13 | public static List PlayerList { get; } 14 | 15 | static GreatestLord() 16 | { 17 | statsDictionary = new Dictionary(); 18 | playerData = 19 | JsonConvert.DeserializeObject>>>(File.ReadAllText("memory/greatestlord.json")); 20 | } 21 | 22 | public static Dictionary Update(LinkedList> endingPlayerStats) 23 | { 24 | Dictionary scoreDict = new Dictionary(); 25 | scoreDict["Gold"] = 0; 26 | scoreDict["WeightedTroopsKilled"] = 0; 27 | scoreDict["LordKills"] = 0; 28 | scoreDict["MapStartYear"] = 0; 29 | scoreDict["MapStartMonth"] = 0; 30 | scoreDict["MapEndYear"] = 0; 31 | scoreDict["MapEndMonth"] = 0; 32 | scoreDict["WeightedBuildingsDestroyed"] = 0; 33 | 34 | Dictionary mapStats = new Dictionary(); 35 | 36 | foreach (KeyValuePair> entry in playerData["Map"]) 37 | { 38 | int addr = Convert.ToInt32(entry.Value["address"], 16); 39 | object value = Reader.ReadType(addr, entry.Value["type"].ToString()); 40 | mapStats[entry.Key] = value; 41 | try 42 | { 43 | scoreDict[entry.Key] = Convert.ToInt32(value); 44 | } 45 | catch (Exception) 46 | { 47 | continue; 48 | } 49 | } 50 | if ((int)mapStats["MapStartYear"] == 0) 51 | { 52 | return statsDictionary; 53 | } 54 | 55 | statsDictionary["Map"] = mapStats; 56 | 57 | LinkedList> playerStats = new LinkedList>(); 58 | for (var i = 0; i < MAX_PLAYERS; i++) 59 | { 60 | Dictionary currentPlayer = new Dictionary(); 61 | currentPlayer["PlayerNumber"] = i + 1; 62 | foreach (KeyValuePair> entry in playerData["Player"]) 63 | { 64 | int addr = Convert.ToInt32(entry.Value["address"], 16) + Convert.ToInt32(entry.Value["offset"], 16) * i; 65 | string type = entry.Value["type"]; 66 | 67 | object value = Reader.ReadType(addr, type); 68 | currentPlayer[entry.Key] = value; 69 | 70 | if (entry.Key == "Active" && value.ToString().ToLowerInvariant() == "false") 71 | { 72 | break; 73 | } 74 | 75 | try 76 | { 77 | scoreDict[entry.Key] = Convert.ToInt32(value); 78 | } 79 | catch (Exception) 80 | { 81 | continue; 82 | } 83 | } 84 | if (currentPlayer["Active"].ToString().ToLowerInvariant() == "false") 85 | { 86 | continue; 87 | } 88 | 89 | foreach (var player in endingPlayerStats) 90 | { 91 | if ((int)player["PlayerNumber"] == (int)currentPlayer["PlayerNumber"]) 92 | { 93 | currentPlayer["EconomyScore"] = player["EconomyScore"]; 94 | currentPlayer["MilitaryScore"] = player["MilitaryScore"]; 95 | currentPlayer["Score"] = player["Score"]; 96 | currentPlayer["LargestWeightedArmy"] = player["LargestWeightedArmy"]; 97 | currentPlayer["LargestArmy"] = player["LargestArmy"]; 98 | } 99 | } 100 | 101 | currentPlayer["VanillaScore"] = 102 | GreatestLord.CalculateScore(scoreDict["Gold"], scoreDict["LordKills"], scoreDict["WeightedTroopsKilled"], 103 | scoreDict["WeightedBuildingsDestroyed"], scoreDict["MapStartYear"], scoreDict["MapStartMonth"], 104 | scoreDict["MapEndYear"], scoreDict["MapEndMonth"]); 105 | playerStats.AddLast(currentPlayer); 106 | } 107 | statsDictionary["PlayerStatistics"] = playerStats; 108 | return statsDictionary; 109 | } 110 | 111 | public static long CalculateScore 112 | (int gold, int lordKills, int weightedKills, int weightedBuildings, int startYear, int startMonth, int endYear, int endMonth) 113 | { 114 | const long multiplier = 0x66666667; 115 | long goldBonus = ((gold * multiplier) >> 32) / 4; 116 | long score = goldBonus + weightedKills + weightedBuildings * 100; 117 | score = score + (score * lordKills) / 4; 118 | 119 | int dateBonus = (endYear - startYear) * 12; 120 | dateBonus -= startMonth; 121 | dateBonus += endMonth; 122 | 123 | if (dateBonus < 1) 124 | { 125 | dateBonus = 1; 126 | } 127 | int bonusDivider = 200 + dateBonus; 128 | 129 | score = score * 200; 130 | score = score / bonusDivider; 131 | return score; 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /SHCLiveStatReader/memory/player.json: -------------------------------------------------------------------------------- 1 | { 2 | "Active": { 3 | "address": "0x24BA557", 4 | "type": "boolean", 5 | "category": "other", 6 | "offset": "0x1" 7 | }, 8 | "Name": { 9 | "address": "0x24BA286", 10 | "type": "string", 11 | "category": "other", 12 | "offset": "0x5A" 13 | }, 14 | "Team": { 15 | "address": "0x24BAAB6", 16 | "type": "byte", 17 | "category": "other", 18 | "offset": "0x1" 19 | }, 20 | "MapName": { 21 | "address": "0x24B649C", 22 | "type": "string", 23 | "category": "other", 24 | "offset": "0" 25 | }, 26 | "TotalGold": { 27 | "address": "0x24BA564", 28 | "type": "integer", 29 | "category": "other", 30 | "offset": "0x4" 31 | }, 32 | "Gold": { 33 | "address": "0x11F2938", 34 | "type": "integer", 35 | "category": "other", 36 | "offset": "0x39F4" 37 | }, 38 | "TaxesSetting": { 39 | "address": "0x11F45B4", 40 | "type": "integer", 41 | "category": "other", 42 | "offset": "0x39F4" 43 | }, 44 | "Units": { 45 | "address": "0x11F27E4", 46 | "type": "integer", 47 | "category": "military", 48 | "offset": "0x39F4" 49 | }, 50 | "WeightedUnits": { 51 | "address": "0x11F5D14", 52 | "type": "integer", 53 | "category": "military", 54 | "offset": "0x39F4" 55 | }, 56 | "SiegeEngineCount": { 57 | "address": "0x11F27E8", 58 | "type": "integer", 59 | "category": "military", 60 | "offset": "0x39F4" 61 | }, 62 | "CurrentEconomyBuildings": { 63 | "address": "0x11F5AB0", 64 | "type": "integer", 65 | "category": "military", 66 | "offset": "0x4" 67 | }, 68 | "CurrentTotalBuildings": { 69 | "address": "0x129377C", 70 | "type": "integer", 71 | "category": "military", 72 | "offset": "0x4" 73 | }, 74 | "FarmCount": { 75 | "address": "0x11F5D54", 76 | "type": "integer", 77 | "category": "other", 78 | "offset": "0x39F4" 79 | }, 80 | "InnCount": { 81 | "address": "0x11F46FC", 82 | "type": "integer", 83 | "category": "other", 84 | "offset": "0x39F4" 85 | }, 86 | "PitchRigCount": { 87 | "address": "0x11F5DBC", 88 | "type": "integer", 89 | "category": "other", 90 | "offset": "0x39F4" 91 | }, 92 | "IronMineCount": { 93 | "address": "0x11F5DB8", 94 | "type": "integer", 95 | "category": "other", 96 | "offset": "0x39F4" 97 | }, 98 | "StoneQuarryCount": { 99 | "address": "0x11F5DC0", 100 | "type": "integer", 101 | "category": "other", 102 | "offset": "0x39F4" 103 | }, 104 | "BakeryCount": { 105 | "address": "0x11F4FBC", 106 | "type": "integer", 107 | "category": "other", 108 | "offset": "0x39F4" 109 | }, 110 | "WorkingWoodcutterCount": { 111 | "address": "0x11F4F48", 112 | "type": "integer", 113 | "category": "other", 114 | "offset": "0x39F4" 115 | }, 116 | "TotalFood": { 117 | "address": "0x11f44C8", 118 | "type": "integer", 119 | "category": "other", 120 | "offset": "0x39F4" 121 | }, 122 | "Popularity": { 123 | "address": "0x11F2870", 124 | "type": "word", 125 | "category": "other", 126 | "offset": "0x39F4" 127 | }, 128 | "Population": { 129 | "address": "0x11F45AC", 130 | "type": "word", 131 | "category": "other", 132 | "offset": "0x39F4" 133 | }, 134 | "Housing": { 135 | "address": "0x11F24A0", 136 | "type": "word", 137 | "category": "other", 138 | "offset": "0x39F4" 139 | }, 140 | "Wood": { 141 | "address": "0x11F2904", 142 | "type": "integer", 143 | "category": "resource", 144 | "offset": "0x39F4" 145 | }, 146 | "Stone": { 147 | "address": "0x11F290C", 148 | "type": "integer", 149 | "category": "resource", 150 | "offset": "0x39F4" 151 | }, 152 | "Iron": { 153 | "address": "0x11F2914", 154 | "type": "integer", 155 | "category": "resource", 156 | "offset": "0x39F4" 157 | }, 158 | "Pitch": { 159 | "address": "0x11F2918", 160 | "type": "integer", 161 | "category": "resource", 162 | "offset": "0x39F4" 163 | }, 164 | "Apples": { 165 | "address": "0x11F2930", 166 | "type": "integer", 167 | "category": "resource", 168 | "offset": "0x39F4" 169 | }, 170 | "Meat": { 171 | "address": "0x11F292C", 172 | "type": "integer", 173 | "category": "resource", 174 | "offset": "0x39F4" 175 | }, 176 | "Cheese": { 177 | "address": "0x11F2928", 178 | "type": "integer", 179 | "category": "resource", 180 | "offset": "0x39F4" 181 | }, 182 | "Bread": { 183 | "address": "0x11F2924", 184 | "type": "integer", 185 | "category": "resource", 186 | "offset": "0x39F4" 187 | }, 188 | "MetalArmor": { 189 | "address": "0x11F295C", 190 | "type": "integer", 191 | "category": "resource", 192 | "offset": "0x39F4" 193 | }, 194 | "LeatherArmor": { 195 | "address": "0x11F2958", 196 | "type": "integer", 197 | "category": "resource", 198 | "offset": "0x39F4" 199 | }, 200 | "Swords": { 201 | "address": "0x11F2954", 202 | "type": "integer", 203 | "category": "resource", 204 | "offset": "0x39F4" 205 | }, 206 | "Maces": { 207 | "address": "0x11F2950", 208 | "type": "integer", 209 | "category": "resource", 210 | "offset": "0x39F4" 211 | }, 212 | "Pikes": { 213 | "address": "0x11F294c", 214 | "type": "integer", 215 | "category": "resource", 216 | "offset": "0x39F4" 217 | }, 218 | "Spears": { 219 | "address": "0x11F2948", 220 | "type": "integer", 221 | "category": "resource", 222 | "offset": "0x39F4" 223 | }, 224 | "Crossbows": { 225 | "address": "0x11F2944", 226 | "type": "integer", 227 | "category": "resource", 228 | "offset": "0x39F4" 229 | }, 230 | "Bows": { 231 | "address": "0x11F2940", 232 | "type": "integer", 233 | "category": "resource", 234 | "offset": "0x39F4" 235 | }, 236 | "Flour": { 237 | "address": "0x11F293C", 238 | "type": "integer", 239 | "category": "resource", 240 | "offset": "0x39F4" 241 | }, 242 | "Hops": { 243 | "address": "0x11F2908", 244 | "type": "integer", 245 | "category": "resource", 246 | "offset": "0x39F4" 247 | }, 248 | "Ale": { 249 | "address": "0x11F2934", 250 | "type": "integer", 251 | "category": "resource", 252 | "offset": "0x39F4" 253 | }, 254 | "Wheat": { 255 | "address": "0x11F2920", 256 | "type": "integer", 257 | "category": "resource", 258 | "offset": "0x39F4" 259 | }, 260 | "WeightedLosses": { 261 | "address": "0x11F46CC", 262 | "type": "integer", 263 | "category": "military", 264 | "offset": "0x39F4" 265 | }, 266 | "WeightedTroopsKilled": { 267 | "address": "0x24BA840", 268 | "type": "integer", 269 | "category": "military", 270 | "offset": "0x4" 271 | }, 272 | "WeightedBuildingsDestroyed": { 273 | "address": "0x24BA864", 274 | "type": "integer", 275 | "category": "military", 276 | "offset": "0x4" 277 | }, 278 | "GoodsSent": { 279 | "address": "0x24BA8D0", 280 | "type": "integer", 281 | "category": "resource", 282 | "offset": "0x4" 283 | }, 284 | "GoodsReceived": { 285 | "address": "0x24BA8AC", 286 | "type": "integer", 287 | "category": "resource", 288 | "offset": "0x4" 289 | }, 290 | "AleCoveragePercent": { 291 | "address": "011F46D0", 292 | "type": "integer", 293 | "category": "popularity", 294 | "offset": "0x39F4" 295 | } 296 | } -------------------------------------------------------------------------------- /SHCLiveStatReader/Player.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System; 3 | using System.Linq; 4 | using System.Collections.Generic; 5 | using System.IO; 6 | using static SHC.Constants; 7 | 8 | namespace SHC 9 | { 10 | class Player 11 | { 12 | readonly static Dictionary> names; 13 | readonly static Dictionary> weights; 14 | readonly static Dictionary> buildingData; 15 | 16 | static Player() { 17 | buildingData = JsonConvert.DeserializeObject>>(File.ReadAllText("memory/buildings.json")); 18 | weights = JsonConvert.DeserializeObject>>(File.ReadAllText("memory/weights.json")); 19 | names = JsonConvert.DeserializeObject>>(File.ReadAllText("memory/names.json")); 20 | } 21 | 22 | private Dictionary mostRecentStats = new Dictionary() 23 | { 24 | {"PlayerNumber", 0 }, 25 | {"Active", true }, 26 | {"Alive", true }, 27 | {"LargestWeightedArmy", 0 }, 28 | {"LargestArmy", 0 }, 29 | {"EconomyScore", 0 }, 30 | {"MilitaryScore", 0 }, 31 | {"Score", 0 } 32 | }; 33 | 34 | private int Number { get; } 35 | 36 | private bool IsAlive 37 | { 38 | get 39 | { 40 | return Reader.TestZero(PLAYER_DEATH_TIME + 4 * (Number - 1), 4); 41 | } 42 | } 43 | 44 | public static Dictionary> Data { set; get; } 45 | 46 | public Player(int number) 47 | { 48 | this.Number = number; 49 | } 50 | 51 | public void ResetWeightedStats() 52 | { 53 | mostRecentStats["LargestWeightedArmy"] = 0; 54 | mostRecentStats["LargestArmy"] = 0; 55 | mostRecentStats["WeightedUnits"] = 0; 56 | } 57 | 58 | public Dictionary Update() 59 | { 60 | int gold = 0; 61 | int weightedUnits = 0; 62 | int resources = 0; 63 | 64 | mostRecentStats["PlayerNumber"] = this.Number; 65 | bool alive = this.IsAlive; 66 | mostRecentStats["Alive"] = alive; 67 | 68 | if (!alive) 69 | { 70 | return mostRecentStats; 71 | } 72 | 73 | foreach (KeyValuePair> entry in Data) 74 | { 75 | int baseAddress = Convert.ToInt32((string)entry.Value["address"], 16); 76 | int offset = Convert.ToInt32((string)entry.Value["offset"], 16) * (Number - 1); 77 | int addr = baseAddress + offset; 78 | 79 | if (entry.Key == "Gold") 80 | { 81 | gold += Reader.ReadInt(addr, 4); 82 | mostRecentStats["Gold"] = gold; 83 | continue; 84 | } else if (entry.Key == "WeightedUnits") 85 | { 86 | weightedUnits += Reader.ReadInt(addr, 4); 87 | mostRecentStats["WeightedUnits"] = weightedUnits; 88 | mostRecentStats["LargestWeightedArmy"] = Math.Max(Convert.ToInt32(mostRecentStats["LargestWeightedArmy"]), weightedUnits); 89 | continue; 90 | } 91 | 92 | string type = (string)entry.Value["type"]; 93 | object value = Reader.ReadType(addr, type); 94 | 95 | if (entry.Key == "Units") 96 | { 97 | mostRecentStats["LargestArmy"] = Math.Max(Convert.ToInt32(mostRecentStats["LargestArmy"]), Convert.ToInt32(value)); 98 | } 99 | 100 | if ((string)entry.Value["category"] == "resource") 101 | { 102 | try 103 | { 104 | resources += Convert.ToInt32(value) * weights["Resources"][entry.Key]; 105 | } 106 | catch (KeyNotFoundException) 107 | { 108 | resources += Convert.ToInt32(value); 109 | } 110 | } 111 | mostRecentStats[entry.Key] = value; 112 | } 113 | mostRecentStats["Resources"] = resources; 114 | return mostRecentStats; 115 | } 116 | 117 | 118 | public static void Update(LinkedList> gameData) 119 | { 120 | UpdateBuildings(gameData); 121 | UpdateScore(gameData); 122 | } 123 | 124 | public static void UpdateBuildings(LinkedList> gameData) 125 | { 126 | int addr = Convert.ToInt32(buildingData["Buildings"]["address"], 16); 127 | int offset = Convert.ToInt32(buildingData["Buildings"]["offset"], 16); 128 | int ownerOffset = Convert.ToInt32(buildingData["Buildings"]["owneroffset"], 16); 129 | int workersNeededOffset = Convert.ToInt32(buildingData["Buildings"]["workersneededoffset"], 16); 130 | int workersWorkingOffset = Convert.ToInt32(buildingData["Buildings"]["workersworkingoffset"], 16); 131 | int workersMissingOffset = Convert.ToInt32(buildingData["Buildings"]["workersmissingoffset"], 16); 132 | int snoozedOffset = Convert.ToInt32(buildingData["Buildings"]["snoozedoffset"], 16); 133 | int totalBuildings = Reader.ReadInt(Convert.ToInt32(buildingData["Buildings"]["total"], 16), 4); 134 | 135 | for (var i = 0; i < gameData.Count; i++) 136 | { 137 | gameData.ElementAt(i)["Buildings"] = new Dictionary(); 138 | gameData.ElementAt(i)["CurrentWeightedBuildings"] = 0; 139 | gameData.ElementAt(i)["WeightedActiveBuildings"] = 0; 140 | gameData.ElementAt(i)["CurrentWorkersNeeded"] = 0; 141 | gameData.ElementAt(i)["CurrentWorkersWorking"] = 0; 142 | gameData.ElementAt(i)["CurrentWorkersMissing"] = 0; 143 | } 144 | byte[] buildingArray = Reader.ReadBytes(addr, 0x490 * 10000); 145 | int count = 0; 146 | for (var i = 0; i < totalBuildings;) 147 | { 148 | int buildingID = BitConverter.ToInt32(buildingArray, count * offset); 149 | int owner = buildingArray[count * offset + ownerOffset]; 150 | int workers = buildingArray[count * offset + workersNeededOffset]; 151 | int workersWorking = buildingArray[count * offset + workersWorkingOffset]; 152 | int workersMissing = buildingArray[count * offset + workersMissingOffset]; 153 | bool snoozed = buildingArray[count * offset + snoozedOffset] == 0; 154 | 155 | if (buildingID != 0 || count > 2 * i) 156 | { 157 | if (owner > 0 && owner < 9) 158 | { 159 | int playerPos = 0; 160 | while (playerPos < gameData.Count && Convert.ToInt32(gameData.ElementAt(playerPos)["PlayerNumber"]) != owner) 161 | { 162 | playerPos++; 163 | } 164 | if (playerPos >= gameData.Count || buildingID == 0) 165 | { 166 | i++; 167 | count++; 168 | continue; 169 | } 170 | if (!snoozed) 171 | { 172 | gameData.ElementAt(playerPos)["WeightedActiveBuildings"] = Convert.ToInt32(gameData.ElementAt(playerPos)["WeightedActiveBuildings"]) + weights["Buildings"][buildingID.ToString()]; 173 | } 174 | gameData.ElementAt(playerPos)["CurrentWeightedBuildings"] = Convert.ToInt32(gameData.ElementAt(playerPos)["CurrentWeightedBuildings"]) + weights["Buildings"][buildingID.ToString()]; 175 | gameData.ElementAt(playerPos)["CurrentWorkersNeeded"] = Convert.ToInt32(gameData.ElementAt(playerPos)["CurrentWorkersNeeded"]) + workers; 176 | gameData.ElementAt(playerPos)["CurrentWorkersWorking"] = Convert.ToInt32(gameData.ElementAt(playerPos)["CurrentWorkersWorking"]) + workersWorking; 177 | gameData.ElementAt(playerPos)["CurrentWorkersMissing"] = Convert.ToInt32(gameData.ElementAt(playerPos)["CurrentWorkersMissing"]) + workersMissing; 178 | 179 | if (!names["Buildings"].ContainsKey(buildingID.ToString())) 180 | { 181 | if (!((Dictionary)gameData.ElementAt(playerPos)["Buildings"]).ContainsKey(buildingID.ToString())) 182 | { 183 | ((Dictionary)gameData.ElementAt(playerPos)["Buildings"])["Unknown"] = 0; 184 | } 185 | ((Dictionary)gameData.ElementAt(playerPos)["Buildings"])["Unknown"] = ((Dictionary)gameData.ElementAt(playerPos)["Buildings"])["Unknown"] + 1; 186 | } 187 | else 188 | { 189 | string buildingName = names["Buildings"][buildingID.ToString()]; 190 | if (!((Dictionary)gameData.ElementAt(playerPos)["Buildings"]).ContainsKey(buildingName)) 191 | { 192 | ((Dictionary)gameData.ElementAt(playerPos)["Buildings"])[buildingName] = 0; 193 | } 194 | ((Dictionary)gameData.ElementAt(playerPos)["Buildings"])[buildingName] = ((Dictionary)gameData.ElementAt(playerPos)["Buildings"])[buildingName] + 1; 195 | } 196 | } 197 | i++; 198 | } 199 | count++; 200 | } 201 | } 202 | 203 | private static void UpdateScore(LinkedList> gameData) 204 | { 205 | for (var i = 0; i < gameData.Count; i++) 206 | { 207 | if (!Convert.ToBoolean(gameData.ElementAt(i)["Alive"])) 208 | { 209 | continue; 210 | } 211 | int economyScore = 0; 212 | int militaryScore = 0; 213 | militaryScore += Convert.ToInt32(gameData.ElementAt(i)["WeightedTroopsKilled"]); 214 | militaryScore += 5 * Convert.ToInt32(gameData.ElementAt(i)["WeightedBuildingsDestroyed"]); 215 | economyScore += (Convert.ToInt32(gameData.ElementAt(i)["Resources"]) + Convert.ToInt32(gameData.ElementAt(i)["GoodsSent"]) + Convert.ToInt32(gameData.ElementAt(i)["Gold"])) / 10; 216 | militaryScore += Convert.ToInt32(gameData.ElementAt(i)["WeightedUnits"]); 217 | militaryScore += 5 * Convert.ToInt32(gameData.ElementAt(i)["CurrentWeightedBuildings"]); 218 | militaryScore += Convert.ToInt32(gameData.ElementAt(i)["Population"]); 219 | gameData.ElementAt(i)["EconomyScore"] = economyScore; 220 | gameData.ElementAt(i)["MilitaryScore"] = militaryScore; 221 | gameData.ElementAt(i)["Score"] = economyScore + militaryScore; 222 | } 223 | } 224 | } 225 | } 226 | --------------------------------------------------------------------------------