This commit is contained in:
zznty
2022-05-24 15:49:43 +07:00
commit c20e468b99
32 changed files with 1654 additions and 0 deletions

391
.gitignore vendored Normal file
View File

@@ -0,0 +1,391 @@
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Mono auto generated files
mono_crash.*
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
[Ll]ogs/
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUnit
*.VisualState.xml
TestResult.xml
nunit-*.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
# ASP.NET Scaffolding
ScaffoldingReadMe.txt
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.tlog
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Coverlet is a free, cross platform Code Coverage Tool
coverage*.json
coverage*.xml
coverage*.info
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# NuGet Symbol Packages
*.snupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Nuget personal access tokens and Credentials
nuget.config
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
*.appxbundle
*.appxupload
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- [Bb]ackup.rdl
*- [Bb]ackup ([0-9]).rdl
*- [Bb]ackup ([0-9][0-9]).rdl
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# CodeRush personal settings
.cr/personal
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
# Local History for Visual Studio
.localhistory/
# BeatPulse healthcheck temp database
healthchecksdb
# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/
# Fody - auto-generated XML schema
FodyWeavers.xsd
# VS Code files for those working on multiple tools
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
*.code-workspace
# Local History for Visual Studio Code
.history/
# Windows Installer files from build outputs
*.cab
*.msi
*.msix
*.msm
*.msp
# JetBrains Rider
.idea/
*.sln.iml
GameBinaries
TorchBinaries

View File

@@ -0,0 +1,25 @@
using System.Collections.ObjectModel;
using Torch;
using Torch.Views;
namespace LightPerms.Discord;
public class Config : ViewModel
{
[Display(Name = "Token", Description = "Discord bot token")]
public string Token { get; set; } = "bot-token";
[Display(Name = "Guild Id", Description = "Id of the guild to work with")]
public ulong GuildId { get; set; }
[Display(Name = "Role Configs")]
public ObservableCollection<DiscordRoleConfig> RoleConfigs { get; set; } = new();
}
public class DiscordRoleConfig : ViewModel
{
[Display(Name = "Role Id", Description = "Id of the discord role to work with")]
public ulong RoleId { get; set; }
[Display(Name = "Group Name", Description = "Name of the group to work with (read guide on LightPerms plugin page to create a new group)")]
public string GroupName { get; set; } = "role-group";
public override string ToString() => GroupName;
}

View File

@@ -0,0 +1,143 @@
using Discord;
using Discord.WebSocket;
using NLog;
using NLog.Fluent;
using PetaPoco;
using Torch.API;
using Torch.Managers;
namespace LightPerms.Discord;
public class DiscordManager : Manager
{
private static readonly ILogger Log = LogManager.GetCurrentClassLogger();
private readonly Config _config;
[Dependency]
private readonly IPermissionsManager _permissionsManager = null!;
private readonly DiscordSocketClient _client = new(new()
{
GatewayIntents = GatewayIntents.Guilds | GatewayIntents.GuildMessages | GatewayIntents.GuildMembers
});
private IDatabase _db = null!;
public Dictionary<string, ulong> WaitingUsers { get; } = new();
public DiscordManager(ITorchBase torchInstance, Config config) : base(torchInstance)
{
_config = config;
_client.SlashCommandExecuted += ClientOnSlashCommandExecutedAsync;
_client.GuildMemberUpdated += ClientOnGuildMemberUpdatedAsync;
_client.Ready += ClientOnReady;
_client.Log += message =>
{
NLog.Fluent.Log.Level(message.Severity switch
{
LogSeverity.Critical => LogLevel.Fatal,
LogSeverity.Error => LogLevel.Error,
LogSeverity.Warning => LogLevel.Warn,
LogSeverity.Info => LogLevel.Info,
LogSeverity.Debug => LogLevel.Debug,
LogSeverity.Verbose => LogLevel.Trace,
_ => throw new ArgumentOutOfRangeException()
}).LoggerName(message.Source).Exception(message.Exception).Message(message.Message).Write();
return Task.CompletedTask;
};
}
private async Task ClientOnReady()
{
var guild = _client.GetGuild(_config.GuildId);
if (_config.RoleConfigs.Any(b => guild.Roles.All(c => c.Id != b.RoleId)))
throw new InvalidOperationException("Some roles are not exists in the guild");
await AddSlashCommandsAsync(guild);
await guild.DownloadUsersAsync();
await Task.WhenAll(guild.Users.Select(b => ClientOnGuildMemberUpdatedAsync(default, b)));
}
public override void Attach()
{
base.Attach();
_db = _permissionsManager.Db;
_db.Execute(LinkedUser.CreateTableSql);
Task.Run(AttachAsync);
}
private async Task AttachAsync()
{
await _client.LoginAsync(TokenType.Bot, _config.Token);
await _client.StartAsync();
}
private async Task ClientOnGuildMemberUpdatedAsync(Cacheable<SocketGuildUser, ulong> cacheable, SocketGuildUser user)
{
if (await _db.SingleOrDefaultAsync<LinkedUser>(Sql.Builder.Where("discord_id = @0", user.Id.ToString())) is not { } linkedUser ||
await _db.SingleOrDefaultAsync<GroupMember>(Sql.Builder.Where("client_id = @0", linkedUser.ClientId)) is not { } groupMember)
return;
bool HasRole(ulong id) => user.Roles.Any(b => b.Id == id);
foreach (var (role, group) in _config.RoleConfigs.Select(b => (b, _permissionsManager.GetGroup(b.GroupName)!)))
{
if (!HasRole(role.RoleId) && groupMember.GroupUid == group.Uid)
{
_permissionsManager.AssignGroup(linkedUser.ClientIdNumber, "player");
Log.Info($"Removed group {role.GroupName} from {linkedUser.ClientId}");
}
else if (HasRole(role.RoleId) && groupMember.GroupUid != group.Uid)
{
_permissionsManager.AssignGroup(linkedUser.ClientIdNumber, group.Name);
groupMember.GroupUid = group.Uid;
Log.Info($"Assigned group {role.GroupName} to {linkedUser.ClientId}");
}
}
}
private async Task ClientOnSlashCommandExecutedAsync(SocketSlashCommand arg)
{
if (arg.CommandName != "lp-link")
return;
if (GetClientIdByDiscordId(arg.User.Id) is { })
{
await arg.RespondAsync("You are already linked with lp.");
return;
}
var username = Format.UsernameAndDiscriminator(arg.User, false);
if (!WaitingUsers.ContainsKey(username))
{
await arg.RespondAsync("Type `!lp link` in-game first.");
return;
}
var clientId = WaitingUsers[username];
WaitingUsers.Remove(username);
await _db.InsertAsync(new LinkedUser
{
DiscordId = arg.User.Id.ToString(),
ClientId = clientId.ToString()
});
await arg.RespondAsync("Linked successfully.");
}
private Task AddSlashCommandsAsync(SocketGuild guild)
{
return guild.CreateApplicationCommandAsync(new SlashCommandBuilder()
.WithName("lp-link")
.WithDescription("Light perms link with game")
.Build());
}
public ulong? GetClientIdByDiscordId(ulong discordId)
{
if (_db.SingleOrDefault<LinkedUser>(Sql.Builder.Where("discord_id = @0", discordId.ToString())) is not { } user)
return null;
return ulong.Parse(user.ClientId);
}
}

