diff --git a/README.md b/README.md index 51d297e..ed120ce 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,9 @@ # RLBot +

+ +

+ ### Short Description Saltie is a bot that uses Neural Networks and Machine Learning to learn how to play the game. It also has tools for training bots and collecting the replays from a lot of distributed computers diff --git a/RLBotInjectorSource/RLBot Injector/Injector.cs b/RLBotInjectorSource/RLBot Injector/Injector.cs index 5286a1c..b8dc179 100644 --- a/RLBotInjectorSource/RLBot Injector/Injector.cs +++ b/RLBotInjectorSource/RLBot Injector/Injector.cs @@ -34,57 +34,136 @@ class Injector [DllImport("kernel32", CharSet = CharSet.Ansi, ExactSpelling = true, SetLastError = true)] private static extern bool CloseHandle(IntPtr hObject); + [DllImport("kernel32", CharSet = CharSet.Ansi, ExactSpelling = true, SetLastError = true)] + private static extern bool GetExitCodeThread(IntPtr hThread, out uint lpExitCode); + private const int PROCESS_ALL_ACCESS = 0x1f0fff; private const int MEM_COMMIT = 0x1000; private const int MEM_RELEASE = 0x8000; private const int PAGE_READWRITE = 0x4; + private const int STILL_ACTIVE = 259; + + private static IntPtr hProcess = IntPtr.Zero; + private static IntPtr allocAddress = IntPtr.Zero; + private static IntPtr libThread = IntPtr.Zero; + private static byte[] dllBytes; + + private static string getLastFunctionError(string functionName, int errorCode = -1) + { + return string.Format("{0} failed with error code {1}", functionName, errorCode == -1 ? Marshal.GetLastWin32Error() : errorCode); + } - public static bool Inject(int pID, string dllLocation) + private static void cleanUp() + { + if (hProcess != IntPtr.Zero) + { + if (allocAddress != IntPtr.Zero) + { + VirtualFreeEx(hProcess, allocAddress, (uint)dllBytes.Length, MEM_RELEASE); + allocAddress = IntPtr.Zero; + } + + CloseHandle(hProcess); + hProcess = IntPtr.Zero; + + if (dllBytes != null) + dllBytes = null; + } + + if (libThread != IntPtr.Zero) + { + CloseHandle(libThread); + libThread = IntPtr.Zero; + } + } + + public static bool Inject(int pID, string dllLocation, ref string error) { if (!File.Exists(dllLocation)) { + error = "The RLBot Dll file could not be found"; return false; } - IntPtr hProcess = OpenProcess(PROCESS_ALL_ACCESS, true, pID); + hProcess = OpenProcess(PROCESS_ALL_ACCESS, true, pID); if (hProcess == IntPtr.Zero) { + error = getLastFunctionError("OpenProcess"); return false; } - byte[] dllBytes = Encoding.ASCII.GetBytes(dllLocation); - IntPtr allocAddress = VirtualAllocEx(hProcess, IntPtr.Zero, (uint)dllBytes.Length, MEM_COMMIT, PAGE_READWRITE); + dllBytes = Encoding.Unicode.GetBytes(dllLocation); + allocAddress = VirtualAllocEx(hProcess, IntPtr.Zero, (uint)dllBytes.Length, MEM_COMMIT, PAGE_READWRITE); if (allocAddress == IntPtr.Zero) { + error = getLastFunctionError("VirtualAllocEx"); + cleanUp(); return false; } IntPtr kernelMod = GetModuleHandle("kernel32.dll"); - IntPtr loadLibAddr = GetProcAddress(kernelMod, "LoadLibraryA"); - if (kernelMod == IntPtr.Zero | loadLibAddr == IntPtr.Zero) + if (kernelMod == IntPtr.Zero) + { + error = getLastFunctionError("GetModuleHandle"); + cleanUp(); + return false; + } + + IntPtr loadLibAddr = GetProcAddress(kernelMod, "LoadLibraryW"); + + if (loadLibAddr == IntPtr.Zero) { + error = getLastFunctionError("GetProcAddress"); + cleanUp(); return false; } - WriteProcessMemory(hProcess, allocAddress, dllBytes, (uint)dllBytes.Length, 0); - IntPtr libThread = CreateRemoteThread(hProcess, IntPtr.Zero, 0, loadLibAddr, allocAddress, 0, IntPtr.Zero); + if (WriteProcessMemory(hProcess, allocAddress, dllBytes, (uint)dllBytes.Length, 0) == false) + { + error = getLastFunctionError("WriteProcessMemory"); + cleanUp(); + return false; + } + + libThread = CreateRemoteThread(hProcess, IntPtr.Zero, 0, loadLibAddr, allocAddress, 0, IntPtr.Zero); if (libThread == IntPtr.Zero) { + error = getLastFunctionError("CreateRemoteThread"); + cleanUp(); return false; } else { WaitForSingleObject(libThread, 5000); - CloseHandle(libThread); - } - VirtualFreeEx(hProcess, allocAddress, (uint)dllBytes.Length, MEM_RELEASE); - CloseHandle(hProcess); + uint exitCode; + + if (GetExitCodeThread(libThread, out exitCode) == false) + { + error = getLastFunctionError("GetExitCodeThread"); + cleanUp(); + return false; + } + + if (exitCode == STILL_ACTIVE) + { + error = "The remote thread has not terminated within 5 seconds"; + cleanUp(); + return false; + } + else if (exitCode == 0) + { + error = "The remote LoadLibraryW call has failed"; + cleanUp(); + return false; + } + } + cleanUp(); return true; } } diff --git a/RLBotInjectorSource/RLBot Injector/MainForm.cs b/RLBotInjectorSource/RLBot Injector/MainForm.cs index 25e04fd..a3957c1 100644 --- a/RLBotInjectorSource/RLBot Injector/MainForm.cs +++ b/RLBotInjectorSource/RLBot Injector/MainForm.cs @@ -29,7 +29,7 @@ public MainForm() private void MainForm_Load(object sender, EventArgs e) { - string[] dllPaths = Directory.GetFiles(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "RLBot*.dll"); + string[] dllPaths = Directory.GetFiles(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "RLBot*Core.dll"); if (dllPaths.Length == 0) { @@ -69,7 +69,7 @@ private void injectorTimer_Tick(object sender, EventArgs e) foreach (ProcessModule module in rlProcess.Modules) { - if (Regex.IsMatch(module.ModuleName, "RLBot.*.dll")) + if (Regex.IsMatch(module.ModuleName, "RLBot.*Core.dll")) { MessageBox.Show("The RLBot Dll has already been injected into Rocket League!\n" + "Injecting it more than once is not possible.", @@ -81,7 +81,9 @@ private void injectorTimer_Tick(object sender, EventArgs e) } } - if (Injector.Inject(rlProcess.Id, dllPath)) + string error = string.Empty; + + if (Injector.Inject(rlProcess.Id, dllPath, ref error)) { statusLabel.Text = "Injection successful!"; statusLabel.Update(); @@ -92,7 +94,8 @@ private void injectorTimer_Tick(object sender, EventArgs e) { statusLabel.Text = "Injection failed!"; statusLabel.Update(); - (new SoundPlayer(Properties.Resources.FailedToInjectTheDll)).PlaySync(); + (new SoundPlayer(Properties.Resources.FailedToInjectTheDll)).Play(); + MessageBox.Show(string.Format("Failed to inject the RLBot Dll: {0}.", error), "Injection failed", MessageBoxButtons.OK, MessageBoxIcon.Error); Environment.Exit((int)ExitCodes.INJECTION_FAILED); } } diff --git a/RLBotInjectorSource/RLBot Injector/Properties/AssemblyInfo.cs b/RLBotInjectorSource/RLBot Injector/Properties/AssemblyInfo.cs index 79b9df3..8dab7ed 100644 --- a/RLBotInjectorSource/RLBot Injector/Properties/AssemblyInfo.cs +++ b/RLBotInjectorSource/RLBot Injector/Properties/AssemblyInfo.cs @@ -10,7 +10,7 @@ [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("ccman32")] [assembly: AssemblyProduct("RLBot Injector")] -[assembly: AssemblyCopyright("Copyright © 2017 ccman32")] +[assembly: AssemblyCopyright("Copyright © 2018 ccman32")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] diff --git a/RLBot_Core.dll b/RLBot_Core.dll index 3f165f3..aa30754 100644 Binary files a/RLBot_Core.dll and b/RLBot_Core.dll differ diff --git a/RLBot_Injector.exe b/RLBot_Injector.exe index 3618b8b..4482f46 100644 Binary files a/RLBot_Injector.exe and b/RLBot_Injector.exe differ diff --git a/atba.cfg b/atba.cfg index ad0be1a..97152bf 100644 --- a/atba.cfg +++ b/atba.cfg @@ -18,4 +18,21 @@ paint_finish_1_id = 1978 paint_finish_2_id = 1978 engine_audio_id = 0 trails_id = 0 -goal_explosion_id = 1971 \ No newline at end of file +goal_explosion_id = 1971 + +[Participant Loadout Orange] +# Name that will be displayed in game +name = AlwaysTowardsBallAgent +team_color_id = 1 +custom_color_id = 1 +car_id = 23 +decal_id = 0 +wheels_id = 818 +boost_id = 0 +antenna_id = 287 +hat_id = 0 +paint_finish_1_id = 266 +paint_finish_2_id = 266 +engine_audio_id = 0 +trails_id = 0 +goal_explosion_id = 1971 diff --git a/bot_manager.py b/bot_manager.py index 353fe93..842f7e4 100644 --- a/bot_manager.py +++ b/bot_manager.py @@ -1,6 +1,6 @@ import ctypes -from datetime import datetime import gzip +from datetime import datetime, timedelta import importlib import mmap import os @@ -12,6 +12,9 @@ import bot_input_struct as bi import game_data_struct as gd import rate_limiter +import sys +import imp +import traceback from conversions import binary_converter as compressor @@ -19,7 +22,8 @@ OUTPUT_SHARED_MEMORY_TAG = 'Local\\RLBotOutput' INPUT_SHARED_MEMORY_TAG = 'Local\\RLBotInput' -RATE_LIMITED_ACTIONS_PER_SECOND = 60 +GAME_TICK_PACKET_REFRESHES_PER_SECOND = 120 # 2*60. https://en.wikipedia.org/wiki/Nyquist_rate +MAX_AGENT_CALL_PERIOD = timedelta(seconds=1.0/30) # Minimum call rate when paused. REFRESH_IN_PROGRESS = 1 REFRESH_NOT_IN_PROGRESS = 0 MAX_CARS = 10 @@ -50,6 +54,14 @@ def __init__(self, terminateEvent, callbackEvent, config_file, name, team, index self.batch_size = 1000 self.upload_size = 20 + def load_agent(self, agent_module): + try: + agent = agent_module.Agent(self.name, self.team, self.index, config_file=self.config_file) + except TypeError as e: + print(e) + agent = agent_module.Agent(self.name, self.team, self.index) + return agent + def run(self): # Set up shared memory map (offset makes it so bot only writes to its own input!) and map to buffer filename = "" @@ -64,11 +76,11 @@ def run(self): lock = ctypes.c_long(0) game_tick_packet = gd.GameTickPacket() # We want to do a deep copy for game inputs so people don't mess with em - # Get bot module - agent_module = importlib.import_module(self.module_name) # Create Ratelimiter - r = rate_limiter.RateLimiter(RATE_LIMITED_ACTIONS_PER_SECOND) + r = rate_limiter.RateLimiter(GAME_TICK_PACKET_REFRESHES_PER_SECOND) + last_tick_game_time = None # What the tick time of the last observed tick was + last_call_real_time = datetime.now() # When we last called the Agent # Find car with same name and assign index for i in range(MAX_CARS): @@ -76,17 +88,15 @@ def run(self): self.index = i continue + # Get bot module + agent_module = importlib.import_module(self.module_name) # Create bot from module - try: - agent = agent_module.Agent(self.name, self.team, self.index, config_file=self.config_file) - except TypeError as e: - print(e) - agent = agent_module.Agent(self.name, self.team, self.index) + agent = self.load_agent(agent_module) if hasattr(agent, 'create_model_hash'): self.model_hash = agent.create_model_hash() else: - self.model_hash = 0 #int(hashlib.sha256(self.name.encode('utf-8')).hexdigest(), 16) % 2 ** 64 + self.model_hash = 0 self.server_manager.set_model_hash(self.model_hash) @@ -101,6 +111,8 @@ def run(self): old_time = 0 counter = 0 + last_module_modification_time = os.stat(agent_module.__file__).st_mtime + # Run until main process tells to stop while not self.terminateEvent.is_set(): before = datetime.now() @@ -120,18 +132,44 @@ def run(self): print('\n\n\n\n Match has ended so ending bot loop\n\n\n\n\n') break - # Call agent - controller_input = agent.get_output_vector(game_tick_packet) - - # Write all player inputs - player_input.fThrottle = controller_input[0] - player_input.fSteer = controller_input[1] - player_input.fPitch = controller_input[2] - player_input.fYaw = controller_input[3] - player_input.fRoll = controller_input[4] - player_input.bJump = controller_input[5] - player_input.bBoost = controller_input[6] - player_input.bHandbrake = controller_input[7] + # Run the Agent only if the gameInfo has updated. + tick_game_time = game_tick_packet.gameInfo.TimeSeconds + should_call_while_paused = datetime.now() - last_call_real_time >= MAX_AGENT_CALL_PERIOD + if tick_game_time != last_tick_game_time or should_call_while_paused: + last_tick_game_time = tick_game_time + last_call_real_time = datetime.now() + + try: + # Reload the Agent if it has been modified. + new_module_modification_time = os.stat(agent_module.__file__).st_mtime + if new_module_modification_time != last_module_modification_time: + last_module_modification_time = new_module_modification_time + print('Reloading Agent: ' + agent_module.__file__) + imp.reload(agent_module) + agent = self.load_agent(agent_module) + + # Call agent + controller_input = agent.get_output_vector(game_tick_packet) + + if not controller_input: + raise Exception('Agent "{}" did not return a player_input tuple.'.format(agent_module.__file__)) + + # Write all player inputs + player_input.fThrottle = controller_input[0] + player_input.fSteer = controller_input[1] + player_input.fPitch = controller_input[2] + player_input.fYaw = controller_input[3] + player_input.fRoll = controller_input[4] + player_input.bJump = controller_input[5] + player_input.bBoost = controller_input[6] + player_input.bHandbrake = controller_input[7] + + except Exception as e: + traceback.print_exc() + + # Workaround for windows streams behaving weirdly when not in command prompt + sys.stdout.flush() + sys.stderr.flush() current_time = game_tick_packet.gameInfo.TimeSeconds @@ -161,6 +199,7 @@ def run(self): # Ratelimit here after = datetime.now() + after2 = time.time() # cant ever drop below 30 frames if after2 - before2 > 0.02: diff --git a/game_data_struct.py b/game_data_struct.py index 1dad816..152bacc 100644 --- a/game_data_struct.py +++ b/game_data_struct.py @@ -225,6 +225,12 @@ def rotate_game_tick_packet_boost_omitted(game_tick_packet): game_tick_packet.gameball.AngularVelocity.Y = -1 * game_tick_packet.gameball.AngularVelocity.Y game_tick_packet.gameball.Acceleration.X = -1 * game_tick_packet.gameball.Acceleration.X game_tick_packet.gameball.Acceleration.Y = -1 * game_tick_packet.gameball.Acceleration.Y + + # ball touch data + game_tick_packet.gameball.LatestTouch.sHitLocation.X = -1 * game_tick_packet.gameball.LatestTouch.sHitLocation.X + game_tick_packet.gameball.LatestTouch.sHitLocation.Y = -1 * game_tick_packet.gameball.LatestTouch.sHitLocation.Y + game_tick_packet.gameball.LatestTouch.sHitNormal.X = -1 * game_tick_packet.gameball.LatestTouch.sHitNormal.X + game_tick_packet.gameball.LatestTouch.sHitNormal.Y = -1 * game_tick_packet.gameball.LatestTouch.sHitNormal.Y # Rotate Yaw 180 degrees is all that is necessary. ball_yaw = game_tick_packet.gameball.Rotation.Yaw diff --git a/images/RLBot.png b/images/RLBot.png new file mode 100644 index 0000000..982842d Binary files /dev/null and b/images/RLBot.png differ diff --git a/runner.py b/runner.py index 00c6df9..c909988 100644 --- a/runner.py +++ b/runner.py @@ -26,6 +26,7 @@ RLBOT_CONFIGURATION_HEADER = 'RLBot Configuration' INPUT_SHARED_MEMORY_TAG = 'Local\\RLBotInput' BOT_CONFIG_LOADOUT_HEADER = 'Participant Loadout' +BOT_CONFIG_LOADOUT_ORANGE_HEADER = 'Participant Loadout Orange' BOT_CONFIG_MODULE_HEADER = 'Bot Location' USER_CONFIGURATION_HEADER = 'User Info' @@ -69,7 +70,7 @@ def run_agent(terminate_event, callback_event, config_file, name, team, index, m bm.run() -if __name__ == '__main__': +def main(): # Set up RLBot.cfg framework_config = configparser.RawConfigParser() framework_config.read(RLBOT_CONFIG_FILE) @@ -127,6 +128,16 @@ def run_agent(terminate_event, callback_event, config_file, name, team, index, m else: bot_config.read(participant_configs[i]) + team_num = framework_config.getint(PARTICPANT_CONFIGURATION_HEADER, + PARTICPANT_TEAM_PREFIX + str(i)) + + loadout_header = BOT_CONFIG_LOADOUT_HEADER + if (team_num == 1 and bot_config.has_section(BOT_CONFIG_LOADOUT_ORANGE_HEADER)): + loadout_header = BOT_CONFIG_LOADOUT_ORANGE_HEADER + + if gameInputPacket.sPlayerConfiguration[i].ucTeam == 0: + num_team_0 += 1 + gameInputPacket.sPlayerConfiguration[i].bBot = framework_config.getboolean(PARTICPANT_CONFIGURATION_HEADER, PARTICPANT_BOT_KEY_PREFIX + str(i)) gameInputPacket.sPlayerConfiguration[i].bRLBotControlled = framework_config.getboolean( @@ -136,34 +147,32 @@ def run_agent(terminate_event, callback_event, config_file, name, team, index, m PARTICPANT_BOT_SKILL_KEY_PREFIX + str(i)) gameInputPacket.sPlayerConfiguration[i].iPlayerIndex = i - gameInputPacket.sPlayerConfiguration[i].wName = get_sanitized_bot_name(name_dict, - bot_config.get(BOT_CONFIG_LOADOUT_HEADER, - 'name')) - gameInputPacket.sPlayerConfiguration[i].ucTeam = framework_config.getint(PARTICPANT_CONFIGURATION_HEADER, - PARTICPANT_TEAM_PREFIX + str(i)) - if gameInputPacket.sPlayerConfiguration[i].ucTeam == 0: - num_team_0 += 1 - gameInputPacket.sPlayerConfiguration[i].ucTeamColorID = bot_config.getint(BOT_CONFIG_LOADOUT_HEADER, + + gameInputPacket.sPlayerConfiguration[i].wName = get_sanitized_bot_name(name_dict, bot_config.get(loadout_header, + 'name')) + gameInputPacket.sPlayerConfiguration[i].ucTeam = team_num + gameInputPacket.sPlayerConfiguration[i].ucTeamColorID = bot_config.getint(loadout_header, 'team_color_id') - gameInputPacket.sPlayerConfiguration[i].ucCustomColorID = bot_config.getint(BOT_CONFIG_LOADOUT_HEADER, + gameInputPacket.sPlayerConfiguration[i].ucCustomColorID = bot_config.getint(loadout_header, 'custom_color_id') - gameInputPacket.sPlayerConfiguration[i].iCarID = bot_config.getint(BOT_CONFIG_LOADOUT_HEADER, 'car_id') - gameInputPacket.sPlayerConfiguration[i].iDecalID = bot_config.getint(BOT_CONFIG_LOADOUT_HEADER, 'decal_id') - gameInputPacket.sPlayerConfiguration[i].iWheelsID = bot_config.getint(BOT_CONFIG_LOADOUT_HEADER, 'wheels_id') - gameInputPacket.sPlayerConfiguration[i].iBoostID = bot_config.getint(BOT_CONFIG_LOADOUT_HEADER, 'boost_id') - gameInputPacket.sPlayerConfiguration[i].iAntennaID = bot_config.getint(BOT_CONFIG_LOADOUT_HEADER, 'antenna_id') - gameInputPacket.sPlayerConfiguration[i].iHatID = bot_config.getint(BOT_CONFIG_LOADOUT_HEADER, 'hat_id') - gameInputPacket.sPlayerConfiguration[i].iPaintFinish1ID = bot_config.getint(BOT_CONFIG_LOADOUT_HEADER, + gameInputPacket.sPlayerConfiguration[i].iCarID = bot_config.getint(loadout_header, 'car_id') + gameInputPacket.sPlayerConfiguration[i].iDecalID = bot_config.getint(loadout_header, 'decal_id') + gameInputPacket.sPlayerConfiguration[i].iWheelsID = bot_config.getint(loadout_header, 'wheels_id') + gameInputPacket.sPlayerConfiguration[i].iBoostID = bot_config.getint(loadout_header, 'boost_id') + gameInputPacket.sPlayerConfiguration[i].iAntennaID = bot_config.getint(loadout_header, 'antenna_id') + gameInputPacket.sPlayerConfiguration[i].iHatID = bot_config.getint(loadout_header, 'hat_id') + gameInputPacket.sPlayerConfiguration[i].iPaintFinish1ID = bot_config.getint(loadout_header, 'paint_finish_1_id') - gameInputPacket.sPlayerConfiguration[i].iPaintFinish2ID = bot_config.getint(BOT_CONFIG_LOADOUT_HEADER, + gameInputPacket.sPlayerConfiguration[i].iPaintFinish2ID = bot_config.getint(loadout_header, 'paint_finish_2_id') - gameInputPacket.sPlayerConfiguration[i].iEngineAudioID = bot_config.getint(BOT_CONFIG_LOADOUT_HEADER, + gameInputPacket.sPlayerConfiguration[i].iEngineAudioID = bot_config.getint(loadout_header, 'engine_audio_id') - gameInputPacket.sPlayerConfiguration[i].iTrailsID = bot_config.getint(BOT_CONFIG_LOADOUT_HEADER, 'trails_id') - gameInputPacket.sPlayerConfiguration[i].iGoalExplosionID = bot_config.getint(BOT_CONFIG_LOADOUT_HEADER, + gameInputPacket.sPlayerConfiguration[i].iTrailsID = bot_config.getint(loadout_header, 'trails_id') + gameInputPacket.sPlayerConfiguration[i].iGoalExplosionID = bot_config.getint(loadout_header, 'goal_explosion_id') config_files.append(bot_config) - bot_names.append(bot_config.get(BOT_CONFIG_LOADOUT_HEADER, 'name')) + + bot_names.append(bot_config.get(loadout_header, 'name')) bot_teams.append(framework_config.getint(PARTICPANT_CONFIGURATION_HEADER, PARTICPANT_TEAM_PREFIX + str(i))) if gameInputPacket.sPlayerConfiguration[i].bRLBotControlled: bot_modules.append(bot_config.get(BOT_CONFIG_MODULE_HEADER, 'agent_module')) @@ -216,3 +225,7 @@ def run_agent(terminate_event, callback_event, config_file, name, team, index, m for callback in callbacks: if not callback.is_set(): terminated = False + + +if __name__ == '__main__': + main()