Skip to content

Commit 25046b7

Browse files
committed
Add support for player 2. Ensure messages are exchanged in structured objects
1 parent febf473 commit 25046b7

File tree

10 files changed

+387
-159
lines changed

10 files changed

+387
-159
lines changed

README.md

Lines changed: 3 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,5 @@
11
# Sinden Companion
22

3-
## TODO
4-
- More game offsets
5-
- Pass logger to config
6-
73
## Getting started
84
- Download the most recent release from the release page
95
- Unzip the content in the same folder as Lightgun.exe
@@ -70,8 +66,9 @@ game_profiles:
7066
title: "My Game"
7167
# Optional : Set a memory reader to read the game's memory and change profile dynamically
7268
memscan:
73-
# Pointer path to use
74-
code: mygame.exe+0x0018AC00,0x364,0x4
69+
# Pointer paths to use. First element maps to Player 1, second to Player 2.
70+
paths:
71+
- mygame.exe+0x0018AC00,0x364,0x4
7572
# Type of value to read (byte, short, int, uint)
7673
type: "byte"
7774
# Matching values
@@ -84,47 +81,6 @@ game_profiles:
8481
0x7: "Single" # Sniper
8582
```
8683
87-
## Simple configuration file for testing
88-
89-
```yaml
90-
global:
91-
recoil_on_switch: true
92-
debug: true
93-
recoil_profiles:
94-
- name: "Single"
95-
automatic: false
96-
pulse_length: 0
97-
delay_between_pulses: 0
98-
offscreen: false
99-
strength: 50
100-
- name: "Auto_Fast"
101-
automatic: true
102-
pulse_length: 40
103-
delay_between_pulses: 3
104-
offscreen: false
105-
strength: 3
106-
game_profiles:
107-
- name: "Notepad"
108-
profile: "Auto_Fast"
109-
match:
110-
exe: "Notepad.exe"
111-
- name: "Sinden"
112-
profile: "Single"
113-
match:
114-
title: "SindenLightgun"
115-
- name: "Assault Cube"
116-
profile: "Single"
117-
match:
118-
exe: "ac_client.exe"
119-
memscan:
120-
code: ac_client.exe+0x0018AC00,0x364,0x4
121-
size: 1
122-
match:
123-
0x0: "Single" # Knife
124-
0x1: "Single" # Glock
125-
0x6: "Auto_Fast" # AR
126-
0x7: "Single" # Sniper
127-
```
12884
12985
Launching the application and switching between Notepad and Lightgun.exe should produce recoil events.
13086

SindenCompanion/AppForm.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ public partial class AppForm : Form
1717
private readonly Config _conf;
1818
public readonly RichTextBox WpfRichTextBox;
1919

20-
private Action<RecoilProfile> _callback;
20+
private Action<int, RecoilProfile> _callback;
2121

2222
private bool _userRequestedClose;
2323

@@ -50,7 +50,7 @@ public AppForm(Config conf)
5050
WpfRichTextBox = wpfRichTextBox;
5151
}
5252

53-
public void SetCallback(Action<RecoilProfile> callback)
53+
public void SetCallback(Action<int, RecoilProfile> callback)
5454
{
5555
_callback = callback;
5656
}
@@ -115,7 +115,7 @@ private void NotificationIconMenu_Opened(object sender, EventArgs e)
115115
foreach (var profile in _conf.RecoilProfiles)
116116
{
117117
var item = new ToolStripMenuItem(profile.Name);
118-
item.Click += (s, a) => { _callback(profile); };
118+
item.Click += (s, a) => { _callback(-1,profile); };
119119
changeProfileMenuItem.DropDownItems.Add(item);
120120
}
121121
}

SindenCompanion/Config.cs

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -87,14 +87,12 @@ public static Config GetInstance()
8787
_fw.Filter = "config.yaml";
8888
_fw.Changed += new FileSystemEventHandler((e, a) =>
8989
{
90-
if (Logger != null)
91-
{
92-
Logger.Information("Detected changes to config file.");
93-
}
90+
if (Logger != null) Logger.Information("Detected changes to config file.");
9491
_instance = null;
9592
});
9693
_fw.EnableRaisingEvents = true;
9794
}
95+
9896
if (Logger != null)
9997
Logger.Information("Loading configuration.");
10098

@@ -110,7 +108,7 @@ public static Config GetInstance()
110108
var result = validator.Validate(_instance);
111109
if (!result.IsValid)
112110
{
113-
string reason = "";
111+
var reason = "";
114112
foreach (var failure in result.Errors)
115113
reason += "Property " + failure.PropertyName + " failed validation. Error was: " +
116114
failure.ErrorMessage + "\n";
@@ -167,7 +165,7 @@ public bool Matches(ForegroundProcess fp)
167165

168166
if ($"{fp.ProcessName}.exe" == Match.Exe) return true;
169167

170-
if (Match.Title == fp.WindowTitle) return true;
168+
if (!string.IsNullOrEmpty(Match.Title) && Match.Title == fp.WindowTitle) return true;
171169

172170
return false;
173171
}
@@ -199,8 +197,8 @@ public GameProfileValidator(Config parent)
199197
.WithMessage(x => $"GameProfile.{x.Name}.Match.Exe must be set when title is empty.");
200198
RuleFor(x => x.Match.Title).NotNull().NotEmpty().When(x => x.Match != null && x.Match.Exe == null)
201199
.WithMessage(x => $"GameProfile.{x.Name}.Match.Title must be set when exe is empty.");
202-
RuleFor(x => x.Memscan.Code).NotNull().When(x => x.Memscan != null)
203-
.WithMessage("A valid pointer path must be provided.");
200+
RuleFor(x => x.Memscan.Paths).NotNull().NotEmpty().Must(x => x.Length <= 2).When(x => x.Memscan != null)
201+
.WithMessage("At least one pointer path must be provided. At most two pointer paths can be provided.");
204202
RuleFor(x => x.Memscan.Type).NotNull().Must(ValidScanType).When(x => x.Memscan != null).WithMessage(x =>
205203
$"GameProfile.{x.Name}.Memscan.Type: A valid type must be provided ({string.Join(", ", MemScan.TypeMap.Keys)})");
206204
RuleFor(x => x.Memscan.Match).NotNull().NotEmpty().When(x => x.Memscan != null)
@@ -233,7 +231,7 @@ public class MemScan
233231
//{"string", ScanType.String},
234232
};
235233

236-
public string Code { get; set; }
234+
public string[] Paths { get; set; }
237235
public string Type { get; set; }
238236

239237
public Dictionary<int, string> Match { get; set; }

SindenCompanion/Program.cs

Lines changed: 98 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ public class App : IDisposable
1717
private readonly ServerInterface _server;
1818
private bool _clientReady = false;
1919
private Config _conf;
20-
private RecoilProfile _currentProfile;
20+
private List<RecoilProfile> _currentProfile = new List<RecoilProfile> { null, null };
2121

2222
public App(ILogger logger)
2323
{
@@ -33,11 +33,20 @@ public void Dispose()
3333
foreach (var memlib in _memReaders.Values) memlib.CloseProcess();
3434
}
3535

36-
public void ChangeProfile(RecoilProfile rp)
36+
public void ChangeProfile(int playerIndex, RecoilProfile rp)
3737
{
38-
_server.SendMessage(MessageBuilder.Build("profile", rp).AsMessage());
39-
if (_conf.Global.RecoilOnSwitch) _server.SendMessage(MessageBuilder.Build("recoil", null).AsMessage());
40-
_currentProfile = rp;
38+
var rpw = new RecoilProfileWrapper()
39+
{
40+
RecoilProfile = rp,
41+
Player = playerIndex
42+
};
43+
_server.SendMessage(MessageBuilder.Build("profile", rpw).AsMessage());
44+
if (_conf.Global.RecoilOnSwitch)
45+
_server.SendMessage(MessageBuilder.Build("recoil", playerIndex).AsMessage());
46+
if (playerIndex == -1)
47+
_currentProfile = new List<RecoilProfile> { rp, rp };
48+
else
49+
_currentProfile[playerIndex] = rp;
4150
}
4251

4352
public void InjectionNotification()
@@ -106,54 +115,75 @@ public void WindowEventHandler(IntPtr hWinEventHook, uint eventType, IntPtr hwnd
106115
return;
107116
}
108117

109-
dynamic value;
110-
string profName = null;
111-
switch (matchedGp.Memscan.Type)
118+
var idx = 0;
119+
foreach (var path in matchedGp.Memscan.Paths)
112120
{
113-
case "byte":
114-
value = memlib.ReadByte(matchedGp.Memscan.Code);
115-
matchedGp.Memscan.Match.TryGetValue(value, out profName);
116-
break;
117-
case "short":
118-
value = memlib.Read2Byte(matchedGp.Memscan.Code);
119-
matchedGp.Memscan.Match.TryGetValue(value, out profName);
120-
break;
121-
case "int":
122-
value = memlib.ReadInt(matchedGp.Memscan.Code);
123-
matchedGp.Memscan.Match.TryGetValue(value, out profName);
124-
break;
125-
case "uint":
126-
value = memlib.ReadUInt(matchedGp.Memscan.Code);
127-
matchedGp.Memscan.Match.TryGetValue(value, out profName);
128-
break;
129-
default:
130-
_logger.Error("Unsupported memory scan type: {@Type}", matchedGp.Memscan.Type);
131-
return;
132-
}
121+
dynamic value;
122+
string profName = null;
123+
try
124+
{
125+
switch (matchedGp.Memscan.Type)
126+
{
127+
case "byte":
128+
value = memlib.ReadByte(path);
129+
matchedGp.Memscan.Match.TryGetValue(value, out profName);
130+
break;
131+
case "short":
132+
value = memlib.Read2Byte(path);
133+
matchedGp.Memscan.Match.TryGetValue(value, out profName);
134+
break;
135+
case "int":
136+
value = memlib.ReadInt(path);
137+
matchedGp.Memscan.Match.TryGetValue(value, out profName);
138+
break;
139+
case "uint":
140+
value = memlib.ReadUInt(path);
141+
matchedGp.Memscan.Match.TryGetValue(value, out profName);
142+
break;
143+
default:
144+
_logger.Error("Unsupported memory scan type: {@Type}",
145+
matchedGp.Memscan.Type);
146+
return;
147+
}
148+
}
149+
catch (Exception e)
150+
{
151+
_logger.Error(e,
152+
"[P{@Index}] Exception while reading memory. The application may still be initializing or the pointer path is incorrect: {@Exception}",
153+
idx + 1, e);
154+
continue;
155+
}
133156

134-
if (!string.IsNullOrEmpty(profName))
135-
{
136-
matchedRp = _conf.RecoilProfiles.FirstOrDefault(p => p.Name == profName);
137-
if (matchedRp == null)
157+
_logger.Debug("[P{@Index}] Memory scan result: {@Value} -> {@Profile}", idx + 1, value,
158+
profName);
159+
if (!string.IsNullOrEmpty(profName))
160+
{
161+
matchedRp = _conf.RecoilProfiles.FirstOrDefault(p => p.Name == profName);
162+
if (matchedRp == null)
163+
{
164+
_logger.Error(
165+
"[{@Game}][MEM][Player {@Index}] {@Value} -> {@Profile} not found. Check your configuration.",
166+
matchedGp.Name, idx + 1, value, profName);
167+
continue;
168+
}
169+
}
170+
else
138171
{
139172
_logger.Error(
140-
"[{@Game}][MEM] {@Value} -> {@Profile} not found. Check your configuration.",
141-
matchedGp.Name, value, profName);
173+
"[{@Game}][MEM][Player {@Index}] {@Value} -> No profile found. Check your configuration.",
174+
matchedGp.Name, idx + 1, value);
142175
continue;
143176
}
144-
}
145-
else
146-
{
147-
_logger.Error("[{@Game}][MEM] {@Value} -> No profile found. Check your configuration.",
148-
matchedGp.Name, value);
149-
continue;
150-
}
151177

152-
if (matchedRp != _currentProfile)
153-
{
154-
_logger.Information("[{@Game}][MEM] {@Value} -> {@Profile}", matchedGp.Name, value,
155-
matchedRp.Name);
156-
ChangeProfile(matchedRp);
178+
if (matchedRp != _currentProfile[idx])
179+
{
180+
_logger.Information("[{@Game}][MEM][Player {@Index}] {@Value} -> {@Profile}",
181+
matchedGp.Name, idx + 1, value,
182+
matchedRp.Name);
183+
ChangeProfile(idx, matchedRp);
184+
}
185+
186+
idx++;
157187
}
158188

159189

@@ -171,10 +201,10 @@ public void WindowEventHandler(IntPtr hWinEventHook, uint eventType, IntPtr hwnd
171201
return;
172202
}
173203

174-
if (matchedRp != _currentProfile)
204+
if (matchedRp != _currentProfile[0])
175205
{
176206
_logger.Information("[{@Game}] {@Profile}", matchedGp.Name, matchedRp.Name);
177-
ChangeProfile(matchedRp);
207+
ChangeProfile(-1, matchedRp);
178208
}
179209
}
180210
}
@@ -192,22 +222,28 @@ select MessageBuilder.FromMessage(msg))
192222
_clientReady = true;
193223
break;
194224
case "recoilack":
195-
if (!(bool)e.Payload)
196-
_logger.Error($"Failed to recoil");
225+
var recoilResp = RecoilResponse.FromString(e.Payload.ToString());
226+
if (!recoilResp.Success)
227+
_logger.Error("Failed to recoil: {@Reason}", recoilResp.Reason);
197228
else
198-
_logger.Information($"Recoil ACK - Success");
229+
_logger.Information($"Recoil success");
199230
break;
200231
case "profileack":
201-
var suc = (bool)e.Payload;
202-
if (!suc)
203-
{
204-
_logger.Error($"Failed to apply profile, Sinden may still be initializing - will retry");
205-
_currentProfile = null;
206-
}
232+
var resp = RecoilProfileResponse.FromString(e.Payload.ToString());
233+
234+
if (!resp.Success)
235+
switch (resp.Reason)
236+
{
237+
case "No lightgun at requested index.":
238+
_logger.Error(
239+
$"Failed to apply profile -> No lightgun detected at requested player index");
240+
break;
241+
default:
242+
_logger.Error("Failed to apply profile: {@Reason}", resp.Reason);
243+
break;
244+
}
207245
else
208-
{
209-
_logger.Information("Successfully applied profile. {@Profile}", _currentProfile);
210-
}
246+
_logger.Information("Successfully applied profiles. {@Profile}", _currentProfile);
211247

212248
break;
213249
}
@@ -223,11 +259,7 @@ internal class Program
223259
[DllImport("user32.dll")]
224260
private static extern IntPtr SetWinEventHook(uint eventMin, uint eventMax, IntPtr hmodWinEventProc,
225261
WinEventDelegate lpfnWinEventProc, uint idProcess, uint idThread, uint dwFlags);
226-
[DllImport("kernel32.dll", SetLastError = true)]
227-
[return: MarshalAs(UnmanagedType.Bool)]
228-
static extern bool AllocConsole();
229-
[System.Runtime.InteropServices.DllImport("kernel32.dll")]
230-
private static extern bool AttachConsole(int dwProcessId);
262+
231263

232264
[STAThread]
233265
private static void Main(string[] args)
@@ -236,13 +268,13 @@ private static void Main(string[] args)
236268
try
237269
{
238270
conf = Config.GetInstance();
239-
240271
}
241272
catch (Exception e)
242273
{
243274
// Crash outright if we don't have a working config
244275
throw new InvalidOperationException($"Failed to read configuration: {e}");
245276
}
277+
246278
var mainForm = new AppForm(conf);
247279
var logger = Logger.CreateDesktopLogger(conf.Global.Debug, mainForm.WpfRichTextBox);
248280
Config.Logger = logger;

0 commit comments

Comments
 (0)