View File

@@ -0,0 +1,3 @@
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<PropertyChanged />
</Weavers>

View File

@@ -0,0 +1,93 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net48</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PlatformTarget>x64</PlatformTarget>
<LangVersion>10</LangVersion>
<UseWpf>true</UseWpf>
<TorchDir>$(SolutionDir)TorchBinaries\</TorchDir>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<DebugType>none</DebugType>
</PropertyGroup>
<ItemGroup>
<Reference Include="NLog, Version=4.0.0.0, Culture=neutral, PublicKeyToken=5120e14c03d0593c">
<HintPath>$(TorchDir)NLog.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Sandbox.Common, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null">
<HintPath>$(TorchDir)DedicatedServer64\Sandbox.Common.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Sandbox.Game, Version=0.1.1.0, Culture=neutral, PublicKeyToken=null">
<HintPath>$(TorchDir)DedicatedServer64\Sandbox.Game.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Sandbox.Graphics, Version=0.1.1.0, Culture=neutral, PublicKeyToken=null">
<HintPath>$(TorchDir)DedicatedServer64\Sandbox.Graphics.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="System.Net.Http" />
<Reference Include="Torch">
<HintPath>$(TorchDir)Torch.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Torch.API">
<HintPath>$(TorchDir)Torch.API.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Torch.Server">
<HintPath>$(TorchDir)Torch.Server.exe</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="VRage, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null">
<HintPath>$(TorchDir)DedicatedServer64\VRage.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="VRage.Game, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null">
<HintPath>$(TorchDir)DedicatedServer64\VRage.Game.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="VRage.Input, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null">
<HintPath>$(TorchDir)DedicatedServer64\VRage.Input.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="VRage.Library, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null">
<HintPath>$(TorchDir)DedicatedServer64\VRage.Library.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="VRage.Math, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null">
<HintPath>$(TorchDir)DedicatedServer64\VRage.Math.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="VRage.Network, Version=1.0.53.0, Culture=neutral, PublicKeyToken=null">
<HintPath>$(TorchDir)DedicatedServer64\VRage.Network.dll</HintPath>
<Private>False</Private>
</Reference>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Discord.Net.Commands" Version="3.6.1" />
<PackageReference Include="Discord.Net.WebSocket" Version="3.6.1" />
<PackageReference Include="PropertyChanged.Fody" Version="4.0.0" PrivateAssets="all" />
</ItemGroup>
<ItemGroup>
<None Update="manifest.xml">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\LightPerms\LightPerms.csproj" Private="false" PrivateAssets="all" />
</ItemGroup>
<Target Name="CopyToTorch" AfterTargets="Build">
<ZipDirectory DestinationFile="$(TorchDir)Plugins\$(AssemblyName).zip" SourceDirectory="$(TargetDir)" Overwrite="true" />
</Target>
</Project>

View File

@@ -0,0 +1,20 @@
using PetaPoco;
namespace LightPerms.Discord;
[TableName("linked_users")]
[PrimaryKey(nameof(Uid), AutoIncrement = true)]
public class LinkedUser
{
[Column]
public long Uid { get; set;}
[Column]
public string DiscordId { get; set; } = "0";
[Column]
public string ClientId { get; set; } = "0";
internal const string CreateTableSql = "create table if not exists linked_users (uid INTEGER PRIMARY KEY AUTOINCREMENT, discord_id TEXT NOT NULL, client_id TEXT NOT NULL);";
[Ignore]
public ulong ClientIdNumber => ulong.Parse(ClientId);
}

View File

