From 45f66cc6279abe648a823cda21b034139d0b1677 Mon Sep 17 00:00:00 2001 From: Joshua Mckinnon Date: Sat, 25 Nov 2023 17:32:33 -0800 Subject: [PATCH] Initial commit I forgot to push changes as I go lol --- .gitignore | 2 + README.md | 64 +++++++ globals.d.lua | 12 ++ parts.json | 266 +++++++++++++++++++++++++++++ scripts/generateDefinitions.py | 54 ++++++ scripts/generatePartDefinitions.py | 107 ++++++++++++ types.d.lua | 39 +++++ 7 files changed, 544 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 globals.d.lua create mode 100644 parts.json create mode 100644 scripts/generateDefinitions.py create mode 100644 scripts/generatePartDefinitions.py create mode 100644 types.d.lua diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3d43339 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +build/ +__pycache__/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..fe4cb7d --- /dev/null +++ b/README.md @@ -0,0 +1,64 @@ +# autopilot.lua + +Full code completion for [pilot.lua](https://github.com/iimurpyh/pilot-lua/wiki/) using JohnnyMorganz' Luau Language Server. + +## Getting Started + +To get started, follow these simple steps: + +1. **Download the Definitions File:** + - Head over to the [Releases](https://github.com/your-username/autopilot.lua/releases) tab. + - Download the latest version of the `pilot.d.lua` file. + +3. **Install luau-lsp for VSCode:** + - Install JohnnyMorganz's [luau-lsp](https://github.com/JohnnyMorganz/luau-lsp) extension for Visual Studio Code. + +4. **Configure VSCode Settings:** + - Place `pilot.d.lua` in your project directory. + - Create a `.vscode` folder in your project directory. + - Inside the `.vscode` folder, create a `settings.json` file. + - Add the following properties to `settings.json`: + + ```json + { + "luau-lsp.sourcemap.enabled": false, + "luau-lsp.types.roblox": true, + "luau-lsp.types.definitionFiles": [ + "./pilot.d.lua" + ] + } + ``` + +5. **Enjoy Full Code Completion:** + - With the setup complete, you now have full code completion for `pilot.lua` in Visual Studio Code. + +# Build Steps + +If you want to build autopilot.lua from scratch, follow the steps below: + +1. **Clone the Repository** + - Clone the repository to your local machine + + ``` + git clone https://github.com/flxwed/autopilot-lua.git + ``` + +2. **Generate Definitions via Script** + - Open your cloned repository in your favorite code editor + - Run `scripts/generateDefinitions.py` and pass the project directory as the first argument. + - After the script finishes, the generated code can be found at `build/pilot.d.lua`. + + ``` + py scripts/generateDefinitions.py . + > .\build\pilot.d.lua created successfully + cat build/pilot.d.lua + > type PortLike = number | {GUID: string} + type Properties = {[string]: any} + ... + ``` + +## Contributions + +We welcome contributions and bug reports. Feel free to fork the repository, make your changes, and submit a pull request. If you encounter any issues or have suggestions for improvement, please open an issue. + +Happy coding with autopilot.lua! diff --git a/globals.d.lua b/globals.d.lua new file mode 100644 index 0000000..61ee2f2 --- /dev/null +++ b/globals.d.lua @@ -0,0 +1,12 @@ +-- Microcontroller Globals +-- GetPartFromPort and GetPartsFromPort are generated via script +declare GetPort: (port: PortLike) -> Part +declare TriggerPort: (port: PortLike) -> Part + +declare SandboxID: string +declare SandboxRunID: number + +declare Beep: (pitch: number) -> () +declare JSONDecode: (json: string) -> {[string]: any} +declare JSONEncode: (dataToEncode: {[string]: any}) -> string +declare Communicate: () -> () diff --git a/parts.json b/parts.json new file mode 100644 index 0000000..a2a5444 --- /dev/null +++ b/parts.json @@ -0,0 +1,266 @@ +{ + "Part": { + "default": true, + "properties": { + "ClassName": "string", + "Position": "Vector2", + "CFrame": "CFrame", + "GUID": "string" + }, + "methods": { + "Trigger": {}, + "Configure": { + "arguments": [{"properties": "Properties"}] + } + }, + "events": { + "Triggered": [], + "Configured": [] + } + }, + "Gyro": { + "extends": "Part", + "methods": { + "PointAt": { + "arguments": [{"position": "Vector3"}] + } + } + }, + "Keyboard": { + "extends": "Part", + "methods": { + "SimulateKeyPress": { + "arguments": [{"key": "string?"}, {"player": "string"}] + }, + "SimulateTextInput": { + "arguments": [{"input": "string?"}, {"player": "string"}] + } + }, + "events": { + "KeyPressed": [{"key": "Enum.KeyCode"}, {"keyString": "string"}, {"state": "Enum.UserInputState"}, {"player": "string"}], + "TextInputted": [{"text": "string"}, {"player": "string"}] + } + }, + "Microphone": { + "extends": "Part", + "events": { + "Chatted": [{"player": "string"}, {"message": "string"}] + } + }, + "LifeSensor": { + "extends": "Part", + "methods": { + "GetReading": { + "returns": "{[string]: Vector3}" + } + } + }, + "Instrument": { + "extends": "Part", + "methods": { + "GetReading": { + "arguments": [{"typeId": "number"}], + "returns": "number | Vector3" + } + } + }, + "EnergyShield": { + "extends": "Part", + "methods": { + "GetShieldHealth": { + "returns": "number" + } + }, + "properties": { + "ShieldStrength": "number", + "RegenerationSpeed": "number", + "ShieldRadius": "number" + } + }, + "Disk": { + "extends": "Part", + "methods": { + "ClearDisk": {}, + "Write": { + "arguments": [{"key": "string"}, {"data": "string"}] + }, + "Read": { + "arguments": [{"key": "string"}], + "returns": "string" + }, + "ReadEntireDisk": { + "returns": "{[string]: string}" + } + }, + "properties": { + "ShieldStrength": "number", + "RegenerationSpeed": "number", + "ShieldRadius": "number" + } + }, + "Bin": { + "extends": "Part", + "methods": { + "GetAmount": { + "returns": "number" + }, + "GetResource": { + "returns": "string" + } + } + }, + "Container": { + "extends": "Bin" + }, + "Modem": { + "extends": "Part", + "methods": { + "PostRequest": { + "arguments": [{"domain": "string"}, {"data": "string"}] + }, + "GetRequest": { + "arguments": [{"domain": "string"}], + "returns": "string" + }, + "SendMessage": { + "arguments": [{"data": "string"}, {"id": "number"}] + }, + "RealPostRequest": { + "arguments": [{"domain": "string"}, {"data": "string"}, {"asyncBool": "boolean"}, {"transformFunction": "(...any) -> ()"}, {"optionalHeaders": "{[string]: any}?"}], + "returns": "{response: string, success: boolean}" + } + }, + "events": { + "MessageSent": [{"data": "string"}] + } + }, + "Screen": { + "extends": "Part", + "methods": { + "GetDimensions": { + "returns": "Vector2" + }, + "ClearElements": { + "arguments": [{"className": "string?"}, {"properties": "Properties?"}] + }, + "CreateElement": { + "arguments": [{"className": "string"}, {"properties": "Properties"}], + "returns": "ScreenObject" + } + } + }, + "TouchScreen": { + "extends": "Screen", + "methods": { + "GetCursor": { + "returns": "Cursor" + }, + "GetCursors": { + "returns": "{Cursor}" + } + }, + "events": { + "CursorMoved": [{"cursor": "Cursor"}], + "CursorPressed": [{"cursor": "Cursor"}], + "CursorReleased": [{"cursor": "Cursor"}] + } + }, + "TouchSensor": { + "extends": "Part", + "events": { + "Touched": [] + } + }, + "Button": { + "extends": "Part", + "events": { + "OnClick": [{"player": "string"}] + } + }, + "Light": { + "extends": "Part", + "methods": { + "SetColor": { + "arguments": [{"color": "Color3"}] + } + } + }, + "Rail": { + "extends": "Part", + "methods": { + "SetPosition": { + "arguments": [{"depth": "number"}] + } + } + }, + "StarMap": { + "extends": "Part", + "methods": { + "GetBodies": { + "returns": "Iterator" + }, + "GetSystems": { + "returns": "Iterator" + } + } + }, + "Telescope": { + "extends": "Part", + "methods": { + "GetCoordinate": { + "returns": "RegionInfo" + }, + "WhenRegionLoads": { + "arguments": [{"callback": "(regionData: RegionInfo) -> ()"}] + } + } + }, + "Speaker": { + "extends": "Part", + "methods": { + "PlaySound": { + "arguments": [{"soundId": "number"}] + }, + "ClearSounds": {}, + "Chat": { + "arguments": [{"message": "string"}] + } + } + }, + "Reactor": { + "extends": "Part", + "methods": { + "GetFuel": { + "returns": "{[number]: number}" + }, + "GetTemp": { + "returns": "number" + } + } + }, + "Dispenser": { + "extends": "Part", + "methods": { + "Dispense": {} + } + }, + "Faucet": { + "extends": "Dispenser" + }, + "Servo": { + "extends": "Dispenser", + "methods": { + "SetAngle": { + "arguments": [{"angle": "number"}] + } + } + }, + "BlackBox": { + "extends": "Part", + "methods": { + "GetLogs": { + "returns": "RegionLog" + } + } + } +} \ No newline at end of file diff --git a/scripts/generateDefinitions.py b/scripts/generateDefinitions.py new file mode 100644 index 0000000..86423a8 --- /dev/null +++ b/scripts/generateDefinitions.py @@ -0,0 +1,54 @@ +import os +import sys +import json +from generatePartDefinitions import PartDefinitionsGenerator + +class DefinitionsGenerator(): + pass + +def main(): + if len(sys.argv) < 2: + print("Error: Project directory must be passed in as a command-line argument.") + exit(1) + project = sys.argv[1] + globals_file = os.path.join(project, "globals.d.lua") + types_file = os.path.join(project, "types.d.lua") + parts_file = os.path.join(project, "parts.json") + output_file = os.path.join(project, "build", "pilot.d.lua") + content = "" + # Generate types + with open(types_file, 'r') as fr: + content += fr.read().strip() + "\n" + # Get parts data + parts_data = None + with open(parts_file, 'r') as fr: + parts_data = json.load(fr) + # Generate parts + part_generator = PartDefinitionsGenerator(parts_data) + content += "-- Part Types\n" + content += part_generator.generate().strip() + "\n" + # Generate globals + with open(globals_file, 'r') as fr: + content += fr.read().strip() + "\n" + # Generate port globals + content += "-- Port-related microcontroller globals\n" + single_overloads = [] + multi_overloads = [] + for part, _ in parts_data.items(): + single_overloads.append(f"((port: PortLike, partType: \"{part}\") -> {part})") + multi_overloads.append(f"((port: PortLike, partType: \"{part}\") -> {{{part}}})") + single_overloads.append(f"((port: PortLike, partType: string) -> {part})") + multi_overloads.append(f"((port: PortLike, partType: string) -> {{{part}}})") + sep = '\n & ' + content += f"declare GetPartFromPort: {sep.join(single_overloads)}\n" + content += f"declare GetPartsFromPort: {sep.join(single_overloads)}\n" + # Finalize + content = content.strip().replace("\n\n", "\n") + "\n" + if not os.path.isdir(os.path.join(project, "build")): + os.makedirs(os.path.join(project, "build")) + with open(output_file, 'w+') as fw: + fw.write(content) + print(f"{output_file} created successfully") + +if __name__ == "__main__": + main() diff --git a/scripts/generatePartDefinitions.py b/scripts/generatePartDefinitions.py new file mode 100644 index 0000000..96967a7 --- /dev/null +++ b/scripts/generatePartDefinitions.py @@ -0,0 +1,107 @@ +import os +import sys +import json + +def _parse_list_dict(listdict: list) -> list[tuple]: + data = [] + for entry in listdict: + entry: dict = entry + if len(entry.items()) != 1: + raise Exception(f"Expected exactly one entry: {entry}") + data.append(list(entry.items())[0]) + return data + +class PartDefinitionsGenerator(): + def __init__(self, data: dict): + self.data = data + self.parts = {} + self.default_part_name = None + + def _get_part(self, name: str) -> dict | None: + part = self.data.get(name) + if part == None: + return None + if not part.get("_generated", False): + self._generate_and_store_part(part) + return part + + def _generate_part(self, part: dict): + code = [] + if "extends" in part: + super_part = self._get_part(part["extends"]) + if super_part == None: + raise Exception(f"Unknown part in extends clause: {part['extends']}") + for key in ["methods", "properties", "events"]: + if not key in part: + part.update({key: {}}) + part[key].update(super_part.get(key, {})) + if part.get("default", False) == True: + if self.default_part_name: + raise Exception(f"{part['_name']} and {self.default_part_name} cannot both be the default part") + self.default_part_name = part["_name"] + for method_name, method in part.get("methods", {}).items(): + returns = method.get("returns", "()") + arguments: list = method.get("arguments", []) + arguments_code = [] + for arg_name, arg_type in _parse_list_dict(arguments): + arguments_code.append(f"{arg_name}: {arg_type}") + code.append(f"{method_name}: ({', '.join(arguments_code)}) -> {returns}") + for property_name, property_value in part.get("properties", {}).items(): + code.append(f"{property_name}: {property_value}") + i = -1 + event_code = "" + for event_name, event in part.get("events", {}).items(): + i += 1 + event_arguments_code = [] + for arg_name, arg_type in _parse_list_dict(event): + event_arguments_code.append(f"{arg_name}: {arg_type}") + event_name = f"\"{event_name}\"" + if i == 0: + event_code += f"Connect: ((self: {part['_name']}, event: {event_name}, callback: ({', '.join(event_arguments_code)}) -> ()) -> EventConnection)" + else: + event_code += f"\n & ((self: {part['_name']}, event: {event_name}, callback: ({', '.join(event_arguments_code)}) -> ()) -> EventConnection)" + if i == -1: + raise Exception(f"{part['_name']} has no events? {part}") + code.append(event_code) + code_sep = f",\n " + return f"type {part['_name']} = {{\n {code_sep.join(code)}\n}}" + + def _generate_and_store_part(self, part: dict): + if self.parts.get(part['_name']): return self.parts[part['_name']] + generated_part = self._generate_part(part) + self.parts[part['_name']] = generated_part + return generated_part + + def generate(self): + self.parts = {} + for part_name, part in self.data.items(): + part.update({ "_name": part_name }) + self._generate_and_store_part(part) + return f"\n".join(list(self.parts.values())) + +def main(): + if len(sys.argv) < 2: + print("Error: parts.json must be passed in as a command-line argument.") + exit(1) + input_file = sys.argv[1] + output_file = os.path.splitext(input_file)[0] + ".d.lua" + try: + with open(input_file, 'r') as fr: + parts = json.load(fr) + part_definitions_generator = PartDefinitionsGenerator(parts) + part_definitions = part_definitions_generator.generate() + with open(output_file, 'w+') as fw: + fw.write("--!nocheck\n" + part_definitions) + print(f"{output_file} created successfully") + except FileNotFoundError: + print(f"Error: File not found: {input_file}") + exit(1) + except json.JSONDecodeError as e: + print(f"Error: Unable to parse JSON: {e.msg}") + exit(1) + except Exception as e: + print(f"Error: Failed to generate.", end=" ") + raise e + +if __name__ == "__main__": + main() diff --git a/types.d.lua b/types.d.lua new file mode 100644 index 0000000..0bc2a06 --- /dev/null +++ b/types.d.lua @@ -0,0 +1,39 @@ +-- Utility Types +type PortLike = number | {GUID: string} +type Properties = {[string]: any} +type Iterator = () -> (K, V) + +-- Microcontroller Types +type EventConnection = { + Unbind: (self: EventConnection) -> () +} +type ScreenObject = { + ChangeProperties: (self: ScreenObject, properties: Properties) -> (), + AddChild: (self: ScreenObject, child: ScreenObject) -> (), + Destroy: (self: ScreenObject) -> () +} +type Cursor = { + X: number, + Y: number, + Player: string, + Pressed: boolean +} +type RegionInfo = { + Name: string, + Type: "Planet" | "Star", + SubType: string, + Color: Color3, + TidallyLocked: boolean, + Resources: {string}, + Gravity: number, + Temperature: number, + BeaconCount: number, + HasRings: boolean +} +type RegionLog = { + { + Event: "HyperDrive" | "Aliens" | "Spawned" | "Death" | "ExitRegion" | "Poison" | "Irradiated" | "Suffocating" | "Freezing" | "Melting", + Desc: string, + TimeAgo: number + } +}