├── 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 |
--------------------------------------------------------------------------------