@@ -0,0 +1,26 @@
using System.Text.RegularExpressions;
using Torch.API.Managers;
using Torch.Commands;
using Torch.Commands.Permissions;
using VRage.Game.ModAPI;
namespace LightPerms.Discord;
[Category("lp")]
public class LpCommands : CommandModule
{
private static readonly Regex DiscordTagRegex = new(@"^.{3,32}#[0-9]{4}$");
[Command("link", "Link you with your discord account. (type `/lp-link` on server discord after)")]
[Permission(MyPromoteLevel.None)]
public void Link(string discordUsername)
{
if (!DiscordTagRegex.IsMatch(discordUsername))
{
Context.Respond("Please enter a valid discord username! (like abcde#1234)");
return;
}
Context.Torch.CurrentSession.Managers.GetManager<DiscordManager>().WaitingUsers[discordUsername] = Context.Player.SteamUserId;
Context.Respond("Type `/lp-link` on server discord");
}
}

View File

@@ -0,0 +1,29 @@
using System.IO;
using System.Windows.Controls;
using Torch;
using Torch.API;
using Torch.API.Managers;
using Torch.API.Plugins;
using Torch.API.Session;
using Torch.Views;
namespace LightPerms.Discord;
public class Plugin : TorchPluginBase, IWpfPlugin
{
private Persistent<Config> _config = null!;
public override void Init(ITorchBase torch)
{
base.Init(torch);
_config = Persistent<Config>.Load(Path.Combine(StoragePath, "LightPerms.Discord.cfg"));
Torch.Managers.GetManager<ITorchSessionManager>().AddFactory(s => new DiscordManager(s.Torch, _config.Data));
}
public UserControl GetControl() => new PropertyGrid
{
Margin = new(3),
DataContext = _config.Data
};
}

View File

