├── ByondSharp.Samples ├── Models │ └── BYONDUserData.cs ├── TimerSample.cs ├── ByondSharp.Samples.csproj ├── SmallSamples.cs ├── BYONDDataService.cs ├── Discord │ └── DiscordSample.cs ├── Deferred │ └── Timers.cs └── Util │ └── PriorityQueue.cs ├── ByondSharp ├── FFI │ ├── ByondFFIAttribute.cs │ ├── ByondResponse.cs │ └── ByondFFI.cs ├── ByondSharp.csproj └── Deferred │ └── TaskManager.cs ├── ByondSharpGenerator ├── ByondSharpGenerator.csproj ├── GlobalSuppressions.cs └── FFIGenerator.cs ├── LICENSE.md ├── ByondSharp.sln ├── README.md └── .gitignore /ByondSharp.Samples/Models/BYONDUserData.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ByondSharp.Samples.Models; 4 | 5 | public class BYONDUserData 6 | { 7 | public string Key { get; set; } 8 | public string CKey { get; set; } 9 | public string Gender { get; set; } 10 | public DateTime Joined { get; set; } 11 | public bool IsMember { get; set; } 12 | } -------------------------------------------------------------------------------- /ByondSharp/FFI/ByondFFIAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ByondSharp.FFI; 4 | 5 | /// 6 | /// Static methods decorated with this attribute will be compiled into BYOND-callable methods 7 | /// 8 | [AttributeUsage(AttributeTargets.Method)] 9 | public class ByondFFIAttribute : Attribute 10 | { 11 | public bool Deferrable = false; 12 | } -------------------------------------------------------------------------------- /ByondSharpGenerator/ByondSharpGenerator.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | 6 | 7 | 8 | AnyCPU 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /ByondSharp.Samples/TimerSample.cs: -------------------------------------------------------------------------------- 1 | using ByondSharp.FFI; 2 | using System.Diagnostics; 3 | 4 | namespace ByondSharp.Samples; 5 | 6 | /// 7 | /// Stateful example of operations possible with ByondSharp. With these two calls, one can maintain 8 | /// a stopwatch and keep track of the passing of time. 9 | /// 10 | public class TimerSample 11 | { 12 | private static Stopwatch _sw; 13 | 14 | [ByondFFI] 15 | public static void StartStopwatch() 16 | { 17 | if (_sw is not null) 18 | { 19 | return; 20 | } 21 | 22 | _sw = new Stopwatch(); 23 | _sw.Start(); 24 | } 25 | 26 | [ByondFFI] 27 | public static string GetStopwatchStatus() => _sw.Elapsed.ToString(); 28 | } -------------------------------------------------------------------------------- /ByondSharp/FFI/ByondResponse.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.Json; 3 | using System.Text.Json.Serialization; 4 | 5 | namespace ByondSharp.FFI; 6 | 7 | public enum ResponseCode 8 | { 9 | Unknown, 10 | Success, 11 | Error, 12 | Deferred 13 | } 14 | 15 | public struct ByondResponse 16 | { 17 | private static readonly JsonSerializerOptions JsonSerializerConfig = new JsonSerializerOptions() { IncludeFields = true }; 18 | 19 | public ResponseCode ResponseCode; 20 | [JsonIgnore] 21 | public Exception _Exception; 22 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 23 | public string Exception => _Exception?.ToString(); 24 | public string Data; 25 | 26 | public override string ToString() => JsonSerializer.Serialize(this, JsonSerializerConfig); 27 | } -------------------------------------------------------------------------------- /ByondSharp/ByondSharp.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | true 6 | AnyCPU;x86 7 | win-x86 8 | OnBuildSuccess 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Brett Williams 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, 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, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /ByondSharp/FFI/ByondFFI.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | 4 | namespace ByondSharp.FFI; 5 | 6 | /// 7 | /// Facilitates the return of data to BYOND through the FFI 8 | /// 9 | public class ByondFFI 10 | { 11 | /// 12 | /// Pointer to the last string sent to BYOND 13 | /// 14 | /// 15 | /// This is very important in reducing memory leaks. We will deallocate this string in every following call. 16 | /// 17 | private static IntPtr _returnString; 18 | 19 | /// 20 | /// Generates a response for BYOND, specifically a pointer to a c string 21 | /// 22 | /// The string to be returned to BYOND 23 | /// A pointer to the location of the c string in the heap 24 | /// This MUST be properly freed else it will create a memory leak 25 | private static IntPtr FFIReturn(string s) 26 | { 27 | if (_returnString != IntPtr.Zero) 28 | { 29 | Marshal.FreeHGlobal(_returnString); 30 | } 31 | _returnString = Marshal.StringToHGlobalAnsi(s); 32 | return _returnString; 33 | } 34 | 35 | public static IntPtr FFIReturn(ByondResponse response) => FFIReturn(response.ToString()); 36 | } -------------------------------------------------------------------------------- /ByondSharp.Samples/ByondSharp.Samples.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | true 6 | AnyCPU;x86 7 | win-x86 8 | OnBuildSuccess 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /ByondSharp.Samples/SmallSamples.cs: -------------------------------------------------------------------------------- 1 | using ByondSharp.FFI; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | namespace ByondSharp.Samples; 8 | 9 | /// 10 | /// Brief examples for use of ByondSharp. 11 | /// 12 | public class SmallSamples 13 | { 14 | [ByondFFI] 15 | public static string RepeatMe(List args) => $"You said '{string.Join(", ", args)}'"; 16 | 17 | [ByondFFI] 18 | public static void DoNothing() 19 | { 20 | // We do nothing! 21 | } 22 | 23 | [ByondFFI] 24 | public static string DoNothingButReturnString() => "You did it!"; 25 | 26 | [ByondFFI(Deferrable = true)] 27 | public static async Task GetBYONDUserAsync(List args) 28 | { 29 | if (args.Count == 0) 30 | return null; 31 | 32 | var ds = new BYONDDataService(); 33 | var data = await ds.GetUserData(args[0], CancellationToken.None); 34 | return $"CKey: {data.CKey} -- Key: {data.Key} -- Joined: {data.Joined} -- Is member: {data.IsMember} -- Gender: {data.Gender}"; 35 | } 36 | 37 | [ByondFFI] 38 | public static string AttachDebugger() 39 | { 40 | #if DEBUG 41 | if (!Debugger.IsAttached) 42 | { 43 | Debugger.Launch(); 44 | } 45 | return "Debugger launched."; 46 | #else 47 | return "ByondSharp was built in release mode, the debugger is not available. Please re-build in debug mode."; 48 | #endif 49 | } 50 | } -------------------------------------------------------------------------------- /ByondSharpGenerator/GlobalSuppressions.cs: -------------------------------------------------------------------------------- 1 | // This file is used by Code Analysis to maintain SuppressMessage 2 | // attributes that are applied to this project. 3 | // Project-level suppressions either have no target or are given 4 | // a specific target and scoped to a namespace, type, member, etc. 5 | 6 | using System.Diagnostics.CodeAnalysis; 7 | 8 | [assembly: SuppressMessage("MicrosoftCodeAnalysisReleaseTracking", "RS2008:Enable analyzer release tracking", Justification = "", Scope = "member", Target = "~F:ByondSharpGenerator.FFIGenerator.NonStaticMethodError")] 9 | [assembly: SuppressMessage("MicrosoftCodeAnalysisReleaseTracking", "RS2008:Enable analyzer release tracking", Justification = "", Scope = "member", Target = "~F:ByondSharpGenerator.FFIGenerator.InvalidReturnTypeError")] 10 | [assembly: SuppressMessage("MicrosoftCodeAnalysisReleaseTracking", "RS2008:Enable analyzer release tracking", Justification = "", Scope = "member", Target = "~F:ByondSharpGenerator.FFIGenerator.TooManyParametersError")] 11 | [assembly: SuppressMessage("MicrosoftCodeAnalysisReleaseTracking", "RS2008:Enable analyzer release tracking", Justification = "", Scope = "member", Target = "~F:ByondSharpGenerator.FFIGenerator.InvalidParameterError")] 12 | [assembly: SuppressMessage("MicrosoftCodeAnalysisReleaseTracking", "RS2008:Enable analyzer release tracking", Justification = "", Scope = "member", Target = "~F:ByondSharpGenerator.FFIGenerator.InvalidVisibilityError")] 13 | [assembly: SuppressMessage("MicrosoftCodeAnalysisReleaseTracking", "RS2008:Enable analyzer release tracking", Justification = "", Scope = "member", Target = "~F:ByondSharpGenerator.FFIGenerator.CannotDeferSyncMethod")] 14 | -------------------------------------------------------------------------------- /ByondSharp/Deferred/TaskManager.cs: -------------------------------------------------------------------------------- 1 | using ByondSharp.FFI; 2 | using System; 3 | using System.Collections.Concurrent; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | 9 | namespace ByondSharp.Deferred; 10 | 11 | public class TaskManager 12 | { 13 | private static ulong _currentId = 0; 14 | private static ConcurrentDictionary> Jobs { get; } = new ConcurrentDictionary>(); 15 | 16 | [ByondFFI] 17 | public static string PollJobs() 18 | { 19 | return string.Join(";", Jobs.Where(x => x.Value.IsCompleted).Select(x => x.Key)); 20 | } 21 | 22 | [ByondFFI] 23 | public static string GetResult(List args) 24 | { 25 | if (args.Count == 0) 26 | return null; 27 | 28 | if (!ulong.TryParse(args[0], out var id)) 29 | { 30 | throw new Exception($"Could not parse {args[0]} as a valid job ID, is this really a ulong?"); 31 | } 32 | 33 | if (!Jobs.TryRemove(id, out var job)) 34 | { 35 | throw new Exception($"ID {id} not present in TaskManager, was this job already collected?"); 36 | } 37 | 38 | if (!job.IsCompleted) 39 | { 40 | throw new Exception($"Cannot collect job results, job {id} is not yet complete."); 41 | } 42 | 43 | return job.Result; 44 | } 45 | 46 | public static ulong RunTask(Task job) 47 | { 48 | if (job.Status == TaskStatus.Created) 49 | job.Start(); 50 | var id = Interlocked.Increment(ref _currentId); 51 | if (!Jobs.TryAdd(id, job)) 52 | { 53 | throw new Exception($"Attempted to schedule job {id}, but the ID was already used for scheduling. Please report this!"); 54 | } 55 | return id; 56 | } 57 | } -------------------------------------------------------------------------------- /ByondSharp.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.30804.86 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ByondSharp", "ByondSharp\ByondSharp.csproj", "{2922CA40-2C23-419C-B469-77F3DBB26DDA}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ByondSharpGenerator", "ByondSharpGenerator\ByondSharpGenerator.csproj", "{BF29EF68-FD3D-4A89-928E-C810F01E3FB0}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ByondSharp.Samples", "ByondSharp.Samples\ByondSharp.Samples.csproj", "{B031B1E0-68C7-4847-AB4D-ACADDDADF14B}" 11 | EndProject 12 | Global 13 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 14 | Debug|Any CPU = Debug|Any CPU 15 | Debug|x86 = Debug|x86 16 | Release|Any CPU = Release|Any CPU 17 | Release|x86 = Release|x86 18 | EndGlobalSection 19 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 20 | {2922CA40-2C23-419C-B469-77F3DBB26DDA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {2922CA40-2C23-419C-B469-77F3DBB26DDA}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {2922CA40-2C23-419C-B469-77F3DBB26DDA}.Debug|x86.ActiveCfg = Debug|x86 23 | {2922CA40-2C23-419C-B469-77F3DBB26DDA}.Debug|x86.Build.0 = Debug|x86 24 | {2922CA40-2C23-419C-B469-77F3DBB26DDA}.Release|Any CPU.ActiveCfg = Release|Any CPU 25 | {2922CA40-2C23-419C-B469-77F3DBB26DDA}.Release|Any CPU.Build.0 = Release|Any CPU 26 | {2922CA40-2C23-419C-B469-77F3DBB26DDA}.Release|x86.ActiveCfg = Release|x86 27 | {2922CA40-2C23-419C-B469-77F3DBB26DDA}.Release|x86.Build.0 = Release|x86 28 | {BF29EF68-FD3D-4A89-928E-C810F01E3FB0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {BF29EF68-FD3D-4A89-928E-C810F01E3FB0}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {BF29EF68-FD3D-4A89-928E-C810F01E3FB0}.Debug|x86.ActiveCfg = Debug|Any CPU 31 | {BF29EF68-FD3D-4A89-928E-C810F01E3FB0}.Debug|x86.Build.0 = Debug|Any CPU 32 | {BF29EF68-FD3D-4A89-928E-C810F01E3FB0}.Release|Any CPU.ActiveCfg = Release|Any CPU 33 | {BF29EF68-FD3D-4A89-928E-C810F01E3FB0}.Release|Any CPU.Build.0 = Release|Any CPU 34 | {BF29EF68-FD3D-4A89-928E-C810F01E3FB0}.Release|x86.ActiveCfg = Release|Any CPU 35 | {BF29EF68-FD3D-4A89-928E-C810F01E3FB0}.Release|x86.Build.0 = Release|Any CPU 36 | {B031B1E0-68C7-4847-AB4D-ACADDDADF14B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 37 | {B031B1E0-68C7-4847-AB4D-ACADDDADF14B}.Debug|Any CPU.Build.0 = Debug|Any CPU 38 | {B031B1E0-68C7-4847-AB4D-ACADDDADF14B}.Debug|x86.ActiveCfg = Debug|Any CPU 39 | {B031B1E0-68C7-4847-AB4D-ACADDDADF14B}.Debug|x86.Build.0 = Debug|Any CPU 40 | {B031B1E0-68C7-4847-AB4D-ACADDDADF14B}.Release|Any CPU.ActiveCfg = Release|Any CPU 41 | {B031B1E0-68C7-4847-AB4D-ACADDDADF14B}.Release|Any CPU.Build.0 = Release|Any CPU 42 | {B031B1E0-68C7-4847-AB4D-ACADDDADF14B}.Release|x86.ActiveCfg = Release|Any CPU 43 | {B031B1E0-68C7-4847-AB4D-ACADDDADF14B}.Release|x86.Build.0 = Release|Any CPU 44 | EndGlobalSection 45 | GlobalSection(SolutionProperties) = preSolution 46 | HideSolutionNode = FALSE 47 | EndGlobalSection 48 | GlobalSection(ExtensibilityGlobals) = postSolution 49 | SolutionGuid = {F942FBE9-190E-416F-BBCF-AEC781744B01} 50 | EndGlobalSection 51 | EndGlobal 52 | -------------------------------------------------------------------------------- /ByondSharp.Samples/BYONDDataService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using ByondSharp.Samples.Models; 6 | using Microsoft.Toolkit.HighPerformance; 7 | using RestSharp; 8 | 9 | namespace ByondSharp.Samples; 10 | 11 | /// 12 | /// This is provided as an example for external library use, with RestSharp, as well as an async method call. This is a sample adapted from the Scrubby parsing server. 13 | /// 14 | public class BYONDDataService 15 | { 16 | private const string BaseUrl = "https://www.byond.com/"; 17 | private readonly RestClient _client; 18 | 19 | public BYONDDataService() 20 | { 21 | _client = new RestClient(BaseUrl); 22 | } 23 | 24 | public async Task GetUserData(string key, CancellationToken cancellationToken) 25 | { 26 | var request = new RestRequest($"members/{key}").AddQueryParameter("format", "text"); 27 | var response = await _client.ExecuteAsync(request, cancellationToken); 28 | 29 | if (response.StatusCode != HttpStatusCode.OK) 30 | { 31 | return null; 32 | } 33 | 34 | if (response.Content.Contains("not found.")) 35 | { 36 | throw new BYONDUserNotFoundException($"User {key} not found."); 37 | } 38 | 39 | if (response.Content.Contains("is not active.")) 40 | { 41 | throw new BYONDUserInactiveException($"User {key} is not active."); 42 | } 43 | 44 | return ParseResponse(response.Content.AsSpan()); 45 | } 46 | 47 | private static BYONDUserData ParseResponse(ReadOnlySpan data) 48 | { 49 | var toReturn = new BYONDUserData(); 50 | 51 | foreach(var line in data.Tokenize('\n')) 52 | { 53 | if (line.StartsWith("general")) 54 | { 55 | continue; 56 | } 57 | 58 | var lineContext = line.Trim("\r\t "); 59 | var equalsLoc = lineContext.IndexOf('='); 60 | if (equalsLoc == -1) 61 | continue; 62 | 63 | var key = lineContext[..equalsLoc].Trim(); 64 | var value = lineContext[(equalsLoc + 1)..].Trim("\" "); 65 | if (key.Equals("key", StringComparison.OrdinalIgnoreCase)) 66 | { 67 | toReturn.Key = value.ToString(); 68 | } 69 | else if (key.Equals("ckey", StringComparison.OrdinalIgnoreCase)) 70 | { 71 | toReturn.CKey = value.ToString(); 72 | } 73 | else if (key.Equals("gender", StringComparison.OrdinalIgnoreCase)) 74 | { 75 | toReturn.Gender = value.ToString(); 76 | } 77 | else if (key.Equals("joined", StringComparison.OrdinalIgnoreCase)) 78 | { 79 | toReturn.Joined = DateTime.SpecifyKind(DateTime.Parse(value), DateTimeKind.Utc); 80 | } 81 | else if (key.Equals("is_member", StringComparison.OrdinalIgnoreCase)) 82 | { 83 | toReturn.IsMember = value.Equals("1", StringComparison.OrdinalIgnoreCase); 84 | } 85 | } 86 | 87 | return toReturn; 88 | } 89 | } 90 | 91 | public class BYONDUserNotFoundException : Exception 92 | { 93 | public BYONDUserNotFoundException(string message) : base(message) { } 94 | } 95 | 96 | public class BYONDUserInactiveException : Exception 97 | { 98 | public BYONDUserInactiveException(string message) : base(message) { } 99 | } -------------------------------------------------------------------------------- /ByondSharp.Samples/Discord/DiscordSample.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Threading.Tasks; 4 | using ByondSharp.FFI; 5 | using Discord; 6 | using Discord.Commands; 7 | using Discord.WebSocket; 8 | using Microsoft.Extensions.DependencyInjection; 9 | 10 | namespace ByondSharp.Samples.Discord; 11 | 12 | public class DiscordSample 13 | { 14 | private static DiscordSocketClient _client; 15 | 16 | [ByondFFI] 17 | public static async Task InitializeDiscord() 18 | { 19 | // Do not re-initialize 20 | if (_client != null) 21 | return; 22 | 23 | _client = new DiscordSocketClient(); 24 | await _client.LoginAsync(TokenType.Bot, "your-token-here"); 25 | await _client.StartAsync(); 26 | 27 | var services = new ServiceCollection() 28 | .AddSingleton(_client) 29 | .AddSingleton() 30 | .BuildServiceProvider(); 31 | 32 | var commandService = services.GetRequiredService(); 33 | var commandHandler = new CommandHandler(services, commandService, _client); 34 | await commandHandler.InitializeAsync(); 35 | } 36 | 37 | [ByondFFI] 38 | public static string GetDiscordMessage() => RepeaterModule.IncomingMessages.TryDequeue(out var result) ? result : null; 39 | } 40 | 41 | public class CommandHandler 42 | { 43 | private readonly DiscordSocketClient _client; 44 | private readonly CommandService _commands; 45 | private readonly IServiceProvider _services; 46 | 47 | public CommandHandler(IServiceProvider services, CommandService commands, DiscordSocketClient client) 48 | { 49 | _commands = commands; 50 | _services = services; 51 | _client = client; 52 | } 53 | 54 | public async Task InitializeAsync() 55 | { 56 | // Pass the service provider to the second parameter of 57 | // AddModulesAsync to inject dependencies to all modules 58 | // that may require them. 59 | await _commands.AddModuleAsync(_services); 60 | _client.MessageReceived += HandleCommandAsync; 61 | } 62 | 63 | public async Task HandleCommandAsync(SocketMessage messageParam) 64 | { 65 | // Don't process the command if it was a system message 66 | if (!(messageParam is SocketUserMessage message)) return; 67 | 68 | // Create a number to track where the prefix ends and the command begins 69 | int argPos = 0; 70 | 71 | // Determine if the message is a command based on the prefix and make sure no bots trigger commands 72 | if (!(message.HasCharPrefix('!', ref argPos) || 73 | message.HasMentionPrefix(_client.CurrentUser, ref argPos)) || 74 | message.Author.IsBot) 75 | return; 76 | 77 | // Create a WebSocket-based command context based on the message 78 | var context = new SocketCommandContext(_client, message); 79 | 80 | // Execute the command with the command context we just 81 | // created, along with the service provider for precondition checks. 82 | await _commands.ExecuteAsync( 83 | context: context, 84 | argPos: argPos, 85 | services: _services); 86 | } 87 | } 88 | 89 | public class RepeaterModule : ModuleBase 90 | { 91 | public static ConcurrentQueue IncomingMessages = new ConcurrentQueue(); 92 | 93 | [Command("repeat")] 94 | public async Task RepeatMessage([Remainder] string message) 95 | { 96 | IncomingMessages.Enqueue($"{Context.User.Username}#{Context.User.DiscriminatorValue:0000}: {message}"); 97 | await ReplyAsync("Sent message!"); 98 | } 99 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ByondSharp 2 | 3 | ByondSharp is a C# library providing easy interop with BYOND (DreamMaker). 4 | 5 | It abstracts away the complexity of handling FFI with BYOND, and makes it easy for developers with any level of experience to create meaningful interop with BYOND from C#. 6 | 7 | ### Use 8 | 9 | ByondSharp works by allowing a developer to write 'normal' C# code, without references to pointers or much consideration for the FFI aspect of their code, and tag their methods they need to export to BYOND with an attribute, ``ByondFFI``. Once tagged the method will be wrapped using source generation and exposed to CDecl calls, making it easily called from BYOND. 10 | 11 | Important things you will need to run ByondSharp in BYOND: 12 | - [.NET 6.0 or greater runtimes](https://dotnet.microsoft.com/download/dotnet/6.0) 13 | 14 | That's it. Really. 15 | 16 | Once you have .NET 6.0 or greater runtimes, writing the code is pretty straight forward. I would recommend looking at the [samples](https://github.com/bobbahbrown/ByondSharp/blob/master/ByondSharp.Samples), especially the [timer sample](https://github.com/bobbahbrown/ByondSharp/blob/master/ByondSharp.Samples/Deferred/Timers.cs), for an essential introduction to the format of these functions. 17 | 18 | At a bare minimum, exported functions must: 19 | - Have the ``ByondFFI`` attribute 20 | - Be ``public`` and ``static`` methods 21 | - Return ``void``, ``string``, or when async return ``Task`` or ``Task`` 22 | - Have zero arguments, or have one argument which is a ``List`` 23 | 24 | Aside from that you're free to do as you please. 25 | 26 | I would highly recommend using the content of the ByondSharp project as the basis for your own project. By not doing this you may miss out on the functionality of the ``TaskManager``. The code included in the sample project (``ByondSharp.Samples``), are just samples and can be ignored. 27 | 28 | See below for the timer sample as a point of reference. 29 | 30 | ```csharp 31 | namespace ByondSharp; 32 | 33 | /// 34 | /// Stateful example of operations possible with ByondSharp. With these two calls, one can maintain 35 | /// a stopwatch and keep track of the passing of time. 36 | /// 37 | public class TimerSample 38 | { 39 | private static Stopwatch _sw; 40 | 41 | [ByondFFI] 42 | public static void StartStopwatch() 43 | { 44 | if (_sw is not null) 45 | { 46 | return; 47 | } 48 | 49 | _sw = new Stopwatch(); 50 | _sw.Start(); 51 | } 52 | 53 | [ByondFFI] 54 | public static string GetStopwatchStatus() 55 | { 56 | return _sw.Elapsed.ToString(); 57 | } 58 | } 59 | ``` 60 | 61 | ### Deferrable Behaviour 62 | 63 | Using the ByondSharp solution, it is possible to add the ``Deferrable`` boolean named argument to any ``ByondFFIAttribute`` on an async method. This will generate an additional version of the method's export, with ``Deferred`` added as a suffix to the method's name. If the method has a return value, the deferred method will return a ``ulong`` id, which is an internal scheduled id for this job. You can then poll the ``TaskManager`` using the ``PollJobs`` exported function to get a semicolon separated list of completed jobs. Once the job appears in this list, you can retrieve the result and remove it from the task manager by calling the ``GetResult`` exported function, with the job id as the only argument. By doing this you accomplish two things: you can run tasks on multiple threads, as is the default behaviour of async tasks, and as well as this you can avoid blocking BYOND's thread waiting for a result from this external call. 64 | 65 | ### How to build 66 | 67 | Knowing how to run it and the brief rules for writing code is not very useful without being able to build the library, so how is it done? 68 | 69 | Quite simple: in Visual Studio, you will build the ByondSharp project in debug or release configuration **for x86 CPUs** (BYOND is a 32-bit application, a 64-bit compiled DLL will not do much!) 70 | 71 | Once this is done, you will generate several files within your ``ByondSharp\bin\x86\[Debug/Release]\net6.0\win-x86\copy_to_byond`` directory. Copy these to the location in which they will be referenced from BYOND. So long as no unanticipated files are generated during compilation (namely non-DLLs) then these should be the only files you require. 72 | 73 | All other files generated outside this folder can be discarded, but the aforementioned files __must__ be present in a directory that BYOND can access. 74 | 75 | ### Using in BYOND 76 | 77 | To use external DLLs in BYOND, simply use the ``call()()`` proc. For example: 78 | 79 | ```dm 80 | #define byondsharp_repeatme(options) call("byondsharpNE", "RepeatMe")(options) 81 | #define byondsharp_startstopwatch(options) call("byondsharpNE", "StartStopwatch")(options) 82 | #define byondsharp_getstopwatchstatus(options) call("byondsharpNE", "GetStopwatchStatus")(options) 83 | 84 | /world/New() 85 | byondsharp_startstopwatch(null) 86 | while (1) 87 | var/timelog = byondsharp_getstopwatchstatus(null) 88 | world.log << timelog 89 | world.log << byondsharp_repeatme(timelog) 90 | sleep(1) 91 | ``` 92 | 93 | ### What about Linux? 94 | 95 | As dotnet does not natively support Linux x86, you'll have to use a patched version of dotnet 6/7. 96 | 97 | https://github.com/Servarr/dotnet-linux-x86 98 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Aa][Rr][Mm]/ 27 | [Aa][Rr][Mm]64/ 28 | bld/ 29 | [Bb]in/ 30 | [Oo]bj/ 31 | [Ll]og/ 32 | 33 | # Visual Studio 2015/2017 cache/options directory 34 | .vs/ 35 | # Uncomment if you have tasks that create the project's static files in wwwroot 36 | #wwwroot/ 37 | 38 | # Visual Studio 2017 auto generated files 39 | Generated\ Files/ 40 | 41 | # MSTest test Results 42 | [Tt]est[Rr]esult*/ 43 | [Bb]uild[Ll]og.* 44 | 45 | # NUnit 46 | *.VisualState.xml 47 | TestResult.xml 48 | nunit-*.xml 49 | 50 | # Build Results of an ATL Project 51 | [Dd]ebugPS/ 52 | [Rr]eleasePS/ 53 | dlldata.c 54 | 55 | # Benchmark Results 56 | BenchmarkDotNet.Artifacts/ 57 | 58 | # .NET Core 59 | project.lock.json 60 | project.fragment.lock.json 61 | artifacts/ 62 | 63 | # StyleCop 64 | StyleCopReport.xml 65 | 66 | # Files built by Visual Studio 67 | *_i.c 68 | *_p.c 69 | *_h.h 70 | *.ilk 71 | *.meta 72 | *.obj 73 | *.iobj 74 | *.pch 75 | *.pdb 76 | *.ipdb 77 | *.pgc 78 | *.pgd 79 | *.rsp 80 | *.sbr 81 | *.tlb 82 | *.tli 83 | *.tlh 84 | *.tmp 85 | *.tmp_proj 86 | *_wpftmp.csproj 87 | *.log 88 | *.vspscc 89 | *.vssscc 90 | .builds 91 | *.pidb 92 | *.svclog 93 | *.scc 94 | 95 | # Chutzpah Test files 96 | _Chutzpah* 97 | 98 | # Visual C++ cache files 99 | ipch/ 100 | *.aps 101 | *.ncb 102 | *.opendb 103 | *.opensdf 104 | *.sdf 105 | *.cachefile 106 | *.VC.db 107 | *.VC.VC.opendb 108 | 109 | # Visual Studio profiler 110 | *.psess 111 | *.vsp 112 | *.vspx 113 | *.sap 114 | 115 | # Visual Studio Trace Files 116 | *.e2e 117 | 118 | # TFS 2012 Local Workspace 119 | $tf/ 120 | 121 | # Guidance Automation Toolkit 122 | *.gpState 123 | 124 | # ReSharper is a .NET coding add-in 125 | _ReSharper*/ 126 | *.[Rr]e[Ss]harper 127 | *.DotSettings.user 128 | 129 | # JustCode is a .NET coding add-in 130 | .JustCode 131 | 132 | # TeamCity is a build add-in 133 | _TeamCity* 134 | 135 | # DotCover is a Code Coverage Tool 136 | *.dotCover 137 | 138 | # AxoCover is a Code Coverage Tool 139 | .axoCover/* 140 | !.axoCover/settings.json 141 | 142 | # Visual Studio code coverage results 143 | *.coverage 144 | *.coveragexml 145 | 146 | # NCrunch 147 | _NCrunch_* 148 | .*crunch*.local.xml 149 | nCrunchTemp_* 150 | 151 | # MightyMoose 152 | *.mm.* 153 | AutoTest.Net/ 154 | 155 | # Web workbench (sass) 156 | .sass-cache/ 157 | 158 | # Installshield output folder 159 | [Ee]xpress/ 160 | 161 | # DocProject is a documentation generator add-in 162 | DocProject/buildhelp/ 163 | DocProject/Help/*.HxT 164 | DocProject/Help/*.HxC 165 | DocProject/Help/*.hhc 166 | DocProject/Help/*.hhk 167 | DocProject/Help/*.hhp 168 | DocProject/Help/Html2 169 | DocProject/Help/html 170 | 171 | # Click-Once directory 172 | publish/ 173 | 174 | # Publish Web Output 175 | *.[Pp]ublish.xml 176 | *.azurePubxml 177 | # Note: Comment the next line if you want to checkin your web deploy settings, 178 | # but database connection strings (with potential passwords) will be unencrypted 179 | *.pubxml 180 | *.publishproj 181 | 182 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 183 | # checkin your Azure Web App publish settings, but sensitive information contained 184 | # in these scripts will be unencrypted 185 | PublishScripts/ 186 | 187 | # NuGet Packages 188 | *.nupkg 189 | # NuGet Symbol Packages 190 | *.snupkg 191 | # The packages folder can be ignored because of Package Restore 192 | **/[Pp]ackages/* 193 | # except build/, which is used as an MSBuild target. 194 | !**/[Pp]ackages/build/ 195 | # Uncomment if necessary however generally it will be regenerated when needed 196 | #!**/[Pp]ackages/repositories.config 197 | # NuGet v3's project.json files produces more ignorable files 198 | *.nuget.props 199 | *.nuget.targets 200 | 201 | # Microsoft Azure Build Output 202 | csx/ 203 | *.build.csdef 204 | 205 | # Microsoft Azure Emulator 206 | ecf/ 207 | rcf/ 208 | 209 | # Windows Store app package directories and files 210 | AppPackages/ 211 | BundleArtifacts/ 212 | Package.StoreAssociation.xml 213 | _pkginfo.txt 214 | *.appx 215 | *.appxbundle 216 | *.appxupload 217 | 218 | # Visual Studio cache files 219 | # files ending in .cache can be ignored 220 | *.[Cc]ache 221 | # but keep track of directories ending in .cache 222 | !?*.[Cc]ache/ 223 | 224 | # Others 225 | ClientBin/ 226 | ~$* 227 | *~ 228 | *.dbmdl 229 | *.dbproj.schemaview 230 | *.jfm 231 | *.pfx 232 | *.publishsettings 233 | orleans.codegen.cs 234 | 235 | # Including strong name files can present a security risk 236 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 237 | #*.snk 238 | 239 | # Since there are multiple workflows, uncomment next line to ignore bower_components 240 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 241 | #bower_components/ 242 | 243 | # RIA/Silverlight projects 244 | Generated_Code/ 245 | 246 | # Backup & report files from converting an old project file 247 | # to a newer Visual Studio version. Backup files are not needed, 248 | # because we have git ;-) 249 | _UpgradeReport_Files/ 250 | Backup*/ 251 | UpgradeLog*.XML 252 | UpgradeLog*.htm 253 | ServiceFabricBackup/ 254 | *.rptproj.bak 255 | 256 | # SQL Server files 257 | *.mdf 258 | *.ldf 259 | *.ndf 260 | 261 | # Business Intelligence projects 262 | *.rdl.data 263 | *.bim.layout 264 | *.bim_*.settings 265 | *.rptproj.rsuser 266 | *- [Bb]ackup.rdl 267 | *- [Bb]ackup ([0-9]).rdl 268 | *- [Bb]ackup ([0-9][0-9]).rdl 269 | 270 | # Microsoft Fakes 271 | FakesAssemblies/ 272 | 273 | # GhostDoc plugin setting file 274 | *.GhostDoc.xml 275 | 276 | # Node.js Tools for Visual Studio 277 | .ntvs_analysis.dat 278 | node_modules/ 279 | 280 | # Visual Studio 6 build log 281 | *.plg 282 | 283 | # Visual Studio 6 workspace options file 284 | *.opt 285 | 286 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 287 | *.vbw 288 | 289 | # Visual Studio LightSwitch build output 290 | **/*.HTMLClient/GeneratedArtifacts 291 | **/*.DesktopClient/GeneratedArtifacts 292 | **/*.DesktopClient/ModelManifest.xml 293 | **/*.Server/GeneratedArtifacts 294 | **/*.Server/ModelManifest.xml 295 | _Pvt_Extensions 296 | 297 | # Paket dependency manager 298 | .paket/paket.exe 299 | paket-files/ 300 | 301 | # FAKE - F# Make 302 | .fake/ 303 | 304 | # CodeRush personal settings 305 | .cr/personal 306 | 307 | # Python Tools for Visual Studio (PTVS) 308 | __pycache__/ 309 | *.pyc 310 | 311 | # Cake - Uncomment if you are using it 312 | # tools/** 313 | # !tools/packages.config 314 | 315 | # Tabs Studio 316 | *.tss 317 | 318 | # Telerik's JustMock configuration file 319 | *.jmconfig 320 | 321 | # BizTalk build output 322 | *.btp.cs 323 | *.btm.cs 324 | *.odx.cs 325 | *.xsd.cs 326 | 327 | # OpenCover UI analysis results 328 | OpenCover/ 329 | 330 | # Azure Stream Analytics local run output 331 | ASALocalRun/ 332 | 333 | # MSBuild Binary and Structured Log 334 | *.binlog 335 | 336 | # NVidia Nsight GPU debugger configuration file 337 | *.nvuser 338 | 339 | # MFractors (Xamarin productivity tool) working folder 340 | .mfractor/ 341 | 342 | # Local History for Visual Studio 343 | .localhistory/ 344 | 345 | # BeatPulse healthcheck temp database 346 | healthchecksdb 347 | 348 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 349 | MigrationBackup/ 350 | 351 | # Test environment for ByondSharp 352 | TestEnvironment/ -------------------------------------------------------------------------------- /ByondSharp.Samples/Deferred/Timers.cs: -------------------------------------------------------------------------------- 1 | using ByondSharp.FFI; 2 | using ByondSharp.Samples.Util; 3 | using System; 4 | using System.Collections.Concurrent; 5 | using System.Collections.Generic; 6 | using System.Globalization; 7 | using System.Text; 8 | using System.Threading; 9 | using System.Threading.Tasks; 10 | 11 | namespace ByondSharp.Samples.Deferred; 12 | 13 | /// 14 | /// Flags for timers from /tg/station codebase 15 | /// 16 | [Flags] 17 | public enum TimerFlag 18 | { 19 | Unique = (1 << 0), 20 | Override = (1 << 1), 21 | ClientTime = (1 << 2), 22 | Stoppable = (1 << 3), 23 | NoHashWait = (1 << 4), 24 | Loop = (1 << 5) 25 | } 26 | 27 | /// 28 | /// Timer subsystem replacement for /tg/-derived codebases. 29 | /// 30 | public class Timers 31 | { 32 | private static readonly ConcurrentDictionary TimerLookup = new ConcurrentDictionary(); 33 | private static readonly ConcurrentDictionary HashLookup = new ConcurrentDictionary(); 34 | private static readonly PriorityQueue TimersQueue = new PriorityQueue(new ReverseComparer()); 35 | private static readonly PriorityQueue RealtimeTimersQueue = new PriorityQueue(new ReverseComparer()); 36 | private static Timer[] _dispatchedSet; 37 | private static ulong _currentId = 1; 38 | 39 | /// 40 | /// Gets the status string of the timer subsystem for the MC tab 41 | /// 42 | /// A string representing the status of the subsystem 43 | [ByondFFI] 44 | public static string Status() => $"BYOND Timers: {TimersQueue.Count}, RWT: {RealtimeTimersQueue.Count}"; 45 | 46 | /// 47 | /// Fires the subsystem, returning all timers which are to be handled this tick 48 | /// 49 | /// Single item list, the current world.time 50 | /// A semi-colon separated list of timer IDs to run, negative if a looped timer. 51 | [ByondFFI] 52 | public static async Task Fire(List args) 53 | { 54 | var currentTime = float.Parse(args[0]); 55 | var result = new ConcurrentBag(); 56 | var tasks = new Task[2]; 57 | 58 | // Process normal timers 59 | tasks[0] = Task.Run(() => CollectCompletedTimers(TimersQueue, result, currentTime, false)); 60 | 61 | // Process real-time timers 62 | tasks[1] = Task.Run(() => CollectCompletedTimers(RealtimeTimersQueue, result, currentTime, true)); 63 | 64 | await Task.WhenAll(tasks); 65 | _dispatchedSet = result.ToArray(); 66 | 67 | if (_dispatchedSet.Length == 0) 68 | { 69 | return null; 70 | } 71 | 72 | var toReturn = new StringBuilder($"{(_dispatchedSet[0].Flags.HasFlag(TimerFlag.Loop) ? "-" : "")}{_dispatchedSet[0].Id}"); 73 | foreach (var t in _dispatchedSet) 74 | { 75 | toReturn.Append($";{(t.Flags.HasFlag(TimerFlag.Loop) ? "-" : "")}{t.Id}"); 76 | } 77 | return toReturn.ToString(); 78 | } 79 | 80 | /// 81 | /// Collects completed timers from the respective priority queue 82 | /// 83 | /// The priority queue of timers to collect from 84 | /// The ConcurrentBag to store the resulting timers in 85 | /// The current world.time 86 | /// Boolean operator dictating if these timers are in real-world time 87 | private static void CollectCompletedTimers(PriorityQueue timers, ConcurrentBag result, float currentTime, bool isRealtime) 88 | { 89 | while (timers.Count > 0 && (isRealtime ? timers.Peek().RealWorldTTR <= DateTime.UtcNow : timers.Peek().TimeToRun <= currentTime)) 90 | { 91 | var timer = timers.Take(); 92 | if (timer.Flags.HasFlag(TimerFlag.Loop)) 93 | { 94 | var copy = timer.Copy(); 95 | 96 | // Update looped time 97 | if (isRealtime) 98 | { 99 | copy.RealWorldTTR = DateTime.UtcNow.AddSeconds(copy.Wait / 10); 100 | } 101 | else 102 | { 103 | copy.TimeToRun = currentTime + copy.Wait; 104 | } 105 | 106 | // Keep track of the new copy 107 | timers.Add(copy); 108 | TimerLookup.AddOrUpdate(copy.Id, copy, (_, _) => copy); 109 | if (copy.Hash != null) 110 | HashLookup.AddOrUpdate(copy.Hash, copy, (_, _) => copy); 111 | } 112 | else 113 | { 114 | TimerLookup.Remove(timer.Id, out _); 115 | if (timer.Hash != null) 116 | HashLookup.Remove(timer.Hash, out _); 117 | } 118 | result.Add(timer); 119 | } 120 | } 121 | 122 | /// 123 | /// Reports timers that were not fired in time back to the timer subsystem 124 | /// 125 | /// The first non-fired timer 126 | /// Due to issues with DNNE, this method requires returning a string to avoid memory access violations. 127 | [ByondFFI] 128 | public static string ReportIncompleteTimers(List args) 129 | { 130 | if (_dispatchedSet == null) 131 | return null; 132 | 133 | var lastDispatch = _dispatchedSet.AsSpan(); 134 | var lastId = ulong.Parse(args[0]); 135 | var foundLast = false; 136 | foreach (var timer in lastDispatch) 137 | { 138 | if (!foundLast && timer.Id == lastId) 139 | foundLast = true; 140 | else if (!foundLast) 141 | continue; 142 | 143 | var t = timer; 144 | 145 | // Add to respective queue 146 | var selectedQueue = t.IsRealTime ? RealtimeTimersQueue : TimersQueue; 147 | 148 | // Remove the previously-added looped timer from the queue if we are re-adding the old timer 149 | if (t.Flags.HasFlag(TimerFlag.Loop) && TimerLookup.TryGetValue(t.Id, out var prevTimer)) 150 | { 151 | selectedQueue.Remove(prevTimer); 152 | } 153 | 154 | if (t.Hash != null) 155 | HashLookup.AddOrUpdate(t.Hash, t, (_, _) => t); 156 | TimerLookup.AddOrUpdate(t.Id, t, (_, _) => t); 157 | selectedQueue.Add(t); 158 | } 159 | 160 | return null; 161 | } 162 | 163 | /// 164 | /// Creates a timer from a set of parameters 165 | /// 166 | /// The ID of the created timer, if created, or null otherwise 167 | [ByondFFI] 168 | public static string CreateTimer(List args) 169 | { 170 | var timer = new Timer() 171 | { 172 | Hash = args[0], 173 | Callback = args[1], 174 | TimeToRun = float.Parse(args[2]), 175 | Wait = float.Parse(args[3]), 176 | Source = args[4], 177 | Name = args[5], 178 | Flags = (TimerFlag)int.Parse(args[6]) 179 | }; 180 | 181 | if (timer.Flags.HasFlag(TimerFlag.ClientTime)) 182 | timer.RealWorldTTR = DateTime.UtcNow.AddSeconds(timer.Wait / 10); 183 | var selectedQueue = timer.IsRealTime ? RealtimeTimersQueue : TimersQueue; 184 | 185 | if (timer.Hash != null) 186 | { 187 | if (HashLookup.TryGetValue(timer.Hash, out var prevTimer) && !timer.Flags.HasFlag(TimerFlag.Override)) 188 | { 189 | return null; 190 | } 191 | else if (prevTimer != null && timer.Flags.HasFlag(TimerFlag.Unique)) 192 | { 193 | timer.Id = prevTimer.Id; 194 | selectedQueue.Remove(prevTimer); 195 | } 196 | } 197 | 198 | if (timer.Id == default) 199 | { 200 | timer.Id = Interlocked.Increment(ref _currentId); 201 | } 202 | 203 | selectedQueue.Add(timer); 204 | TimerLookup.AddOrUpdate(timer.Id, timer, (_, _) => timer); 205 | if (timer.Hash != null) 206 | HashLookup.AddOrUpdate(timer.Hash, timer, (_, _) => timer); 207 | return $"{timer.Id}"; 208 | } 209 | 210 | /// 211 | /// Deletes a timer by ID 212 | /// 213 | /// The ID of the timer if found and deleted 214 | [ByondFFI] 215 | public static string DeleteTimerById(List args) 216 | { 217 | var id = ulong.Parse(args[0]); 218 | if (TimerLookup.TryGetValue(id, out var timer) && timer.Flags.HasFlag(TimerFlag.Stoppable)) 219 | { 220 | DequeueTimer(timer); 221 | return $"{timer.Id}"; 222 | } 223 | return null; 224 | } 225 | 226 | /// 227 | /// Deletes a timer by hash 228 | /// 229 | /// The ID of the timer if found and deleted 230 | [ByondFFI] 231 | public static string DeleteTimerByHash(List args) 232 | { 233 | if (HashLookup.TryGetValue(args[0], out var timer) && timer.Flags.HasFlag(TimerFlag.Stoppable)) 234 | { 235 | DequeueTimer(timer); 236 | return $"{timer.Id}"; 237 | } 238 | return null; 239 | } 240 | 241 | /// 242 | /// Gets the remaining time for a timer 243 | /// 244 | /// The time in deciseconds if found, otherwise null 245 | [ByondFFI] 246 | public static string TimeLeft(List args) 247 | { 248 | var worldTime = float.Parse(args[0]); 249 | var id = ulong.Parse(args[1]); 250 | if (TimerLookup.TryGetValue(id, out var timer)) 251 | { 252 | return timer.IsRealTime 253 | ? ((timer.RealWorldTTR.Value - DateTime.UtcNow).TotalSeconds * 10.0).ToString(CultureInfo.InvariantCulture) 254 | : (timer.TimeToRun - worldTime).ToString(CultureInfo.InvariantCulture); 255 | } 256 | return null; 257 | } 258 | 259 | /// 260 | /// Immediately invokes a timer, removing it from the queue and returning the ID to be used to fire the callback. 261 | /// 262 | /// The ID of the timer if found 263 | [ByondFFI] 264 | public static string InvokeImmediately(List args) 265 | { 266 | var id = ulong.Parse(args[0]); 267 | if (TimerLookup.TryGetValue(id, out var timer)) 268 | { 269 | DequeueTimer(timer); 270 | return $"{timer.Id}"; 271 | } 272 | return null; 273 | } 274 | 275 | /// 276 | /// Removes a timer from its respective priority queue, and removes it from the lookup dictionaries. 277 | /// 278 | /// The timer to dequeue 279 | private static void DequeueTimer(Timer timer) 280 | { 281 | TimerLookup.Remove(timer.Id, out _); 282 | if (timer.Hash != null) 283 | HashLookup.Remove(timer.Hash, out _); 284 | var selectedQueue = timer.IsRealTime ? RealtimeTimersQueue : TimersQueue; 285 | selectedQueue.Remove(timer); 286 | } 287 | } 288 | 289 | public record Timer 290 | { 291 | public ulong Id; 292 | public string Hash; 293 | public string Callback; 294 | public float Wait; 295 | public string Source; 296 | public string Name; 297 | public TimerFlag Flags; 298 | public float TimeToRun; 299 | public DateTime? RealWorldTTR; 300 | public bool IsRealTime => RealWorldTTR.HasValue; 301 | public Timer Copy() => (Timer)MemberwiseClone(); 302 | } 303 | 304 | public class ReverseComparer : IComparer 305 | { 306 | public int Compare(Timer x, Timer y) 307 | { 308 | if (x is null || y is null) 309 | return x is null && y is null ? 0 : (x == null ? -1 : 1); 310 | if (x.Flags.HasFlag(TimerFlag.ClientTime)) 311 | return x.RealWorldTTR == y.RealWorldTTR ? 0 : (x.RealWorldTTR > y.RealWorldTTR ? -1 : 1); 312 | return x.TimeToRun == y.TimeToRun ? 0 : (x.TimeToRun > y.TimeToRun ? -1 : 1); 313 | } 314 | } -------------------------------------------------------------------------------- /ByondSharp.Samples/Util/PriorityQueue.cs: -------------------------------------------------------------------------------- 1 | // Taken from https://github.com/sphinxy/DataStructures/blob/35f41dcb29e5ee6d2217fc5ae4525f0990a6be0a/DataStructures/PriorityQueue.cs 2 | // With some modifications also added by https://github.com/space-wizards/RobustToolbox/blob/b8e5b47e7abced7cbb87cd5c460ef7bd9a91c69a/Robust.Shared/Utility/PriorityQueue.cs 3 | 4 | /* 5 | The MIT License (MIT) 6 | Copyright (c) 2015 Denis Shulepov 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 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, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | */ 23 | 24 | using System; 25 | using System.Collections; 26 | using System.Collections.Generic; 27 | using System.Diagnostics.CodeAnalysis; 28 | 29 | namespace ByondSharp.Samples.Util; 30 | 31 | /// 32 | /// Heap-based resizable max-priority queue. 33 | /// Elements with high priority are served before elements with low priority. 34 | /// Priority is defined by comparing elements, so to separate priority from value use 35 | /// KeyValuePair or a custom class and provide corresponding Comparer. 36 | /// 37 | /// Any comparable type, either through a specified Comparer or implementing IComparable<> 38 | public sealed class PriorityQueue : ICollection 39 | { 40 | private readonly IComparer _comparer; 41 | private T[] _heap; 42 | private const int DefaultCapacity = 10; 43 | private const int ShrinkRatio = 4; 44 | private const int ResizeFactor = 2; 45 | 46 | private int _shrinkBound; 47 | 48 | private static readonly InvalidOperationException EmptyCollectionException = new("Collection is empty."); 49 | 50 | /// 51 | /// Create a max-priority queue with default capacity of 10. 52 | /// 53 | /// Custom comparer to compare elements. If omitted - default will be used. 54 | public PriorityQueue(IComparer comparer = null) : this(DefaultCapacity, comparer) { } 55 | 56 | /// 57 | /// Create a max-priority queue with provided capacity. 58 | /// 59 | /// Initial capacity 60 | /// Custom comparer to compare elements. If omitted - default will be used. 61 | /// Throws when capacity is less than or equal to zero. 62 | /// Throws when comparer is null and does not implement IComparable. 63 | public PriorityQueue(int capacity, IComparer comparer = null) 64 | { 65 | if (capacity <= 0) throw new ArgumentOutOfRangeException(nameof(capacity), "Expected capacity greater than zero."); 66 | 67 | // If no comparer then T must be comparable 68 | if (comparer == null && 69 | !(typeof(IComparable).IsAssignableFrom(typeof(T)) || 70 | typeof(IComparable).IsAssignableFrom(typeof(T)))) 71 | { 72 | throw new ArgumentException("Expected a comparer for types, which do not implement IComparable.", nameof(comparer)); 73 | } 74 | 75 | _comparer = comparer ?? Comparer.Default; 76 | _shrinkBound = capacity / ShrinkRatio; 77 | _heap = new T[capacity]; 78 | } 79 | 80 | /// 81 | /// Current queue capacity 82 | /// 83 | private int Capacity => _heap.Length; 84 | 85 | public IEnumerator GetEnumerator() 86 | { 87 | var array = new T[Count]; 88 | CopyTo(array, 0); 89 | return ((IEnumerable)array).GetEnumerator(); 90 | } 91 | 92 | IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); 93 | 94 | public void Add(T item) 95 | { 96 | if (Count == Capacity) GrowCapacity(); 97 | 98 | _heap[Count++] = item; 99 | // provide the index of the last item as for 1-based heap, but also set shift to -1 100 | _heap.Sift(Count, _comparer, shift: -1); // move item "up" until heap principles are not met 101 | } 102 | 103 | /// 104 | /// Removes and returns a max element from the priority queue. 105 | /// 106 | /// Max element in the collection 107 | /// Throws when queue is empty. 108 | public T Take() 109 | { 110 | if (Count == 0) throw EmptyCollectionException; 111 | 112 | var item = _heap[0]; 113 | Count--; 114 | _heap.Swap(0, Count); // last element at count 115 | _heap[Count] = default!; // release hold on the object 116 | // provide index of first item as for 1-based heap, but also set shift to -1 117 | _heap.Sink(1, Count, _comparer, shift: -1); // move item "down" while heap principles are not met 118 | 119 | if (Count <= _shrinkBound && Count > DefaultCapacity) 120 | { 121 | ShrinkCapacity(); 122 | } 123 | 124 | return item; 125 | } 126 | 127 | /// 128 | /// Returns a max element from the priority queue without removing it. 129 | /// 130 | /// Max element in the collection 131 | /// Throws when queue is empty. 132 | public T Peek() 133 | { 134 | if (Count == 0) throw EmptyCollectionException; 135 | return _heap[0]; 136 | } 137 | 138 | public void Clear() 139 | { 140 | _heap = new T[DefaultCapacity]; 141 | Count = 0; 142 | } 143 | 144 | public bool Contains(T item) => GetItemIndex(item) >= 0; 145 | 146 | public void CopyTo(T[] array, int arrayIndex) 147 | { 148 | if (array == null) throw new ArgumentNullException(nameof(array)); 149 | if (arrayIndex < 0) throw new ArgumentOutOfRangeException(nameof(arrayIndex)); 150 | 151 | if (array.Length - arrayIndex < Count) 152 | throw new ArgumentException("Insufficient space in destination array."); 153 | 154 | Array.Copy(_heap, 0, array, arrayIndex, Count); 155 | 156 | array.HeapSort(arrayIndex, Count, _comparer); 157 | } 158 | 159 | public bool Remove(T item) 160 | { 161 | var index = GetItemIndex(item); 162 | switch (index) 163 | { 164 | case -1: 165 | return false; 166 | case 0: 167 | Take(); 168 | break; 169 | default: 170 | // provide a 1-based index of the item 171 | RemoveAt(index + 1, shift: -1); 172 | break; 173 | } 174 | 175 | return true; 176 | } 177 | 178 | public int Count { get; private set; } 179 | 180 | public bool IsReadOnly => false; 181 | 182 | /// 183 | /// Removes item at given index 184 | /// 185 | /// 1-based index of the element to remove 186 | /// Shift allows to compensate and work with arrays where heap starts not from the element at position 1. 187 | /// Shift -1 allows to work with 0-based heap as if it was 1-based. But the main reason for this is the CopyTo method. 188 | private void RemoveAt(int index, int shift) 189 | { 190 | var itemIndex = index + shift; 191 | Count--; 192 | _heap.Swap(itemIndex, Count); // last element at Count 193 | _heap[Count] = default!; // release hold on the object 194 | // use a 1-based-heap index and then apply shift of -1 195 | var parent = index / 2 + shift; // get parent 196 | // if new item at index is greater than it's parent then sift it up, else sink it down 197 | if (_comparer.GreaterOrEqual(_heap[itemIndex], _heap[parent])) 198 | { 199 | // provide a 1-based-heap index 200 | _heap.Sift(index, _comparer, shift); 201 | } 202 | else 203 | { 204 | // provide a 1-based-heap index 205 | _heap.Sink(index, Count, _comparer, shift); 206 | } 207 | } 208 | 209 | /// 210 | /// Returns the real index of the first occurrence of the given item or -1. 211 | /// 212 | private int GetItemIndex(T item) 213 | { 214 | for (int i = 0; i < Count; i++) 215 | { 216 | if (_comparer.Compare(_heap[i], item) == 0) return i; 217 | } 218 | return -1; 219 | } 220 | 221 | 222 | private void GrowCapacity() 223 | { 224 | int newCapacity = Capacity * ResizeFactor; 225 | Array.Resize(ref _heap, newCapacity); // first element is at position 1 226 | _shrinkBound = newCapacity / ShrinkRatio; 227 | } 228 | 229 | private void ShrinkCapacity() 230 | { 231 | int newCapacity = Capacity / ResizeFactor; 232 | Array.Resize(ref _heap, newCapacity); // first element is at position 1 233 | _shrinkBound = newCapacity / ShrinkRatio; 234 | } 235 | } 236 | 237 | internal static class HeapMethods 238 | { 239 | internal static void Swap(this T[] array, int i, int j) 240 | { 241 | (array[i], array[j]) = (array[j], array[i]); 242 | } 243 | 244 | internal static bool GreaterOrEqual(this IComparer comparer, [AllowNull] T x, [AllowNull] T y) => comparer.Compare(x, y) >= 0; 245 | 246 | /// 247 | /// Moves the item with given index "down" the heap while heap principles are not met. 248 | /// 249 | /// Any comparable type 250 | /// Array, containing the heap 251 | /// 1-based index of the element to sink 252 | /// Number of items in the heap 253 | /// Comparer to compare the items 254 | /// Shift allows to compensate and work with arrays where heap starts not from the element at position 1. 255 | /// Shift -1 allows to work with 0-based heap as if it was 1-based. But the main reason for this is the CopyTo method. 256 | /// 257 | internal static void Sink(this T[] heap, int i, int count, IComparer comparer, int shift) 258 | { 259 | var lastIndex = count + shift; 260 | while (true) 261 | { 262 | var itemIndex = i + shift; 263 | var leftIndex = 2 * i + shift; 264 | if (leftIndex > lastIndex) 265 | { 266 | return; // reached last item 267 | } 268 | 269 | var rightIndex = leftIndex + 1; 270 | var hasRight = rightIndex <= lastIndex; 271 | 272 | var item = heap[itemIndex]; 273 | var left = heap[leftIndex]; 274 | var right = hasRight ? heap[rightIndex] : default; 275 | 276 | // if item is greater than children - heap is fine, exit 277 | // ReSharper disable once RedundantTypeArgumentsOfMethod 278 | if (GreaterOrEqual(comparer, item, left) && (!hasRight || GreaterOrEqual(comparer, item, right))) 279 | { 280 | return; 281 | } 282 | 283 | // else exchange with greater of children 284 | // ReSharper disable once RedundantTypeArgumentsOfMethod 285 | var greaterChildIndex = !hasRight || GreaterOrEqual(comparer, left, right) ? leftIndex : rightIndex; 286 | heap.Swap(itemIndex, greaterChildIndex); 287 | 288 | // continue at new position 289 | i = greaterChildIndex - shift; 290 | } 291 | } 292 | 293 | /// 294 | /// Moves the item with given index "up" the heap while heap principles are not met. 295 | /// 296 | /// Any comparable type 297 | /// Array, containing the heap 298 | /// 1-based index of the element to sink 299 | /// Comparer to compare the items 300 | /// Shift allows to compensate and work with arrays where heap starts not from the element at position 1. 301 | /// Value -1 allows to work with 0-based heap as if it was 1-based. But the main reason for this is the CopyTo method. 302 | /// 303 | internal static void Sift(this T[] heap, int i, IComparer comparer, int shift) 304 | { 305 | while (true) 306 | { 307 | if (i <= 1) return; // reached root 308 | var parent = i / 2 + shift; // get parent 309 | var index = i + shift; 310 | 311 | // if root is greater or equal - exit 312 | if (GreaterOrEqual(comparer, heap[parent], heap[index])) 313 | { 314 | return; 315 | } 316 | 317 | heap.Swap(parent, index); 318 | i = parent - shift; 319 | } 320 | } 321 | 322 | /// 323 | /// Sorts the heap in descending order. 324 | /// 325 | /// Any comparable type 326 | /// Array, containing the heap 327 | /// Index in the array, from which the heap structure begins 328 | /// Number of items in the heap 329 | /// Comparer to compare the items 330 | internal static void HeapSort(this T[] heap, int startIndex, int count, IComparer comparer) 331 | { 332 | var shift = startIndex - 1; 333 | var lastIndex = startIndex + count; 334 | var left = count; 335 | 336 | // take the max item and exchange it with the last one 337 | // decrement the count of items in the heap and sink the first item to restore the heap rules 338 | // repeat for every element in the heap 339 | while (lastIndex > startIndex) 340 | { 341 | lastIndex--; 342 | left--; 343 | heap.Swap(startIndex, lastIndex); 344 | heap.Sink(1, left, comparer, shift); 345 | } 346 | // when done items will be sorted in ascending order, but this is a Max-PriorityQueue, so reverse 347 | Array.Reverse(heap, startIndex, count); 348 | } 349 | } -------------------------------------------------------------------------------- /ByondSharpGenerator/FFIGenerator.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using Microsoft.CodeAnalysis.CSharp; 3 | using Microsoft.CodeAnalysis.CSharp.Syntax; 4 | using Microsoft.CodeAnalysis.Text; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Text; 8 | 9 | namespace ByondSharpGenerator 10 | { 11 | [Generator] 12 | public class FFIGenerator : ISourceGenerator 13 | { 14 | private static readonly DiagnosticDescriptor NonStaticMethodError = new DiagnosticDescriptor(id: "BSFFIGEN001", 15 | title: "Exported method must be static", 16 | messageFormat: "Method '{0}' must be static to be exported as a FFI for BYOND", 17 | category: "ByondSharpGenerator", 18 | DiagnosticSeverity.Error, 19 | isEnabledByDefault: true); 20 | private static readonly DiagnosticDescriptor InvalidReturnTypeError = new DiagnosticDescriptor(id: "BSFFIGEN002", 21 | title: "Invalid return type for exported method", 22 | messageFormat: "{0} '{1}' must have a return type of {2} to be exported as a FFI for BYOND", 23 | category: "ByondSharpGenerator", 24 | DiagnosticSeverity.Error, 25 | isEnabledByDefault: true); 26 | private static readonly DiagnosticDescriptor TooManyParametersError = new DiagnosticDescriptor(id: "BSFFIGEN003", 27 | title: "Too many parameters provided for exported method", 28 | messageFormat: "Method '{0}' must have zero or one arguments to be exported as a FFI for BYOND", 29 | category: "ByondSharpGenerator", 30 | DiagnosticSeverity.Error, 31 | isEnabledByDefault: true); 32 | private static readonly DiagnosticDescriptor InvalidParameterError = new DiagnosticDescriptor(id: "BSFFIGEN004", 33 | title: "Invalid return type for exported method", 34 | messageFormat: "Method '{0}' must have no arguments or a single argument of List to be exported as a FFI for BYOND", 35 | category: "ByondSharpGenerator", 36 | DiagnosticSeverity.Error, 37 | isEnabledByDefault: true); 38 | private static readonly DiagnosticDescriptor InvalidVisibilityError = new DiagnosticDescriptor(id: "BSFFIGEN005", 39 | title: "Invalid visibility for exported method", 40 | messageFormat: "Method '{0}' must be public to be exported as a FFI for BYOND", 41 | category: "ByondSharpGenerator", 42 | DiagnosticSeverity.Error, 43 | isEnabledByDefault: true); 44 | private static readonly DiagnosticDescriptor CannotDeferSyncMethod = new DiagnosticDescriptor(id: "BSFFIGEN006", 45 | title: "Cannot defer synchronous method", 46 | messageFormat: "Method '{0}' must be async to be exported as a deferrable FFI for BYOND", 47 | category: "ByondSharpGenerator", 48 | DiagnosticSeverity.Error, 49 | isEnabledByDefault: true); 50 | private static readonly List ValidAsyncReturnTypes = new List() { "System.Threading.Tasks.Task", "System.Threading.Tasks.Task" }; 51 | 52 | public void Execute(GeneratorExecutionContext context) 53 | { 54 | if (!(context.SyntaxReceiver is SyntaxReceiver receiver)) 55 | return; 56 | 57 | // Check that each method is actually tagged with ByondFFIAttribute 58 | INamedTypeSymbol attrSymbol = context.Compilation.GetTypeByMetadataName("ByondSharp.FFI.ByondFFIAttribute"); 59 | List symbols = new List(); 60 | foreach (MethodDeclarationSyntax method in receiver.CandidateFields) 61 | { 62 | SemanticModel model = context.Compilation.GetSemanticModel(method.SyntaxTree); 63 | IMethodSymbol methodSym = model.GetDeclaredSymbol(method); 64 | if (methodSym.GetAttributes().Any(attr => attr.AttributeClass.Equals(attrSymbol, SymbolEqualityComparer.Default))) 65 | { 66 | // Check the validity of the attribute on this method, first by ensuring it is static 67 | if (!methodSym.IsStatic) 68 | { 69 | context.ReportDiagnostic(Diagnostic.Create(NonStaticMethodError, method.GetLocation(), methodSym.Name)); 70 | continue; 71 | } 72 | 73 | // Next we also need to validate it is public 74 | if (!method.Modifiers.Any(modifier => modifier.IsKind(SyntaxKind.PublicKeyword))) 75 | { 76 | context.ReportDiagnostic(Diagnostic.Create(InvalidVisibilityError, method.GetLocation(), methodSym.Name)); 77 | continue; 78 | } 79 | 80 | // Check for valid return type, validate the async return type when relevant 81 | if (!methodSym.ReturnsVoid && ( 82 | (!methodSym.IsAsync && methodSym.ReturnType.ToDisplayString() != "string") 83 | || (methodSym.IsAsync && !ValidAsyncReturnTypes.Contains(methodSym.ReturnType.ToDisplayString())))) 84 | { 85 | 86 | context.ReportDiagnostic(Diagnostic.Create(InvalidReturnTypeError, method.ReturnType.GetLocation(), methodSym.Name, methodSym.IsAsync ? "Task or Task" : "string or void")); 87 | continue; 88 | } 89 | 90 | // Check for empty or 1 param 91 | if (method.ParameterList.Parameters.Count != 0) 92 | { 93 | if (methodSym.Parameters.Count() > 1) 94 | { 95 | context.ReportDiagnostic(Diagnostic.Create(TooManyParametersError, method.GetLocation(), methodSym.Name)); 96 | continue; 97 | } 98 | 99 | // If there is a param present, it needs to be a List 100 | if (method.ParameterList.Parameters[0].Type.ToString() != "List") 101 | { 102 | context.ReportDiagnostic(Diagnostic.Create(InvalidParameterError, method.GetLocation(), methodSym.Name)); 103 | continue; 104 | } 105 | } 106 | 107 | // Check for async if this is a deferrable method 108 | var attr = methodSym.GetAttributes().First(x => x.AttributeClass.Equals(attrSymbol, SymbolEqualityComparer.Default)); 109 | if (attr.NamedArguments.Length > 0 && (bool)attr.NamedArguments.First(x => x.Key == "Deferrable").Value.Value && !methodSym.IsAsync) 110 | { 111 | context.ReportDiagnostic(Diagnostic.Create(CannotDeferSyncMethod, method.GetLocation(), methodSym.Name)); 112 | continue; 113 | } 114 | 115 | // Add the symbol if it passes all these checks 116 | symbols.Add(model.GetDeclaredSymbol(method)); 117 | } 118 | } 119 | 120 | // Generate additional source code, and add to the compiler 121 | var source = ProcessMethods(symbols, context); 122 | context.AddSource("FFIExports_auto.cs", SourceText.From(source, Encoding.UTF8)); 123 | } 124 | 125 | /// 126 | /// Generate wrappers for a collection of methods so that they can be called by BYOND through FFI 127 | /// 128 | /// The methods to wrap 129 | /// The execution context 130 | /// The generated source code, including wrappers 131 | private static string ProcessMethods(List methods, GeneratorExecutionContext context) 132 | { 133 | INamedTypeSymbol attrSymbol = context.Compilation.GetTypeByMetadataName("ByondSharp.FFI.ByondFFIAttribute"); 134 | var source = new StringBuilder($@" 135 | using System; 136 | using System.Linq; 137 | using System.Runtime.CompilerServices; 138 | using System.Runtime.InteropServices; 139 | using System.Threading; 140 | using System.Threading.Tasks; 141 | "); 142 | 143 | source.Append($@" 144 | namespace ByondSharp 145 | {{ 146 | public class _FFIExports 147 | {{"); 148 | 149 | foreach (var method in methods) 150 | { 151 | var attribute = method.GetAttributes().First(x => x.AttributeClass.Equals(attrSymbol, SymbolEqualityComparer.Default)); 152 | if (attribute.NamedArguments.Length > 0 && (bool)attribute.NamedArguments.First(x => x.Key == "Deferrable").Value.Value) 153 | { 154 | GenerateDeferralMethod(method, source); 155 | } 156 | 157 | string methodReturn = method.ReturnsVoid || method.IsAsync && method.ReturnType.ToDisplayString() == "System.Threading.Tasks.Task" ? "void" : "IntPtr"; 158 | source.Append($@" 159 | [UnmanagedCallersOnly(CallConvs = new[] {{ typeof(CallConvCdecl) }}, EntryPoint = ""{method.Name}"")] 160 | public static {methodReturn} {method.Name}__FFIWrapper(int numArgs, IntPtr argPtr) 161 | {{"); 162 | if (!method.Parameters.IsEmpty) 163 | { 164 | source.Append($@" 165 | // Boilerplate to get data passed 166 | string[] args = new string[numArgs]; 167 | IntPtr[] argPtrs = new IntPtr[numArgs]; 168 | Marshal.Copy(argPtr, argPtrs, 0, numArgs); 169 | for (var x = 0; x < numArgs; x++) 170 | {{ 171 | args[x] = Marshal.PtrToStringUTF8(argPtrs[x]); 172 | if (args[x].Length == 0) 173 | args[x] = null; 174 | }} 175 | "); 176 | } 177 | 178 | source.Append(@" 179 | // Begin calling actual method 180 | try 181 | {"); 182 | 183 | var methodCall = new StringBuilder(); 184 | if (methodReturn != "void") 185 | methodCall.Append("return ByondSharp.FFI.ByondFFI.FFIReturn(new ByondSharp.FFI.ByondResponse() { ResponseCode = ByondSharp.FFI.ResponseCode.Success, Data = "); 186 | methodCall.Append($"{method.ContainingSymbol.ToDisplayString()}.{method.Name}({(method.Parameters.IsEmpty ? "" : "args.ToList()")})"); 187 | if (method.IsAsync) 188 | methodCall.Append(".GetAwaiter().GetResult()"); 189 | if (methodReturn != "void") 190 | methodCall.Append(" })"); 191 | methodCall.Append(";"); 192 | 193 | source.Append($@" 194 | {methodCall} 195 | }} 196 | catch ({(methodReturn != "void" ? "Exception ex" : "Exception")}) 197 | {{ 198 | {(methodReturn != "void" ? "return ByondSharp.FFI.ByondFFI.FFIReturn(new ByondSharp.FFI.ByondResponse() { ResponseCode = ByondSharp.FFI.ResponseCode.Error, _Exception = ex });" : "// As this doesn't expect a value back, we cannot report the runtime")} 199 | }} 200 | }}"); 201 | } 202 | 203 | source.Append(@" 204 | } 205 | }"); 206 | 207 | return source.ToString(); 208 | } 209 | 210 | private static void GenerateDeferralMethod(IMethodSymbol method, StringBuilder source) 211 | { 212 | string methodReturn = method.ReturnsVoid || method.IsAsync && method.ReturnType.ToDisplayString() == "Task" ? "void" : "IntPtr"; 213 | source.Append($@" 214 | [UnmanagedCallersOnly(CallConvs = new[] {{ typeof(CallConvCdecl) }}, EntryPoint = ""{method.Name}Deferred"")] 215 | public static {methodReturn} {method.Name}__FFIWrapperDeferred(int numArgs, IntPtr argPtr) 216 | {{"); 217 | if (!method.Parameters.IsEmpty) 218 | { 219 | source.Append($@" 220 | // Boilerplate to get data passed 221 | string[] args = new string[numArgs]; 222 | IntPtr[] argPtrs = new IntPtr[numArgs]; 223 | Marshal.Copy(argPtr, argPtrs, 0, numArgs); 224 | for (var x = 0; x < numArgs; x++) 225 | {{ 226 | args[x] = Marshal.PtrToStringUTF8(argPtrs[x]); 227 | if (args[x].Length == 0) 228 | args[x] = null; 229 | }} 230 | "); 231 | } 232 | 233 | source.Append(@" 234 | // Begin calling actual method 235 | try 236 | {"); 237 | 238 | var methodCall = new StringBuilder(); 239 | if (methodReturn != "void") 240 | methodCall.Append("return ByondSharp.FFI.ByondFFI.FFIReturn(new ByondSharp.FFI.ByondResponse() { ResponseCode = ByondSharp.FFI.ResponseCode.Deferred, Data = ByondSharp.Deferred.TaskManager.RunTask("); 241 | methodCall.Append($"{method.ContainingSymbol.ToDisplayString()}.{method.Name}({(method.Parameters.IsEmpty ? "" : "args.ToList()")})"); 242 | if (methodReturn == "void") 243 | methodCall.Append(".Start()"); 244 | if (methodReturn != "void") 245 | methodCall.Append(").ToString() })"); 246 | methodCall.Append(";"); 247 | 248 | source.Append($@" 249 | {methodCall} 250 | }} 251 | catch ({(methodReturn != "void" ? "Exception ex" : "Exception")}) 252 | {{ 253 | {(methodReturn != "void" ? "return ByondSharp.FFI.ByondFFI.FFIReturn(new ByondSharp.FFI.ByondResponse() { ResponseCode = ByondSharp.FFI.ResponseCode.Error, _Exception = ex });" : "// As this doesn't expect a value back, we cannot report the runtime")} 254 | }} 255 | }}"); 256 | } 257 | 258 | public void Initialize(GeneratorInitializationContext context) 259 | { 260 | context.RegisterForSyntaxNotifications(() => new SyntaxReceiver()); 261 | } 262 | } 263 | 264 | public class SyntaxReceiver : ISyntaxReceiver 265 | { 266 | public List CandidateFields { get; } = new List(); 267 | 268 | public void OnVisitSyntaxNode(SyntaxNode syntaxNode) 269 | { 270 | if (syntaxNode is MethodDeclarationSyntax methodDeclarationSyntax 271 | && methodDeclarationSyntax.AttributeLists.Count > 0) 272 | { 273 | CandidateFields.Add(methodDeclarationSyntax); 274 | } 275 | } 276 | } 277 | } 278 | --------------------------------------------------------------------------------