├── 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 |
--------------------------------------------------------------------------------