@@ -0,0 +1,5 @@
Add-on for [LightPerms](https://torchapi.com/plugins/view/?guid=5c3f35b3-ac9d-486f-8559-f931536c6700) plugin to automatically assign groups based on discord role
# Setup a bot
if you already have a bot installed with **SEDB** or **Torch Alert** you use token from it else you have to follow the [guide](https://goo.gl/5Do8LJ)

View File

@@ -0,0 +1,12 @@
<?xml version="1.0"?>
<PluginManifest xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<Name>LightPerms.Discord</Name>
<Guid>d53cf5e6-27ea-491b-9579-8506d93f184b</Guid>
<Version>v1.0.1</Version>
<Dependencies>
<PluginDependency>
<Plugin>5c3f35b3-ac9d-486f-8559-f931536c6700</Plugin>
<MinVersion>v1.0.1</MinVersion>
</PluginDependency>
</Dependencies>
</PluginManifest>

View File

@@ -0,0 +1,20 @@
:: This script creates a symlink to the game binaries to account for different installation directories on different systems.
@echo off
set /p path="Please enter the folder location of your Torch.Server.exe: "
cd %~dp0
rmdir TorchBinaries > nul 2>&1
mklink /J ..\TorchBinaries "%path%"
if errorlevel 1 goto Error
echo Done!
echo You can now open the plugin without issue.
goto EndFinal
:Error
echo An error occured creating the symlink.
goto EndFinal
:EndFinal
pause

View File

@@ -0,0 +1,30 @@
using System.Text;
using Torch.API.Managers;
using Torch.Commands;
using Torch.Mod;
using Torch.Mod.Messages;
namespace LightPerms.TorchCommands;
[Category("lp")]
public class Commands : CommandModule
{
[Command("get commands", "Get all command permissions")]
public void GetCommands()
{
var sb = new StringBuilder();
if (Context.Player is null)
sb.AppendLine("Available commands:");
foreach (var commandNode in Context.Torch.CurrentSession.Managers.GetManager<CommandManager>().Commands.WalkTree()
.Where(b => b.IsCommand))
{
sb.Append(" ".PadRight(4)).AppendLine(commandNode.Command.GetPermissionString());
}
if (Context.Player is null)
Context.Respond(sb.ToString());
else
ModCommunication.SendMessageTo(new DialogMessage("Light Perms", "Available commands:", sb.ToString()), Context.Player.SteamUserId);
}
}

View File

@@ -0,0 +1,10 @@
using Torch.Commands;
namespace LightPerms.TorchCommands;
public static class Extensions
{
public static string GetPermissionString(this Command command)
{
return $"command.{string.Join(".", command.Path.Select(b => b.ToLowerInvariant()))}";
}
}

View File

@@ -0,0 +1,3 @@
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<PropertyChanged />
</Weavers>

View File

@@ -0,0 +1,91 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net48</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PlatformTarget>x64</PlatformTarget>
<LangVersion>10</LangVersion>
<UseWpf>true</UseWpf>
<TorchDir>$(SolutionDir)TorchBinaries\</TorchDir>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<DebugType>none</DebugType>
</PropertyGroup>
<ItemGroup>
<Reference Include="NLog, Version=4.0.0.0, Culture=neutral, PublicKeyToken=5120e14c03d0593c">
<HintPath>$(TorchDir)NLog.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Sandbox.Common, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null">
<HintPath>$(TorchDir)DedicatedServer64\Sandbox.Common.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Sandbox.Game, Version=0.1.1.0, Culture=neutral, PublicKeyToken=null">
<HintPath>$(TorchDir)DedicatedServer64\Sandbox.Game.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Sandbox.Graphics, Version=0.1.1.0, Culture=neutral, PublicKeyToken=null">
<HintPath>$(TorchDir)DedicatedServer64\Sandbox.Graphics.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="System.Net.Http" />
<Reference Include="Torch">
<HintPath>$(TorchDir)Torch.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Torch.API">
<HintPath>$(TorchDir)Torch.API.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Torch.Server">
<HintPath>$(TorchDir)Torch.Server.exe</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="VRage, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null">
<HintPath>$(TorchDir)DedicatedServer64\VRage.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="VRage.Game, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null">
<HintPath>$(TorchDir)DedicatedServer64\VRage.Game.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="VRage.Input, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null">
<HintPath>$(TorchDir)DedicatedServer64\VRage.Input.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="VRage.Library, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null">
<HintPath>$(TorchDir)DedicatedServer64\VRage.Library.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="VRage.Math, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null">
<HintPath>$(TorchDir)DedicatedServer64\VRage.Math.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="VRage.Network, Version=1.0.53.0, Culture=neutral, PublicKeyToken=null">
<HintPath>$(TorchDir)DedicatedServer64\VRage.Network.dll</HintPath>
<Private>False</Private>
</Reference>
</ItemGroup>
<ItemGroup>
<PackageReference Include="PropertyChanged.Fody" Version="3.4.0" PrivateAssets="all" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\LightPerms\LightPerms.csproj" Private="false" PrivateAssets="all" />
</ItemGroup>
<ItemGroup>
<None Update="manifest.xml">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
<Target Name="CopyToTorch" AfterTargets="Build">
<ZipDirectory DestinationFile="$(TorchDir)Plugins\$(AssemblyName).zip" SourceDirectory="$(TargetDir)" Overwrite="true" />
</Target>
</Project>

View File

@@ -0,0 +1,157 @@
using System.Reflection;
using System.Reflection.Emit;
using System.Text;
using Sandbox.Game.Multiplayer;
using Sandbox.Game.World;
using Torch.API;
using Torch.API.Managers;
using Torch.Commands;
using Torch.Managers;
using Torch.Managers.PatchManager;
using Torch.Managers.PatchManager.MSIL;
using Torch.Mod;
using Torch.Mod.Messages;
using Torch.Utils;
namespace LightPerms.TorchCommands;
[PatchShim]
public static class PermissionPatch
{
[ReflectedMethodInfo(typeof(CommandManager), nameof(CommandManager.HasPermission))]
private static readonly MethodInfo HasPermissionMethod = null!;
[ReflectedMethodInfo(typeof(Torch.Commands.TorchCommands), nameof(Torch.Commands.TorchCommands.Help))]
private static readonly MethodInfo HelpMethod = null!;
[ReflectedMethodInfo(typeof(Torch.Commands.TorchCommands), nameof(Torch.Commands.TorchCommands.LongHelp))]
private static readonly MethodInfo LongHelpMethod = null!;
[ReflectedMethodInfo(typeof(PermissionPatch), nameof(Prefix))]
private static readonly MethodInfo PrefixMethod = null!;
[ReflectedMethodInfo(typeof(PermissionPatch), nameof(PrefixHelp))]
private static readonly MethodInfo PrefixHelpMethod = null!;
[ReflectedMethodInfo(typeof(PermissionPatch), nameof(PrefixLongHelp))]
private static readonly MethodInfo PrefixLongHelpMethod = null!;
[ReflectedMethodInfo(typeof(PermissionPatch), nameof(Transpiler))]
private static readonly MethodInfo TranspilerMethod = null!;
[ReflectedGetter(Name = "Torch")]
private static readonly Func<Manager, ITorchBase> TorchGetter = null!;
public static void Patch(PatchContext context)
{
context.GetPattern(HasPermissionMethod).Prefixes.Add(PrefixMethod);
context.GetPattern(typeof(CommandManager).GetMethod(nameof(CommandManager.HandleCommand),
new []{typeof(string), typeof(ulong), typeof(bool).MakeByRefType(), typeof(bool)})).Transpilers.Add(TranspilerMethod);
context.GetPattern(HelpMethod).Prefixes.Add(PrefixHelpMethod);
context.GetPattern(LongHelpMethod).Prefixes.Add(PrefixLongHelpMethod);
}
private static bool PrefixHelp(CommandModule __instance)
{
var commandManager = __instance.Context.Torch.CurrentSession.Managers.GetManager<CommandManager>();
commandManager.Commands.GetNode(__instance.Context.Args, out var node);
if (node is null)
{
__instance.Context.Respond(
$"Command not found. Use the {commandManager.Prefix}longhelp command for a full list of commands.");
return false;
}
var command = node.Command;
var children = node.Subcommands.Where(e => __instance.Context.Player == null ||
!e.Value.IsCommand ||
!commandManager.HasPermission(__instance.Context.Player.SteamUserId, e.Value.Command))
.Select(x => x.Key);
var sb = new StringBuilder();
if (command is not null)
{
if (__instance.Context.Player is not null && !commandManager.HasPermission(__instance.Context.Player.SteamUserId, command))
{
__instance.Context.Respond("You are not authorized to use this command.");
return false;
}
sb.AppendLine($"Syntax: {command.SyntaxHelp}");
sb.Append(command.HelpText);
}
if (node.Subcommands.Count > 0)
sb.Append($"\nSubcommands: {string.Join(", ", children)}");
__instance.Context.Respond(sb.ToString());
return false;
}
private static bool PrefixLongHelp(CommandModule __instance)
{
var commandManager = __instance.Context.Torch.CurrentSession.Managers.GetManager<CommandManager>();
commandManager.Commands.GetNode(__instance.Context.Args, out var node);
if (node != null)
{
var command = node.Command;
var children = node.Subcommands.Where(e => __instance.Context.Player == null ||
!e.Value.IsCommand ||
!commandManager.HasPermission(__instance.Context.Player.SteamUserId, e.Value.Command));
var sb = new StringBuilder();
if (command != null && (__instance.Context.Player is null || commandManager.HasPermission(__instance.Context.Player.SteamUserId, command)))
{
sb.AppendLine($"Syntax: {command.SyntaxHelp}");
sb.Append(command.HelpText);
}
if (node.Subcommands.Count > 0)
sb.Append($"\nSubcommands: {string.Join(", ", children)}");
__instance.Context.Respond(sb.ToString());
}
else
{
var sb = new StringBuilder();
foreach (var command in commandManager.Commands.WalkTree())
{
if (command.IsCommand && (__instance.Context.Player is null || commandManager.HasPermission(__instance.Context.Player.SteamUserId, command.Command)))
sb.AppendLine($"{command.Command.SyntaxHelp}\n {command.Command.HelpText}");
}
if (!__instance.Context.SentBySelf)
{
var m = new DialogMessage("Torch Help", "Available commands:", sb.ToString());
ModCommunication.SendMessageTo(m, __instance.Context.Player!.SteamUserId);
}
else
__instance.Context.Respond($"Available commands: {sb}");
}
return false;
}
private static bool Prefix(Manager __instance, ulong steamId, Command command, ref bool __result)
{
__result = steamId == Sync.MyId ||
MySession.Static.IsUserAdmin(steamId) ||
TorchGetter(__instance).Managers.GetManager<IPermissionsManager>().HasPermission(steamId, command.GetPermissionString());
return false;
}
private static IEnumerable<MsilInstruction> Transpiler(IEnumerable<MsilInstruction> ins)
{
foreach (var instruction in ins)
{
if (instruction.OpCode == OpCodes.Ldstr &&
instruction.Operand is MsilOperandInline.MsilOperandString {Value: "You need to be a {0} or higher to use that command."})
yield return instruction.InlineValue("You don't have permission to use that command.");
else
yield return instruction;
}
}
}

View File

@@ -0,0 +1,12 @@
using System.IO;
using System.Windows.Controls;
using Torch;
using Torch.API;
using Torch.API.Plugins;
using Torch.Views;
namespace LightPerms.TorchCommands;
public class Plugin : TorchPluginBase
{
}

View File

@@ -0,0 +1,11 @@
Add-on for [LightPerms](https://torchapi.com/plugins/view/?guid=5c3f35b3-ac9d-486f-8559-f931536c6700) plugin to have better control over commands permissions.
### Usage
All existing torch commands are indexed as `command.something`, spaces are replaced by `.`
`!help` and `!longhelp` commands are modified to reflect permission requirements.
For example if you want give to players ability to use `!fixship` command, you need to invoke this command `!lp add perm player command.fixship`
Wildcards are also supported, for example command to give to admins ability to use all lp commands (you need to create group before using that command) `!lp add perm admin command.lp.*`

View File

@@ -0,0 +1,12 @@
<?xml version="1.0"?>
<PluginManifest xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<Name>LightPerms.TorchCommands</Name>
<Guid>0cf57780-f235-4240-99cd-ebd7463e673a</Guid>
<Version>v1.0.0</Version>
<Dependencies>
<PluginDependency>
<Plugin>5c3f35b3-ac9d-486f-8559-f931536c6700</Plugin>
<MinVersion>v1.0.1</MinVersion>
</PluginDependency>
</Dependencies>
</PluginManifest>

View File

@@ -0,0 +1,20 @@
:: This script creates a symlink to the game binaries to account for different installation directories on different systems.
@echo off
set /p path="Please enter the folder location of your Torch.Server.exe: "
cd %~dp0
rmdir TorchBinaries > nul 2>&1
mklink /J ..\TorchBinaries "%path%"
if errorlevel 1 goto Error
echo Done!
echo You can now open the plugin without issue.
goto EndFinal
:Error
echo An error occured creating the symlink.
goto EndFinal
:EndFinal
pause

28
LightPerms.sln Normal file
View File

@@ -0,0 +1,28 @@

Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LightPerms", "LightPerms\LightPerms.csproj", "{3963D8F4-CCB6-4305-8FEC-A19597404A19}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LightPerms.Discord", "LightPerms.Discord\LightPerms.Discord.csproj", "{B1A35416-6CFB-4AE7-A2F2-818E8F7A8C13}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LightPerms.TorchCommands", "LightPerms.TorchCommands\LightPerms.TorchCommands.csproj", "{8F9D910F-FFE6-4010-921F-5872ACF638BB}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{3963D8F4-CCB6-4305-8FEC-A19597404A19}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3963D8F4-CCB6-4305-8FEC-A19597404A19}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3963D8F4-CCB6-4305-8FEC-A19597404A19}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3963D8F4-CCB6-4305-8FEC-A19597404A19}.Release|Any CPU.Build.0 = Release|Any CPU
{B1A35416-6CFB-4AE7-A2F2-818E8F7A8C13}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B1A35416-6CFB-4AE7-A2F2-818E8F7A8C13}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B1A35416-6CFB-4AE7-A2F2-818E8F7A8C13}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B1A35416-6CFB-4AE7-A2F2-818E8F7A8C13}.Release|Any CPU.Build.0 = Release|Any CPU
{8F9D910F-FFE6-4010-921F-5872ACF638BB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8F9D910F-FFE6-4010-921F-5872ACF638BB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8F9D910F-FFE6-4010-921F-5872ACF638BB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8F9D910F-FFE6-4010-921F-5872ACF638BB}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

