From 6f5439e517c10d4a8d6e22e016b92ba88bdcf44e Mon Sep 17 00:00:00 2001 From: "NGnius (Graham)" Date: Sun, 9 May 2021 21:06:17 -0400 Subject: [PATCH] Implement Robocraft CRF importing (untested -- incomplete GUI) --- NPort/API/IImporter.cs | 11 + NPort/API/ImportRegistry.cs | 27 +++ NPort/API/SimpleImporter.cs | 36 +++ NPort/Bindings/Robocraft.cs | 229 +++++++++++++++++++- NPort/Importers/RobocraftFactoryImporter.cs | 162 ++++++++++++++ NPort/NPort.csproj | 1 + NPort/NPortPlugin.cs | 17 +- clibfj | 2 +- 8 files changed, 468 insertions(+), 17 deletions(-) create mode 100644 NPort/API/IImporter.cs create mode 100644 NPort/API/ImportRegistry.cs create mode 100644 NPort/API/SimpleImporter.cs create mode 100644 NPort/Importers/RobocraftFactoryImporter.cs diff --git a/NPort/API/IImporter.cs b/NPort/API/IImporter.cs new file mode 100644 index 0000000..57b90cc --- /dev/null +++ b/NPort/API/IImporter.cs @@ -0,0 +1,11 @@ +namespace NPort.API +{ + public interface IImporter + { + string Name { get; } + + string Help(string[] args); + + void Import(string[] args); + } +} \ No newline at end of file diff --git a/NPort/API/ImportRegistry.cs b/NPort/API/ImportRegistry.cs new file mode 100644 index 0000000..568cce4 --- /dev/null +++ b/NPort/API/ImportRegistry.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; + +namespace NPort.API +{ + public static class ImportRegistry + { + private static List importers = new List(); + + public static void AddImporter(IImporter i) + { + importers.Add(i); + } + + internal static void Call(string[] args, string name = null) + { + if (name == null) name = args[0]; + foreach (var imp in importers) + { + if (imp.Name == name) + { + imp.Import(args); + break; + } + } + } + } +} \ No newline at end of file diff --git a/NPort/API/SimpleImporter.cs b/NPort/API/SimpleImporter.cs new file mode 100644 index 0000000..d42ee55 --- /dev/null +++ b/NPort/API/SimpleImporter.cs @@ -0,0 +1,36 @@ +using TechbloxModdingAPI; +using TechbloxModdingAPI.Blocks; +using Unity.Mathematics; + +namespace NPort.API +{ + public abstract class SimpleImporter : IImporter + { + public abstract string Name { get; } + public abstract string Help(string[] args); + + public void Import(string[] args) + { + // TODO make this non-blocking (separate thread or maybe async) + if (SimpleImport(args)) + { + // good! + } + else + { + TechbloxModdingAPI.Utility.Logging.LogError($"Failed to import {Name}({args})"); + } + } + + protected void PlaceBlock(BlockIDs type, float3 position, float3 rotation, BlockColor colour, BlockMaterial material) + { + // TODO bulk place (cache and place when Import(...) completes?) + Block placedBlock = Block.PlaceNew(type, position); + placedBlock.Color = colour; + placedBlock.Rotation = rotation; + placedBlock.Material = material; + } + + public abstract bool SimpleImport(string[] args); + } +} \ No newline at end of file diff --git a/NPort/Bindings/Robocraft.cs b/NPort/Bindings/Robocraft.cs index e486f53..85a82a5 100644 --- a/NPort/Bindings/Robocraft.cs +++ b/NPort/Bindings/Robocraft.cs @@ -1,12 +1,15 @@ +using System; using System.Runtime.InteropServices; namespace NPort.Bindings { + // C-style structs used in function signatures public static class Robocraft { [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)] - public struct FactoryRobotListInfo{ - public uint item_id; + public struct FactoryRobotListInfo + { + public UInt32 item_id; public string item_name; public string item_description; public string thumbnail; // url @@ -14,29 +17,235 @@ namespace NPort.Bindings public string added_by_display_name; public string added_date; // ISO date public string expiry_date; // ISO date - public uint cpu; - public uint total_robot_ranking; - public uint rent_count; - public uint buy_count; - public uint buyable; // bool + public UInt32 cpu; + public UInt32 total_robot_ranking; + public UInt32 rent_count; + public UInt32 buy_count; + public UInt32 buyable; // bool public string removed_date; public string ban_date; - public uint featured; // bool + public UInt32 featured; // bool public string banner_message; public float combat_rating; public float cosmetic_rating; public string cube_amounts; // JSON as str } + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)] + public struct FactoryRobotGetInfo + { + public UInt32 item_id; + public string item_name; + public string item_description; + public string thumbnail; // url + public string added_by; + public string added_by_display_name; + public string added_date; // ISO date + public string expiry_date; // ISO date + public UInt32 cpu; + public UInt32 total_robot_ranking; + public UInt32 rent_count; + public UInt32 buy_count; + public UInt32 buyable; // bool + public string removed_date; + public string ban_date; + public UInt32 featured; // bool + public string banner_message; + public float combat_rating; + public float cosmetic_rating; + public string cube_data; + public string colour_data; + public string cube_amounts; // JSON as str + } + + public enum FactoryOrderType : Int32 + { + Suggested = 0, + CombatRating = 1, + CosmeticRating = 2, + Added = 3, + CPU = 4, + MostBought = 5, + } + + public enum FactoryMovementType : Int32 + { + Wheels=100000, + Hovers=200000, + Aerofoils=300000, + Thrusters=400000, + Rudders=500000, + InsectLegs=600000, + MechLegs=700000, + Skis=800000, + TankTreads=900000, + Rotors=1000000, + Sprinters=1100000, + Propellers=1200000 + } + + public enum FactoryWeaponType : Int32 + { + Laser=10000000, + PlasmaLauncher=20000000, + GyroMortar=25000000, + RailCannon=30000000, + NanoDisruptor=40000000, + TeslaBlade=50000000, + AeroflakCannon=60000000, + IonCannon=65000000, + ProtoSeeker=70100000, + ChainShredder=75000000, + } + + public enum FactoryTextSearchType : Int32 + { + All=0, + Player=1, + Name=2, + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)] + public unsafe struct FactorySearchQuery + { + public Int32* page; + public Int32* items_per_page; + public FactoryOrderType* order; + public string movement_filter; + public string weapon_filter; + public Int32* minimum_cpu; + public Int32* maximum_cpu; + public string text_filter; + public FactoryTextSearchType* text_search_field; + public UInt32* buyable; // bool + public UInt32* prepend_featured_robot; // bool + public UInt32* featured_only; // bool + public UInt32* default_page; // bool + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)] + public struct CubeData + { + public UInt32 id; + public byte x; + public byte y; + public byte z; + public byte orientation; + public byte colour; + } + + // raw C-style bindings + + [DllImport("libfj.dll", CallingConvention = CallingConvention.Cdecl)] + static extern UInt32 libfj_factory_front_page(UInt32 size, [In,Out] FactoryRobotListInfo[] array_ptr); + + [DllImport("libfj.dll", CallingConvention = CallingConvention.Cdecl)] + static extern UInt32 libfj_factory_search(UInt32 size, [In,Out] FactoryRobotListInfo[] array_ptr, ref FactorySearchQuery search); + + [DllImport("libfj.dll", CallingConvention = CallingConvention.Cdecl)] + static extern void libfj_factory_robot(UInt32 item_id, out FactoryRobotGetInfo result_out); + + [DllImport("libfj.dll", CallingConvention = CallingConvention.Cdecl)] + static extern UInt32 libfj_factory_robot_cubes(UInt32 size, [In, Out] CubeData[] array_ptr, ref FactoryRobotGetInfo info); + [DllImport("libfj.dll", CallingConvention = CallingConvention.Cdecl)] - static extern void get_factory_front_page(uint size, [In,Out] FactoryRobotListInfo[] array_ptr); + static extern UInt32 libfj_factory_robot_cubes_raw(UInt32 size, [In, Out] CubeData[] array_ptr, string cube_data, string colour_data); + // nicer C#-style wrappers + public static FactoryRobotListInfo[] GetFactoryFrontPage() { uint size = 100; FactoryRobotListInfo[] arr = new FactoryRobotListInfo[size]; - get_factory_front_page(size, arr); + UInt32 result_count = libfj_factory_front_page(size, arr); + if (result_count < arr.Length) + { + FactoryRobotListInfo[] result = new FactoryRobotListInfo[result_count]; + Array.Copy(arr, result, result_count); + return result; + } return arr; } + + public static FactoryRobotListInfo[] SearchFactoryRobots(Int32? page = null, + Int32? items_per_page = null, + FactoryOrderType? order = null, + string movement_filter = null, + string weapon_filter = null, + Int32? max_cpu = null, + Int32? min_cpu = null, + string text_filter = null, + FactoryTextSearchType? text_search_field = null, + bool? buyable = null, + bool? prepend_featured_robot = null, + bool? featured_only = null, + bool? default_page = null) + { + UInt32 size = (UInt32) (page?? 100); + FactoryRobotListInfo[] arr = new FactoryRobotListInfo[size]; + UInt32 result_count; + unsafe + { + // this is unnecessary copying, but nullables aren't marshalled correctly (thanks MS!) + Int32 page2 = page ?? 0; + Int32 items_per_page2 = items_per_page ?? 0; + FactoryOrderType order2 = order ?? FactoryOrderType.Suggested; + Int32 maximum_cpu = max_cpu ?? 0; + Int32 minimum_cpu = min_cpu ?? 0; + FactoryTextSearchType text_search_field2 = text_search_field ?? FactoryTextSearchType.All; + UInt32 buyable2 = buyable.HasValue ? (buyable.Value ? 1u : 0u): 0u; + UInt32 prepend_featured_robot2 = prepend_featured_robot.HasValue + ? (prepend_featured_robot.Value ? 1u : 0u) + : 0u; + UInt32 featured_only2 = featured_only.HasValue ? (featured_only.Value ? 1u : 0u) : 0u; + UInt32 default_page2 = default_page.HasValue ? (default_page.Value ? 1u : 0u) : 0u; + FactorySearchQuery search = new FactorySearchQuery + { + page = page.HasValue ? &page2 : null, + items_per_page = items_per_page.HasValue ? &items_per_page2 : null, + order = &order2, + movement_filter = movement_filter, + weapon_filter = weapon_filter, + maximum_cpu = max_cpu.HasValue ? &maximum_cpu : null, + minimum_cpu = min_cpu.HasValue ? &minimum_cpu : null, + text_filter = text_filter, + text_search_field = text_search_field.HasValue ? &text_search_field2 : null, + buyable = buyable.HasValue? &buyable2 : null, + prepend_featured_robot = prepend_featured_robot.HasValue? &prepend_featured_robot2 : null, + featured_only = featured_only.HasValue? &featured_only2 : null, + default_page = default_page.HasValue? &default_page2 : null, + }; + result_count = libfj_factory_search(size, arr, ref search); + } + + if (result_count < arr.Length) + { + FactoryRobotListInfo[] result = new FactoryRobotListInfo[result_count]; + Array.Copy(arr, result, result_count); + return result; + } + + return arr; + } + + public static FactoryRobotGetInfo GetRobot(UInt32 item_id) + { + FactoryRobotGetInfo result_out = default; + libfj_factory_robot(item_id, out result_out); + return result_out; + } + + public static CubeData[] GetRobotCubes(FactoryRobotGetInfo info) + { + CubeData[] cubes = new CubeData[info.cpu]; + UInt32 cubeCount = libfj_factory_robot_cubes((UInt32)cubes.Length, cubes, ref info); + if (cubeCount < cubes.Length) + { + CubeData[] result = new CubeData[cubeCount]; + Array.Copy(cubes, result, cubeCount); + return result; + } + return cubes; + } } } \ No newline at end of file diff --git a/NPort/Importers/RobocraftFactoryImporter.cs b/NPort/Importers/RobocraftFactoryImporter.cs new file mode 100644 index 0000000..e5b2610 --- /dev/null +++ b/NPort/Importers/RobocraftFactoryImporter.cs @@ -0,0 +1,162 @@ +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using NPort.Bindings; +using TechbloxModdingAPI.Blocks; +using TechbloxModdingAPI.Utility; +using Unity.Mathematics; + +namespace NPort.Importers +{ + public class RobocraftFactoryImporter : API.SimpleImporter + { + public override string Name { get; } = "CRF"; + + public override string Help(string[] args) + { + return "Import a robot from Robocraft's Community Robot Factory"; + } + + public override bool SimpleImport(string[] args) + { + if (args.Length < 2) + { + // user did not specify enough arguments + return false; + } + + string text = args[1]; + + Robocraft.FactoryRobotListInfo[] robots = Robocraft.SearchFactoryRobots(text_filter: text); + if (robots.Length == 0) + { + // no results + return false; + } + + Robocraft.CubeData[] cubes = Robocraft.GetRobotCubes(Robocraft.GetRobot(robots[0].item_id)); + foreach (var cube in cubes) + { + float3 rotation = TranslateBlockRotation(cube.orientation); + float3 position = new float3(cube.x, cube.y, cube.z); + BlockColor colour = QuantizeToBlockColor(cube.colour); + PlaceBlock(BlockIDs.Cube, position, rotation, colour, BlockMaterial.SteelBodywork); + break; + } + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static float3 TranslateBlockRotation(byte rotation) + { + // from https://git.exmods.org/NGnius/Pixi + switch (rotation) + { + case 0: + return new float3(0, 0, 0); // top face, forwards + case 1: + return new float3(0, 0, 90); // left face, forwards + case 2: + return new float3(0, 0, 180); // bottom face, forwards + case 3: + return new float3(0, 0, -90); // front face, down + case 4: + return new float3(0, 90, 0); // top face, right + case 5: + return new float3(0, 90, 90); // front face, right + case 6: + return new float3(-90, -90, 0); // right face, backwards + case 7: + return new float3(0, 90, -90); // back face, right + case 8: + return new float3(0, -90, 90); // back face, left + case 9: + return new float3(0, -90, -90); // front face, left + case 10: + return new float3(90, -90, 0); // left face, down + case 11: + return new float3(90, 90, 0); // right face, forwards + case 12: + return new float3(-90, 90, 0); // left face, up + case 13: + return new float3(0, 90, 180); // bottom face, right + case 14: + return new float3(0, 180, 0); // top face, backwards + case 15: + return new float3(0, 180, 90); // right face, up + case 16: + return new float3(0, 180, 180); // bottom face, backwards + case 17: + return new float3(0, 180, -90); // left face, backwards + case 18: + return new float3(0, -90, 0); // top face, left + case 19: + return new float3(0, -90, 180); // bottom face, left + case 20: + return new float3(90, 0, 0); // front face, down + case 21: + return new float3(90, 180, 0); // back face, down + case 22: + return new float3(-90, 0, 0); // back face, up + case 23: + return new float3(-90, 180, 0); // front face, up + default: +#if DEBUG + Logging.MetaLog($"Unknown rotation {rotation.ToString("X2")}"); +#endif + return float3.zero; + } + // my brain hurts after figuring out all of those rotations + // I wouldn't recommend trying to redo this + } + + private static Dictionary botColorMap = null; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static BlockColor QuantizeToBlockColor(byte cubeColorEnum) + { + if (botColorMap == null) BuildBotColorMap(); + return botColorMap[cubeColorEnum]; + } + + private static void BuildBotColorMap() + { + botColorMap = new Dictionary(); + // standard colours + botColorMap[0] = new BlockColor (BlockColors.White, 0); + botColorMap[1] = new BlockColor (BlockColors.White, 6); + botColorMap[4] = new BlockColor (BlockColors.White, 8); + botColorMap[5] = new BlockColor (BlockColors.Red, 5); + botColorMap[2] = new BlockColor (BlockColors.Orange, 0); + botColorMap[6] = new BlockColor (BlockColors.Yellow, 0); + botColorMap[7] = new BlockColor (BlockColors.Green, 5); + botColorMap[3] = new BlockColor (BlockColors.Aqua, 5); + botColorMap[9] = new BlockColor (BlockColors.Blue, 5); + botColorMap[10] = new BlockColor (BlockColors.Purple, 5); + // premium colours + botColorMap[16] = new BlockColor (BlockColors.Red, 0); + botColorMap[17] = new BlockColor (BlockColors.Red, 7); + botColorMap[11] = new BlockColor (BlockColors.Orange, 6); + botColorMap[18] = new BlockColor (BlockColors.Purple, 9); + botColorMap[19] = new BlockColor (BlockColors.Pink, 9); + botColorMap[20] = new BlockColor (BlockColors.Orange, 5); + botColorMap[14] = new BlockColor (BlockColors.Yellow, 3); + botColorMap[21] = new BlockColor (BlockColors.Green, 7); + botColorMap[22] = new BlockColor (BlockColors.Lime, 8); + botColorMap[13] = new BlockColor (BlockColors.Green, 6); + botColorMap[12] = new BlockColor (BlockColors.Lime, 5); + // blue gang + botColorMap[23] = new BlockColor (BlockColors.Blue, 8); + botColorMap[24] = new BlockColor (BlockColors.Aqua, 8); + botColorMap[25] = new BlockColor (BlockColors.Blue, 7); + botColorMap[26] = new BlockColor (BlockColors.White, 5); + botColorMap[27] = new BlockColor (BlockColors.White, 4); + botColorMap[28] = new BlockColor (BlockColors.Aqua, 4); + botColorMap[29] = new BlockColor (BlockColors.Purple, 8); + // purples & pinks + botColorMap[30] = new BlockColor (BlockColors.Pink, 0); + botColorMap[8] = new BlockColor (BlockColors.Pink, 5); + botColorMap[31] = new BlockColor (BlockColors.Pink, 4); + botColorMap[15] = new BlockColor (BlockColors.Red, 3); + } + } +} \ No newline at end of file diff --git a/NPort/NPort.csproj b/NPort/NPort.csproj index 0e95e7a..34f0986 100644 --- a/NPort/NPort.csproj +++ b/NPort/NPort.csproj @@ -8,6 +8,7 @@ MIT https://git.exmods.org/NGnius/NPort en-CA + true diff --git a/NPort/NPortPlugin.cs b/NPort/NPortPlugin.cs index cca6374..102d461 100644 --- a/NPort/NPortPlugin.cs +++ b/NPort/NPortPlugin.cs @@ -3,6 +3,7 @@ using IllusionPlugin; using TechbloxModdingAPI.Utility; using TechbloxModdingAPI; +using UnityEngine; namespace NPort { @@ -35,18 +36,22 @@ namespace NPort Logging.MetaLog($"{Name} has started up"); #if DEBUG // TEST - Bindings.Robocraft.FactoryRobotListInfo[] items = Bindings.Robocraft.GetFactoryFrontPage(); + Bindings.Robocraft.FactoryRobotListInfo[] items = Bindings.Robocraft.SearchFactoryRobots(text_filter: "a"); foreach (var item in items) { Logging.MetaLog($"{item.item_name} by {item.added_by_display_name} ({item.item_id})"); } #endif + // TODO use reflection to automagically register stuff? + API.ImportRegistry.AddImporter(new Importers.RobocraftFactoryImporter()); } - // unused methods - - public override void OnFixedUpdate() { } // called once per physics update - - public override void OnUpdate() { } // called once per rendered frame (frame update) + public override void OnGUI() + { + if (GUILayout.Button("Get PIG!!! OINK!")) + { + API.ImportRegistry.Call(new []{"CRF", "PIG",}); + } + } } } diff --git a/clibfj b/clibfj index aa8faeb..16358c2 160000 --- a/clibfj +++ b/clibfj @@ -1 +1 @@ -Subproject commit aa8faeb42863ae802626ccb6b2ac3291c87ed928 +Subproject commit 16358c2aecdc07c0ac16a8aff8ec213528ac3950