8
LightPerms/Config.cs Normal file
View File

@@ -0,0 +1,8 @@
using Torch;
namespace LightPerms;
public class Config : ViewModel
{
}

View File

@@ -0,0 +1,3 @@
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<PropertyChanged />
</Weavers>

47
LightPerms/Group.cs Normal file
View File

@@ -0,0 +1,47 @@
using PetaPoco;
namespace LightPerms;
[TableName("groups")]
[PrimaryKey(nameof(Uid), AutoIncrement = true)]
public class Group
{
[Column]
public long Uid { get; set; }
[Column]
public string Name { get; set; } = "group";
internal const string CreateTableSql = "create table if not exists groups (uid INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL);";
}
[TableName("permissions")]
[PrimaryKey(nameof(Uid), AutoIncrement = true)]
public class Permission
{
[Column]
public long Uid { get; set; }
[Column]
public string Value { get; set; } = "*";
[Column]
public long GroupUid { get; set; }
internal const string CreateTableSql = "create table if not exists permissions (uid INTEGER PRIMARY KEY AUTOINCREMENT, value TEXT NOT NULL, group_uid INTEGER, FOREIGN KEY(group_uid) REFERENCES groups(uid));";
}
[TableName("group_members")]
[PrimaryKey(nameof(Uid), AutoIncrement = true)]
public class GroupMember
{
[Column]
public long Uid { get; set; }
[Column]
public string Name { get; set; } = "no name";
[Column]
public string ClientId { get; set; } = "0";
[Column]
public long GroupUid { get; set; }
internal const string CreateTableSql = "create table if not exists group_members (uid INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, client_id INTEGER, group_uid INTEGER, FOREIGN KEY(group_uid) REFERENCES groups(uid));";
}

View File

@@ -0,0 +1,88 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net48</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PlatformTarget>x64</PlatformTarget>
<LangVersion>10</LangVersion>
<UseWpf>true</UseWpf>
<TorchDir>$(SolutionDir)TorchBinaries\</TorchDir>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<DebugType>none</DebugType>
</PropertyGroup>
<ItemGroup>
<Reference Include="NLog, Version=4.0.0.0, Culture=neutral, PublicKeyToken=5120e14c03d0593c">
<HintPath>$(TorchDir)NLog.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Sandbox.Common, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null">
<HintPath>$(TorchDir)DedicatedServer64\Sandbox.Common.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Sandbox.Game, Version=0.1.1.0, Culture=neutral, PublicKeyToken=null">
<HintPath>$(TorchDir)DedicatedServer64\Sandbox.Game.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Sandbox.Graphics, Version=0.1.1.0, Culture=neutral, PublicKeyToken=null">
<HintPath>$(TorchDir)DedicatedServer64\Sandbox.Graphics.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="System.Net.Http" />
<Reference Include="Torch">
<HintPath>$(TorchDir)Torch.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Torch.API">
<HintPath>$(TorchDir)Torch.API.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Torch.Server">
<HintPath>$(TorchDir)Torch.Server.exe</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="VRage, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null">
<HintPath>$(TorchDir)DedicatedServer64\VRage.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="VRage.Game, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null">
<HintPath>$(TorchDir)DedicatedServer64\VRage.Game.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="VRage.Input, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null">
<HintPath>$(TorchDir)DedicatedServer64\VRage.Input.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="VRage.Library, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null">
<HintPath>$(TorchDir)DedicatedServer64\VRage.Library.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="VRage.Math, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null">
<HintPath>$(TorchDir)DedicatedServer64\VRage.Math.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="VRage.Network, Version=1.0.53.0, Culture=neutral, PublicKeyToken=null">
<HintPath>$(TorchDir)DedicatedServer64\VRage.Network.dll</HintPath>
<Private>False</Private>
</Reference>
</ItemGroup>
<ItemGroup>
<PackageReference Include="heh" Version="1.0.4" />
<PackageReference Include="PropertyChanged.Fody" Version="4.0.0" PrivateAssets="all" />
</ItemGroup>
<ItemGroup>
<None Update="manifest.xml">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
<Target Name="CopyToTorch" AfterTargets="Build">
<ZipDirectory DestinationFile="$(TorchDir)Plugins\$(AssemblyName).zip" SourceDirectory="$(TargetDir)" Overwrite="true" />
</Target>
</Project>

163
LightPerms/LpCommands.cs Normal file
View File

@@ -0,0 +1,163 @@
using PetaPoco;
using Torch.API.Managers;
using Torch.Commands;
using Torch.Commands.Permissions;
using VRage.Game.ModAPI;
namespace LightPerms;
[Category("lp")]
public class LpCommands : CommandModule
{
private IPermissionsManager Pm => Context.Torch.Managers.GetManager<IPermissionsManager>();
#if DEBUG
[Command("gen")]
public void GenTest()
{
var db = Pm.Db;
db.Execute("delete from groups");
db.Execute("delete from group_members");
db.Execute("delete from permissions");
var playerGroup = (long)db.Insert(new Group {Name = "player"});
db.Insert(new Permission {Value = "test.permission.player", GroupUid = playerGroup});
var adminGroup = (long)db.Insert(new Group {Name = "admin"});
db.Insert(new Permission {Value = "test.permission.admin", GroupUid = adminGroup});
db.Insert(new GroupMember {Name = Context.Player.DisplayName, ClientId = Context.Player.SteamUserId.ToString(), GroupUid = playerGroup});
}
#endif
[Command("has perm")]
[Permission(MyPromoteLevel.Admin)]
public void HasPermission(string perm, ulong clientId = 0)
{
clientId = Context.Player?.SteamUserId ?? clientId;
if (clientId == 0)
{
Context.Respond("Specify valid client id.");
return;
}
Context.Respond(Pm.HasPermission(clientId, perm) ? "Yes" : "No");
}
[Command("get groups")]
[Permission(MyPromoteLevel.Admin)]
public void GetGroups()
{
Context.Respond($"Available groups:\n {string.Join("\n ", Pm.Db.Query<Group>().Select(b => $"{b.Name} - id {b.Uid}"))}");
}
[Command("get perms")]
[Permission(MyPromoteLevel.Admin)]
public void GetPerms(string groupName)
{
if (Pm.Db.SingleOrDefault<Group>(Sql.Builder.Where("name = @0", groupName)) is not { } group)
{
Context.Respond("No group found with given name");
return;
}
Context.Respond($"Available perms:\n {string.Join("\n ", Pm.Db.Query<Permission>(Sql.Builder.Where("group_uid = @0", group.Uid)).Select(b => b.Value).OrderBy(b => b))}");
}
[Command("add group")]
[Permission(MyPromoteLevel.Admin)]
public void AddGroup(string groupName)
{
if (Pm.Db.Exists<Group>("name = @0", groupName))
{
Context.Respond("Group already exists");
return;
}
Pm.Db.Insert(new Group
{
Name = groupName
});
Context.Respond("Added");
}
[Command("add perm")]
[Permission(MyPromoteLevel.Admin)]
public void AddPerm(string groupName, string permission)
{
if (Pm.Db.SingleOrDefault<Group>(Sql.Builder.Where("name = @0", groupName)) is not { } group)
{
Context.Respond("No group found with given name");
return;
}
if (Pm.Db.Exists<Permission>("group_uid = @0 and value like @1", group.Uid, permission.Replace('*', '%')))
{
Context.Respond("Permission already exists");
return;
}
Pm.Db.Insert(new Permission
{
Value = permission,
GroupUid = group.Uid
});
Context.Respond("Added");
}
[Command("del group")]
[Permission(MyPromoteLevel.Admin)]
public void DelGroup(string groupName)
{
if (Pm.Db.Delete<Group>(Sql.Builder.Where("name = @0", groupName)) != 1)
{
Context.Respond("Group not exists");
return;
}
Context.Respond("Deleted");
}
[Command("del perm")]
[Permission(MyPromoteLevel.Admin)]
public void DelPerm(string groupName, string permission)
{
if (Pm.Db.SingleOrDefault<Group>(Sql.Builder.Where("name = @0", groupName)) is not { } group)
{
Context.Respond("No group found with given name");
return;
}
var count = Pm.Db.Delete<Permission>(Sql.Builder.Where("group_uid = @0 and value like @1", group.Uid, permission.Replace('*', '%')));
if (count > 0)
{
Context.Respond($"Deleted {count} permissions");
return;
}
Context.Respond("No permissions found");
}
[Command("assign group")]
[Permission(MyPromoteLevel.Admin)]
public void AssignGroup(string groupName, ulong clientId = 0)
{
if (clientId == 0)
clientId = Context.Player.SteamUserId;
if (Pm.Db.SingleOrDefault<Group>(Sql.Builder.Where("name = @0", groupName)) is not { } group)
{
Context.Respond("No group found with given name");
return;
}
if (Pm.Db.Exists<GroupMember>("group_uid = @0 and client_id = @1", group.Uid, clientId))
{
Context.Respond("User already assigned to this group");
return;
}
Pm.AssignGroup(clientId, groupName);
Context.Respond("User assigned to the group");
}
}

View File

@@ -0,0 +1,37 @@
using Torch.API;
using Torch.API.Managers;
using Torch.Managers;
namespace LightPerms;
public class MultiplayerMembersManager : Manager
{
[Dependency]
private readonly IPermissionsManager _permissionsManager = null!;
[Dependency]
private readonly IMultiplayerManagerServer _multiplayerManager = null!;
public MultiplayerMembersManager(ITorchBase torchInstance) : base(torchInstance)
{
}
public override void Attach()
{
base.Attach();
_multiplayerManager.PlayerJoined += MultiplayerManagerOnPlayerJoined;
}
private void MultiplayerManagerOnPlayerJoined(IPlayer player)
{
if (_permissionsManager.Db.Exists<GroupMember>("client_id = @0", player.SteamId))
return;
var groupMember = new GroupMember
{
Name = player.Name,
ClientId = player.SteamId.ToString(),
GroupUid = 0
};
_permissionsManager.Db.Insert(groupMember);
}
}

View File

@@ -0,0 +1,85 @@
using heh;
using NLog;
using PetaPoco;
using Sandbox.Game.Multiplayer;
using Torch.API;
using Torch.API.Managers;
using Torch.Managers;
namespace LightPerms;
public interface IPermissionsManager : IManager
{
bool HasPermission(ulong clientId, string permission);
void AssignGroup(ulong clientId, string groupName);
Group? GetGroup(string groupName);
IDatabase Db { get; }
}
public class PermissionsManager : Manager, IPermissionsManager
{
private static readonly ILogger Log = LogManager.GetCurrentClassLogger();
private static readonly char[] InvalidPermissionChars = {'*', '?'};
[Dependency]
private readonly IDbManager _dbManager = null!;
public PermissionsManager(ITorchBase torchInstance) : base(torchInstance)
{
}
public override void Attach()
{
base.Attach();
Db = _dbManager.Create("light_perms");
Db.Execute(Group.CreateTableSql);
Db.Execute(Permission.CreateTableSql);
Db.Execute(GroupMember.CreateTableSql);
if (GetGroup("player") is null)
Db.Insert(new Group
{
Name = "player"
});
}
public bool HasPermission(ulong clientId, string permission)
{
if (permission.IndexOfAny(InvalidPermissionChars) > -1)
throw new InvalidOperationException("Permission should not contain any invalid characters");
var member = Db.SingleOrDefault<GroupMember>(Sql.Builder.Where("client_id = @0", clientId));
var result = member is not null && Db.Exists<Permission>("group_uid = @0 and @1 like replace(replace(value,'*','%'),'?','_')", member.GroupUid, permission);
if (!result)
Log.Info("User ({0}) has no permission '{1}'", clientId, permission);
return result;
}
public void AssignGroup(ulong clientId, string groupName)
{
if (Sync.Players.TryGetPlayerIdentity(new(clientId)) is not { } identity)
throw new InvalidOperationException($"Invalid client id {clientId}");
if (Db.SingleOrDefault<Group>(Sql.Builder.Where("name = @0", groupName)) is not { } group)
throw new InvalidOperationException($"Invalid group name {groupName}");
if (Db.SingleOrDefault<GroupMember>(Sql.Builder.Where("client_id = @0", clientId)) is { } groupMember)
Db.Delete(groupMember);
Db.Insert(new GroupMember
{
Name = identity.DisplayName,
ClientId = clientId.ToString(),
GroupUid = group.Uid
});
}
public Group? GetGroup(string groupName)
{
return Db.SingleOrDefault<Group>(Sql.Builder.Where("name = @0", groupName));
}
public IDatabase Db { get; private set; } = null!;
}

31
LightPerms/Plugin.cs Normal file
View File

@@ -0,0 +1,31 @@
using System.IO;
using System.Windows.Controls;
using heh;
using Torch;
using Torch.API;
using Torch.API.Managers;
using Torch.API.Plugins;
using Torch.API.Session;
using Torch.Views;
namespace LightPerms;
public class Plugin : TorchPluginBase, IWpfPlugin
{
private Persistent<Config> _config = null!;
public override void Init(ITorchBase torch)
{
base.Init(torch);
_config = Persistent<Config>.Load(Path.Combine(StoragePath, "LightPerms.cfg"));
Torch.Managers.AddManager(DbManager.Static);
Torch.Managers.AddManager(new PermissionsManager(Torch));
Torch.Managers.GetManager<ITorchSessionManager>().AddFactory(s => new MultiplayerMembersManager(s.Torch));
}
public UserControl GetControl() => new PropertyGrid
{
Margin = new(3),
DataContext = _config.Data
};
}

25
LightPerms/README.md Normal file
View File

@@ -0,0 +1,25 @@
Light plugin to add permission system (like minecraft's PermissionEx or LuckPerms) to your server.
In this plugins all permissions can be wildcards and belongs to groups
By default plugin creates and assigns `player` group to all players automatically (currently unchangeable)
### Commands
+ `!lp get groups` - Returns list of all groups
+ `!lp add group <group name>` - Creates a new group
+ `!lp del groups <group name>` - Deletes the group
+ `!lp get perms <group name>` - Returns list of permissions in the group
+ `!lp add perm <group name> <permission>` - Adds a new permission to the group
+ `!lp del perm <group name> <permission>` - Deletes a permission from the group
+ `!lp has perm <permission> <client id>` - Returns if player has a permission (client id can be removed to use the sender id)
+ `!lp assign group <group name> <client id>` - Assigns a group to the player (client id can be removed to use the sender id)
### Groups based on discord roles
To automatically assign a group to players if they have a specific role in discord server use [LightPerms.Discord](https://torchapi.com/plugins/view/?guid=d53cf5e6-27ea-491b-9579-8506d93f184b) plugin
### Support in plugins
+ [Kits](https://torchapi.com/plugins/view/?guid=d095391d-b5ec-43a9-8ba4-6c4909227e6e)

6
LightPerms/manifest.xml Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0"?>
<PluginManifest xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<Name>LightPerms</Name>
<Guid>5c3f35b3-ac9d-486f-8559-f931536c6700</Guid>
<Version>v1.0.1</Version>
</PluginManifest>

20
LightPerms/setup.bat Normal file
View File

@@ -0,0 +1,20 @@
:: This script creates a symlink to the game binaries to account for different installation directories on different systems.
@echo off
set /p path="Please enter the folder location of your Torch.Server.exe: "
cd %~dp0
rmdir TorchBinaries > nul 2>&1
mklink /J ..\TorchBinaries "%path%"
if errorlevel 1 goto Error
echo Done!
echo You can now open the plugin without issue.
goto EndFinal
:Error
echo An error occured creating the symlink.
goto EndFinal
:EndFinal
pause