Repository: zkhssb/NectarRCON
Branch: master
Commit: e7349a9bd853
Files: 112
Total size: 224.2 KB
Directory structure:
gitextract_0vsxp36e/
├── .gitattributes
├── .github/
│ └── workflows/
│ └── dotnet-desktop.yml
├── .gitignore
├── LICENSE.txt
├── NectarRCON/
│ ├── App.xaml
│ ├── App.xaml.cs
│ ├── AssemblyInfo.cs
│ ├── Converters/
│ │ ├── BoolToVisibilityConverter.cs
│ │ └── IntToVisibilityConverter.cs
│ ├── Dp/
│ │ ├── DpFile.cs
│ │ └── RconSettingsDp.cs
│ ├── Entity/
│ │ ├── ClearLogValueMessage.cs
│ │ ├── Config.cs
│ │ ├── ETheme.cs
│ │ ├── Group.cs
│ │ ├── MainPageLoadValueMessage.cs
│ │ ├── ServerInformation.cs
│ │ └── ServerPassword.cs
│ ├── Interfaces/
│ │ ├── IConfigService.cs
│ │ ├── IConnectingDialogService.cs
│ │ ├── IGroupService.cs
│ │ ├── IHistoryService.cs
│ │ ├── ILanguageService.cs
│ │ ├── ILogService.cs
│ │ ├── IMessageBoxService.cs
│ │ ├── IRconConnectionInfoService.cs
│ │ ├── IServerInformationService.cs
│ │ └── IServerPasswordService.cs
│ ├── Models/
│ │ ├── GroupModel.cs
│ │ ├── GroupServerItemModel.cs
│ │ └── ServerModel.cs
│ ├── NectarRCON.csproj
│ ├── Rcon/
│ │ ├── IRconConnection.cs
│ │ ├── RconMultiConnection.cs
│ │ └── RconSingleConnection.cs
│ ├── Resources/
│ │ └── Languages/
│ │ ├── en_us.xaml
│ │ ├── zh_cn.xaml
│ │ └── zh_tw.xaml
│ ├── Services/
│ │ ├── ApplicationHostService.cs
│ │ ├── ConfigService.cs
│ │ ├── ConnectingDialogService.cs
│ │ ├── GroupService.cs
│ │ ├── HistoryService.cs
│ │ ├── LanguageService.cs
│ │ ├── LogService.cs
│ │ ├── MessageBoxService.cs
│ │ ├── RconConnectionInfoService.cs
│ │ ├── ServerInformationService.cs
│ │ └── ServerPasswordService.cs
│ ├── ViewModels/
│ │ ├── AboutPageViewModel.cs
│ │ ├── AddGroupPageViewModel.cs
│ │ ├── AddServerWindowViewModel.cs
│ │ ├── EditPasswordWindowViewModel.cs
│ │ ├── EditServerWindowViewModel.cs
│ │ ├── GroupPageViewModel.cs
│ │ ├── JoinGroupWindowViewModel.cs
│ │ ├── MainPageViewModel.cs
│ │ ├── MainWindowViewModel.cs
│ │ ├── ServersPageViewModel.cs
│ │ └── SettingPageViewModel.cs
│ ├── Views/
│ │ └── Pages/
│ │ ├── AboutPage.xaml
│ │ ├── AboutPage.xaml.cs
│ │ ├── AddGroupPage.xaml
│ │ ├── AddGroupPage.xaml.cs
│ │ ├── GroupPage.xaml
│ │ ├── GroupPage.xaml.cs
│ │ ├── MainPage.xaml
│ │ ├── MainPage.xaml.cs
│ │ ├── ServersPage.xaml
│ │ ├── ServersPage.xaml.cs
│ │ ├── SettingPage.xaml
│ │ └── SettingPage.xaml.cs
│ ├── Windows/
│ │ ├── AddServerWindow.xaml
│ │ ├── AddServerWindow.xaml.cs
│ │ ├── EditPasswordWindow.xaml
│ │ ├── EditPasswordWindow.xaml.cs
│ │ ├── EditServerWindow.xaml
│ │ ├── EditServerWindow.xaml.cs
│ │ ├── JoinGroupWindow.xaml
│ │ ├── JoinGroupWindow.xaml.cs
│ │ ├── MainWindow.xaml
│ │ └── MainWindow.xaml.cs
│ └── app.manifest
├── NectarRCON.Adapter.Minecraft/
│ ├── MinecraftRconClient.cs
│ ├── NectarRCON.Adapter.Minecraft.csproj
│ ├── Packet.cs
│ ├── PacketEncoder.cs
│ └── README.md
├── NectarRCON.Core/
│ ├── Helper/
│ │ ├── AdapterHelpers.cs
│ │ ├── DNSHelpers.cs
│ │ └── Win32Helper.cs
│ └── NectarRCON.Core.csproj
├── NectarRCON.Export/
│ ├── Client/
│ │ └── BaseTcpClient.cs
│ ├── Interfaces/
│ │ ├── IRconAdapter.cs
│ │ └── IRconClient.cs
│ └── NectarRCON.Export.csproj
├── NectarRCON.Tests/
│ ├── Adapter/
│ │ └── MinecraftRconTest.cs
│ ├── DNSHelperTests.cs
│ ├── GroupServiceTests.cs
│ ├── MessageBoxServiceTests.cs
│ ├── NectarRCON.Tests.csproj
│ ├── UpdaterTests.cs
│ └── Usings.cs
├── NectarRCON.Updater/
│ ├── AppVersion.cs
│ ├── GithubUpdater.cs
│ ├── IUpdater.cs
│ ├── Model/
│ │ ├── Asset.cs
│ │ └── Release.cs
│ └── NectarRCON.Updater.csproj
├── NectarRCON.sln
├── README.md
└── README_EN.md
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitattributes
================================================
###############################################################################
# Set default behavior to automatically normalize line endings.
###############################################################################
* text=auto
###############################################################################
# Set default behavior for command prompt diff.
#
# This is need for earlier builds of msysgit that does not have it on by
# default for csharp files.
# Note: This is only used by command line
###############################################################################
#*.cs diff=csharp
###############################################################################
# Set the merge driver for project and solution files
#
# Merging from the command prompt will add diff markers to the files if there
# are conflicts (Merging from VS is not affected by the settings below, in VS
# the diff markers are never inserted). Diff markers may cause the following
# file extensions to fail to load in VS. An alternative would be to treat
# these files as binary and thus will always conflict and require user
# intervention with every merge. To do so, just uncomment the entries below
###############################################################################
#*.sln merge=binary
#*.csproj merge=binary
#*.vbproj merge=binary
#*.vcxproj merge=binary
#*.vcproj merge=binary
#*.dbproj merge=binary
#*.fsproj merge=binary
#*.lsproj merge=binary
#*.wixproj merge=binary
#*.modelproj merge=binary
#*.sqlproj merge=binary
#*.wwaproj merge=binary
###############################################################################
# behavior for image files
#
# image files are treated as binary by default.
###############################################################################
#*.jpg binary
#*.png binary
#*.gif binary
###############################################################################
# diff behavior for common document formats
#
# Convert binary document formats to text before diffing them. This feature
# is only available from the command line. Turn it on by uncommenting the
# entries below.
###############################################################################
#*.doc diff=astextplain
#*.DOC diff=astextplain
#*.docx diff=astextplain
#*.DOCX diff=astextplain
#*.dot diff=astextplain
#*.DOT diff=astextplain
#*.pdf diff=astextplain
#*.PDF diff=astextplain
#*.rtf diff=astextplain
#*.RTF diff=astextplain
================================================
FILE: .github/workflows/dotnet-desktop.yml
================================================
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
# This workflow will build, test, sign and package a WPF or Windows Forms desktop application
# built on .NET Core.
# To learn how to migrate your existing application to .NET Core,
# refer to https://docs.microsoft.com/en-us/dotnet/desktop-wpf/migration/convert-project-from-net-framework
#
# To configure this workflow:
#
# 1. Configure environment variables
# GitHub sets default environment variables for every workflow run.
# Replace the variables relative to your project in the "env" section below.
#
# 2. Signing
# Generate a signing certificate in the Windows Application
# Packaging Project or add an existing signing certificate to the project.
# Next, use PowerShell to encode the .pfx file using Base64 encoding
# by running the following Powershell script to generate the output string:
#
# $pfx_cert = Get-Content '.\SigningCertificate.pfx' -Encoding Byte
# [System.Convert]::ToBase64String($pfx_cert) | Out-File 'SigningCertificate_Encoded.txt'
#
# Open the output file, SigningCertificate_Encoded.txt, and copy the
# string inside. Then, add the string to the repo as a GitHub secret
# and name it "Base64_Encoded_Pfx."
# For more information on how to configure your signing certificate for
# this workflow, refer to https://github.com/microsoft/github-actions-for-desktop-apps#signing
#
# Finally, add the signing certificate password to the repo as a secret and name it "Pfx_Key".
# See "Build the Windows Application Packaging project" below to see how the secret is used.
#
# For more information on GitHub Actions, refer to https://github.com/features/actions
# For a complete CI/CD sample to get started with GitHub Action workflows for Desktop Applications,
# refer to https://github.com/microsoft/github-actions-for-desktop-apps
name: .NET Core Desktop
on:
push:
branches: [ "master" ]
pull_request:
branches: [ "master" ]
jobs:
build:
strategy:
matrix:
configuration: [Debug, Release]
runs-on: windows-latest # For a list of available runner types, refer to
# https://help.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idruns-on
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Install .NET Core
uses: actions/setup-dotnet@v3
with:
dotnet-version: 7.0.x
- name: Restore dependencies
run: dotnet restore
- name: Build
run: dotnet build --no-restore
================================================
FILE: .gitignore
================================================
## 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
# NectarRcon Config
config.json
servers.json
passwords.json
# 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/
[Oo]ut/
[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
*.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
# 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
================================================
FILE: LICENSE.txt
================================================
MIT License
Copyright (c) [year] [fullname]
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: NectarRCON/App.xaml
================================================
1.0.0-beta5
================================================
FILE: NectarRCON/App.xaml.cs
================================================
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using NectarRCON.Interfaces;
using NectarRCON.Rcon;
using NectarRCON.Services;
using NectarRCON.ViewModels;
using NectarRCON.Windows;
using System;
using System.IO;
using System.Linq;
using System.Text;
using System.Windows;
using Microsoft.Extensions.Logging;
using NectarRCON.Dp;
using Serilog;
using Wpf.Ui.Mvvm.Contracts;
using Wpf.Ui.Mvvm.Services;
namespace NectarRCON;
///
/// Interaction logic for App.xaml
///
public partial class App
{
private static readonly ILoggerFactory LoggerFactory = new LoggerFactory();
private static string LogFileName = $"logs/program/log{DateTime.Now:yyyyMMddhhmm}.log";
private static readonly IHost Host = Microsoft.Extensions.Hosting.Host
.CreateDefaultBuilder()
.ConfigureLogging(builder =>
{
Log.Logger = new LoggerConfiguration()
.WriteTo.File(LogFileName,
rollingInterval: RollingInterval.Infinite, flushToDiskInterval: TimeSpan.FromSeconds(1))
.CreateLogger();
builder.AddSerilog();
})
.ConfigureServices((context, services) =>
{
services.AddSingleton(LoggerFactory);
services.AddSingleton(typeof(ILogger<>), typeof(Logger<>));
services.AddHostedService();
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
// Rcon Connections
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
services.AddScoped();
services.AddScoped();
services.AddTransient();
services.AddTransient();
}).Build();
public static T GetService()
where T : class
{
return (Host.Services.GetService(typeof(T)) as T)!;
}
public static T GetService(Type type)
where T : class
{
return Host.Services.GetServices().FirstOrDefault(t => t.GetType() == type)!;
}
private async void OnStartup(object sender, StartupEventArgs e)
{
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
foreach (var rconEncoding in Enum.GetValues())
{
rconEncoding.GetEncoding();
}
AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
await Host.StartAsync();
}
private static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
{
var exception = e.ExceptionObject as Exception;
Log.Error("未经处理的异常: {0}", exception);
MessageBox.Show(
exception +
$"\n\n程序遇到异常,即将退出!\n建议前往Github提交Issue\n请前往日志查看详细信息!\nCheck log: {Path.Combine(Environment.CurrentDirectory, LogFileName).Replace("\\", "/")}",
"程序崩溃", MessageBoxButton.OK, MessageBoxImage.Error);
Environment.Exit(1);
}
private async void OnExit(object sender, ExitEventArgs e)
{
await Host.StopAsync();
Host.Dispose();
}
}
================================================
FILE: NectarRCON/AssemblyInfo.cs
================================================
using System.Windows;
[assembly: ThemeInfo(
ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
//(used if a resource is not found in the page,
// or application resource dictionaries)
ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
//(used if a resource is not found in the page,
// app, or any theme specific resource dictionaries)
)]
================================================
FILE: NectarRCON/Converters/BoolToVisibilityConverter.cs
================================================
using System;
using System.Windows.Data;
using System.Windows;
using System.Globalization;
namespace NectarRCON.Converters
{
public class BoolToVisibilityConverter : IValueConverter
{
public object? Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if(value is bool boolValue)
{
return boolValue ? Visibility.Visible : Visibility.Hidden;
}
return null;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}
================================================
FILE: NectarRCON/Converters/IntToVisibilityConverter.cs
================================================
using System;
using System.Windows.Data;
using System.Windows;
using System.Globalization;
namespace NectarRCON.Converters
{
public class IntToVisibilityConverter : IValueConverter
{
public object? Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if(value is int intValue)
{
if(parameter is string boolStringValue)
{
bool boolValue = System.Convert.ToBoolean(boolStringValue);
// 如果参数为False
// <= 0 ? Visible : Hidden
// 否则反过来
return boolValue
? (intValue <= 0 ? Visibility.Hidden : Visibility.Visible)
: (intValue <= 0 ? Visibility.Visible : Visibility.Hidden);
}
return intValue <= 0 ? Visibility.Visible : Visibility.Hidden;
}
return null;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}
================================================
FILE: NectarRCON/Dp/DpFile.cs
================================================
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.Json;
namespace NectarRCON.Dp;
///
/// 数据持久化文件
///
public abstract class DpFile
{
///
/// 文件名
///
protected abstract string Name { get; }
///
/// 文件路径
///
protected virtual string BasePath => string.Empty;
///
/// 实例映射
///
private static readonly Dictionary InstanceMapping = [];
///
/// 保存数据
///
public void Save()
{
var json = JsonSerializer.Serialize((object)this);
var filePath = Path.Combine(AppContext.BaseDirectory,"dp", BasePath, Name);
Directory.CreateDirectory(Path.GetDirectoryName(filePath)!);
File.WriteAllText(filePath, json);
}
///
/// 加载数据
///
/// 文件名
/// 文件路径
/// 类型
/// 实例
private static T? Load(string name, string? basePath = null)
where T : DpFile
{
var filePath = Path.Combine(AppContext.BaseDirectory, "dp", basePath ?? string.Empty, name);
if (!File.Exists(filePath)) return null;
var json = File.ReadAllText(filePath);
return JsonSerializer.Deserialize(json);
}
///
/// 以单例模式加载数据
///
/// 类型
/// 实例
public static T LoadSingleton()
where T:DpFile
{
// 先从_instanceMapping拿数据
if (InstanceMapping.TryGetValue(typeof(T), out var cachedInstance))
{
return (T)cachedInstance;
}
// 如果缓存没有 则使用找到此DPFile的无参构造函数 使用反射实例化后存放到_instanceMapping
var instance = Activator.CreateInstance();
// 从instance中获取Name 随后load
InstanceMapping[typeof(T)] = Load(instance.Name, instance.BasePath) ?? instance;
return (T)InstanceMapping[typeof(T)];
}
}
================================================
FILE: NectarRCON/Dp/RconSettingsDp.cs
================================================
using System.Text;
using System.Text.Json.Serialization;
namespace NectarRCON.Dp;
public enum RconEncoding
{
Utf8 = 0,
Utf16 = 1,
Utf32 = 2,
Gb2312 = 3,
Gbk = 4,
Gb18030 = 5,
Ascii = 6,
Big5 = 7,
HzGb2312 = 8,
}
public static class RconEncodingExtensions
{
public static Encoding GetEncoding(this RconEncoding encoding)
=> encoding switch
{
RconEncoding.Utf8 => Encoding.UTF8,
RconEncoding.Utf16 => Encoding.GetEncoding("UTF-16"),
RconEncoding.Utf32 => Encoding.UTF32,
RconEncoding.Gb2312 => Encoding.GetEncoding("gb2312"),
RconEncoding.Gbk => Encoding.GetEncoding("gbk"),
RconEncoding.Gb18030 => Encoding.GetEncoding("gb18030"),
RconEncoding.Ascii => Encoding.ASCII,
RconEncoding.Big5 => Encoding.GetEncoding("big5"),
RconEncoding.HzGb2312 => Encoding.GetEncoding("hz-gb-2312"),
_ => Encoding.UTF8,
};
}
public class RconSettingsDp : DpFile
{
protected override string Name => "rcon_settings.json";
///
/// 连接时掉线自动尝试重连
///
[JsonPropertyName("auto_reconnect")]
public bool AutoReconnect { get; set; } = true;
///
/// 掉线后不关闭连接窗口
///
///
[JsonPropertyName("is_keep_connection_window_open")]
public bool IsKeepConnectionWindowOpen { get; set; }
///
/// 文本编码
///
[JsonPropertyName("encoding")]
public RconEncoding Encoding { get; set; } = RconEncoding.Utf8;
}
================================================
FILE: NectarRCON/Entity/ClearLogValueMessage.cs
================================================
namespace NectarRCON.Models;
public partial class ClearLogValueMessage
{
}
================================================
FILE: NectarRCON/Entity/Config.cs
================================================
using System.Text.Json.Serialization;
namespace NectarRCON.Models;
public partial class Config
{
[JsonPropertyName("language")]
public string LanguageName { get; set; } = string.Empty;
[JsonPropertyName("theme")]
public ETheme Theme { get; set; }
[JsonPropertyName("command_list")]
public int CommandLimit;
}
================================================
FILE: NectarRCON/Entity/ETheme.cs
================================================
namespace NectarRCON.Models;
public enum ETheme : short
{
System = 0,
Dark = 1,
Light = 2
}
================================================
FILE: NectarRCON/Entity/Group.cs
================================================
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace NectarRCON.Models
{
public class Group
{
[JsonPropertyName("id")]
public required string Id { get; set; }
[JsonPropertyName("name")]
public required string Name { get; set; }
[JsonPropertyName("server_list")]
public required List Servers { get; set; }
}
}
================================================
FILE: NectarRCON/Entity/MainPageLoadValueMessage.cs
================================================
namespace NectarRCON.Models;
public partial class MainPageLoadValueMessage
{
public required bool IsLoaded { get; init; }
}
================================================
FILE: NectarRCON/Entity/ServerInformation.cs
================================================
using System.Text.Json.Serialization;
namespace NectarRCON.Models;
public class ServerInformation
{
[JsonPropertyName("name")]
public string Name { get; set; } = string.Empty;
[JsonPropertyName("address")]
public string Address { get; set; } = string.Empty;
[JsonPropertyName("adapter")]
public string Adapter { get; set; } = string.Empty; // 适配类名
[JsonPropertyName("port")]
public ushort Port { get; set; } = 0;
[JsonIgnore]
public string DisplayAddress
{
get => $"{Address}:{Port}";
}
}
================================================
FILE: NectarRCON/Entity/ServerPassword.cs
================================================
using System.Text.Json.Serialization;
namespace NectarRCON.Models;
public class ServerPassword
{
[JsonPropertyName("password")]
public string Password { get; set; } = string.Empty;
[JsonPropertyName("is_empty")]
public bool IsEmpty { get; set; }
[JsonPropertyName("name")]
public string ServerName { get; set; } = string.Empty;
}
================================================
FILE: NectarRCON/Interfaces/IConfigService.cs
================================================
using NectarRCON.Models;
namespace NectarRCON.Interfaces;
public interface IConfigService
{
void Save();
Config GetConfig();
}
================================================
FILE: NectarRCON/Interfaces/IConnectingDialogService.cs
================================================
using System.Windows.Controls;
namespace NectarRCON.Interfaces;
public interface IConnectingDialogService
{
void SetDialog(Grid grid);
void Show();
void Close();
}
================================================
FILE: NectarRCON/Interfaces/IGroupService.cs
================================================
using NectarRCON.Models;
using System.Collections.Generic;
namespace NectarRCON.Interfaces;
public interface IGroupService
{
///
/// 获取所有组
///
IReadOnlyList GetGroups();
///
/// 通过组名字获取组
///
/// 组名
Group? FindGroup(string name);
///
/// 通过组Id获取组
///
/// 组Id
Group? GetGroup(string groupId);
///
/// 删除组
///
/// 组Id
void Delete(string groupId);
///
/// 添加一个组
///
string Add(Group group);
}
================================================
FILE: NectarRCON/Interfaces/IHistoryService.cs
================================================
namespace NectarRCON.Interfaces;
public class HistoryNode
{
public string? Cmd { get; set; }
public HistoryNode? Prev { get; set; }
public HistoryNode? Next { get; set; }
}
public interface IHistoryService
{
///
/// 获取前一条命令
///
///
///
HistoryNode? Prev(HistoryNode? current);
///
/// 获取后一条命令
///
///
///
HistoryNode? Next(HistoryNode? current);
///
/// 输入命令
///
///
HistoryNode? InputCmd(string cmd);
}
================================================
FILE: NectarRCON/Interfaces/ILanguageService.cs
================================================
using System.Collections.Generic;
using System.Windows;
namespace NectarRCON.Interfaces;
public interface ILanguageService
{
public void Refresh();
public ResourceDictionary GetSelectedLanguage();
public Dictionary GetLanguages();
public void SelectLanguage();
public void SelectLanguage(string languageName, bool name);
public string GetKey(string key);
}
================================================
FILE: NectarRCON/Interfaces/ILogService.cs
================================================
using NectarRCON.Models;
namespace NectarRCON.Interfaces;
public interface ILogService
{
string GetText();
string Log(string message);
void Clear();
void SetServer(ServerInformation server);
void SetGroup(string groupId);
}
================================================
FILE: NectarRCON/Interfaces/IMessageBoxService.cs
================================================
using System;
using System.Windows;
namespace NectarRCON.Interfaces
{
public interface IMessageBoxService
{
void Show(string message);
void Show(string message, string title);
MessageBoxResult Show(string message, string title, MessageBoxButton button);
void Show(string message, string title, MessageBoxImage image);
MessageBoxResult Show(string message, string title, MessageBoxButton button, MessageBoxImage image);
void Show(Exception exception, string? information);
}
}
================================================
FILE: NectarRCON/Interfaces/IRconConnectionInfoService.cs
================================================
using NectarRCON.Models;
using System.Collections.Generic;
namespace NectarRCON.Interfaces;
public interface IRconConnectionInfoService
{
///
/// 清除所有连接信息
///
void Clear();
///
/// 添加一个服务器
///
void AddInformation(string serverName);
///
/// 获取最后一个被添加的服务器
///
ServerInformation? GetLastInformation();
///
/// 获取所有的信息
///
IReadOnlyList GetInformation();
///
/// 是否拥有多个服务器信息
///
public bool HasMultipleInformation
=> GetInformation().Count > 1;
}
================================================
FILE: NectarRCON/Interfaces/IServerInformationService.cs
================================================
using NectarRCON.Models;
using System.Collections.Generic;
namespace NectarRCON.Interfaces;
public interface IServerInformationService
{
ServerInformation? GetServer(string name);
List GetServers();
void AddServer(ServerInformation server);
void Save();
void RemoveServer(string name);
void Update(string name, ServerInformation newInfo);
bool ServerIsExist(string name);
}
================================================
FILE: NectarRCON/Interfaces/IServerPasswordService.cs
================================================
using NectarRCON.Models;
namespace NectarRCON.Interfaces;
public interface IServerPasswordService
{
void Save();
bool IsExist(ServerInformation server);
void Set(ServerInformation server, string? password, bool? isEmpty);
void Remove(ServerInformation server);
ServerPassword? Get(ServerInformation server);
ServerInformation GetSelect();
void Select(ServerInformation server);
}
================================================
FILE: NectarRCON/Models/GroupModel.cs
================================================
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using NectarRCON.Interfaces;
using NectarRCON.Rcon;
using NectarRCON.Services;
using NectarRCON.ViewModels;
using NectarRCON.Windows;
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Net.Sockets;
using System.Security.Authentication;
using System.Threading.Tasks;
using System.Windows;
using System.Xml.Linq;
using Wpf.Ui.Mvvm.Contracts;
namespace NectarRCON.Models;
public partial class GroupModel:ObservableObject
{
private readonly IGroupService _groupService;
private readonly ILanguageService _languageService;
private readonly IRconConnectionInfoService _connectionInfoService;
private IRconConnection _rconConnection;
private readonly INavigationService _navigationService;
private readonly IMessageBoxService _messageService;
private readonly IConnectingDialogService _connectingDialogService;
private readonly ILogService _logService;
[ObservableProperty]
private string _id;
[ObservableProperty]
private GroupPageViewModel _baseModel;
[ObservableProperty]
private string _name;
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(ConnectCommand))]
private ObservableCollection _servers = new();
#pragma warning disable CS8618 // 在退出构造函数时,不可为 null 的字段必须包含非 null 值。请考虑声明为可以为 null。
public GroupModel(string name, GroupPageViewModel baseModel)
#pragma warning restore CS8618 // 在退出构造函数时,不可为 null 的字段必须包含非 null 值。请考虑声明为可以为 null。
{
_groupService = App.GetService();
_languageService = App.GetService();
_connectionInfoService = App.GetService();
_rconConnection = App.GetService(typeof(RconMultiConnection));
_navigationService = App.GetService();
_messageService = App.GetService();
_connectingDialogService = App.GetService();
_logService = App.GetService();
_baseModel = baseModel;
Name = name;
Id = _groupService.FindGroup(name)?.Id ?? throw new InvalidOperationException();
Refresh();
}
private void Refresh()
{
Group? group = _groupService.GetGroup(_id);
if (group != null)
{
Servers.Clear();
Name = group.Name;
foreach (var server in group.Servers)
{
Servers.Add(new(server, this));
}
}
}
[RelayCommand]
public void ItemRemove(string name)
{
Servers.Remove(Servers.Where(s => s.Name == name).First());
Save();
ConnectCommand.NotifyCanExecuteChanged();
}
private bool ConnectCommandCanExecute()
=> Servers.Count >= 1;
[RelayCommand(CanExecute = nameof(ConnectCommandCanExecute))]
public void Connect()
{
_logService.SetGroup(_id);
_connectionInfoService.Clear();
_connectingDialogService.Show();
foreach (var server in Servers)
{
_connectionInfoService.AddInformation(server.Name);
}
if (_connectionInfoService.HasMultipleInformation)
{
_rconConnection = App.GetService(typeof(RconMultiConnection));
}
else
{
_rconConnection = App.GetService(typeof(RconSingleConnection));
}
_navigationService.Navigate(0);
}
[RelayCommand]
public void Add()
{
JoinGroupWindow window = new();
foreach (var server in Servers)
{
window.AddBlackList(server.Name);
}
window.ShowDialog();
window.Close();
if (window.SelectedServer != null)
{
Servers.Add(new(window.SelectedServer, this));
Save();
}
ConnectCommand.NotifyCanExecuteChanged();
}
private void Save()
{
Group group = _groupService.GetGroup(_id)!;
group.Servers.Clear();
foreach (var server in Servers)
{
group.Servers.Add(server.Name);
}
_groupService.Delete(group.Id);
_groupService.Add(group);
Refresh();
}
}
================================================
FILE: NectarRCON/Models/GroupServerItemModel.cs
================================================
using CommunityToolkit.Mvvm.ComponentModel;
namespace NectarRCON.Models;
public partial class GroupServerItemModel : ObservableObject
{
[ObservableProperty]
private string _name;
[ObservableProperty]
private GroupModel _baseModel;
#pragma warning disable CS8618 // 在退出构造函数时,不可为 null 的字段必须包含非 null 值。请考虑声明为可以为 null。
public GroupServerItemModel(string name, GroupModel baseModel)
#pragma warning restore CS8618 // 在退出构造函数时,不可为 null 的字段必须包含非 null 值。请考虑声明为可以为 null。
{
Name = name;
BaseModel = baseModel;
}
}
================================================
FILE: NectarRCON/Models/ServerModel.cs
================================================
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using NectarRCON.Interfaces;
using NectarRCON.Rcon;
using NectarRCON.Services;
using NectarRCON.ViewModels;
using NectarRCON.Windows;
using System.Net.Sockets;
using System.Security.Authentication;
using System.Threading.Tasks;
using System.Windows;
using Wpf.Ui.Mvvm.Contracts;
namespace NectarRCON.Models
{
public partial class ServerModel : ObservableObject
{
private readonly IServerPasswordService _passwordService;
private readonly IServerInformationService _informationService;
private readonly IConnectingDialogService _connectingDialogService;
private readonly IRconConnectionInfoService _connectionInfoService;
private readonly INavigationService _navigationService;
private readonly IServerPasswordService _serverPasswordService;
private readonly ILogService _logService;
private readonly ILanguageService _languageService;
private readonly ServerInformation _information;
private readonly ServersPageViewModel _viewModel;
private readonly IRconConnection _rconConnection;
[ObservableProperty]
private string _name;
[ObservableProperty]
private string _address;
#pragma warning disable CS8618 // CS8618 你说的对,但是我已经初始化了
public ServerModel(ServerInformation information, ServersPageViewModel viewModel)
#pragma warning restore CS8618 // 在退出构造函数时,不可为 null 的字段必须包含非 null 值。请考虑声明为可以为 null。
{
_passwordService = App.GetService();
_informationService = App.GetService();
_languageService = App.GetService();
_connectingDialogService = App.GetService();
_rconConnection = App.GetService(typeof(RconSingleConnection));
_connectionInfoService = App.GetService();
_navigationService = App.GetService();
_serverPasswordService = App.GetService();
_logService = App.GetService();
_information = information;
_viewModel = viewModel;
Name = information.Name;
Address = $"{information.Address}:{information.Port}";
}
private void EditPass()
{
_passwordService.Select(_information);
EditPasswordWindow window = new EditPasswordWindow();
window.ShowDialog();
}
[RelayCommand]
public void EditPassword()
{
EditPass();
}
[RelayCommand]
public void Edit()
{
_passwordService.Select(_information);
EditServerWindow window = new EditServerWindow();
window.ShowDialog();
_viewModel.Refresh();
}
[RelayCommand]
public void Delete()
{
var result = MessageBox.Show(_languageService.GetKey("ui.server_page.confirm"), _languageService.GetKey("ui.server_page.menu.delete"), MessageBoxButton.YesNo, MessageBoxImage.Question);
if (result == MessageBoxResult.Yes)
{
_informationService.RemoveServer(Name);
_informationService.Save();
_passwordService.Remove(_information);
_passwordService.Save();
_viewModel.Refresh();
}
}
[RelayCommand]
public void Connect()
{
_logService.SetServer(_information);
if (_rconConnection.IsConnecting())
return;
if (_rconConnection.IsConnected())
{
_rconConnection.Close();
}
var server = _passwordService.Get(_information);
if (server == null)
{
EditPass();
}
else if (server.Password == null && !server.IsEmpty)
{
EditPass();
}
_connectionInfoService.Clear();
_connectionInfoService.AddInformation(Name);
_serverPasswordService.Select(_information);
_navigationService.Navigate(0);
}
}
}
================================================
FILE: NectarRCON/NectarRCON.csproj
================================================
WinExe
net7.0-windows
enable
true
Resources\Icon.ico
app.manifest
1.0.0-beta5
12
PreserveNewest
================================================
FILE: NectarRCON/Rcon/IRconConnection.cs
================================================
using NectarRCON.Models;
namespace NectarRCON.Rcon;
public delegate void MessageEvent(ServerInformation information, string message);
public delegate void RconEvent(ServerInformation information);
public interface IRconConnection
{
event MessageEvent OnMessage;
event RconEvent OnClosed;
event RconEvent OnConnected;
event RconEvent OnConnecting;
void Connect();
void Send(string command);
void Close();
bool IsConnected();
bool IsConnecting();
}
================================================
FILE: NectarRCON/Rcon/RconMultiConnection.cs
================================================
using NectarRCON.Core.Helper;
using NectarRCON.Export.Interfaces;
using NectarRCON.Interfaces;
using NectarRCON.Models;
using NectarRCON.Rcon;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Sockets;
using System.Security.Authentication;
using System.Windows.Controls;
using NectarRCON.Dp;
using Serilog;
namespace NectarRCON.Services
{
///
/// Rcon多客户端连接服务
///
internal class RconMultiConnection(
IServerPasswordService serverPasswordService,
ILanguageService languageService,
IRconConnectionInfoService rconConnectionInfoService,
IMessageBoxService messageBoxService)
: IRconConnection, IDisposable
{
private readonly RconSettingsDp _settingsDp = DpFile.LoadSingleton();
public event MessageEvent? OnMessage;
public event RconEvent? OnClosed;
public event RconEvent? OnConnected;
public event RconEvent? OnConnecting;
private readonly Dictionary _connections = new();
private bool _isConnecting;
public void Close()
{
foreach(var connection in _connections)
{
connection.Value.Dispose();
}
_connections.Clear();
}
public void Connect()
{
Close();
_isConnecting = true;
var servers = rconConnectionInfoService.GetInformation();
try
{
foreach (ServerInformation info in servers)
{
OnConnecting?.Invoke(info);
// 准备开始连接,先解析这个地址有没有SRV记录
string address = DNSHelpers.SRVQuery(info.Address);
if (string.IsNullOrEmpty(address)) // 如果没有SRV记录,就按照原来的样子设置服务器
address = $"{info.Address.Replace("localhost", "127.0.0.1")}:{info.Port}";
ServerPassword? serverPassword = serverPasswordService.Get(info); // 从设置中读取Rcon密码
string password = serverPassword?.Password ?? string.Empty;
// 创建对应的Rcon客户端实例
// 目前支支持了Minecraft,后期会支持更多(嘛..主要是懒)
IRconAdapter adapter = AdapterHelpers.CreateAdapterInstance(info.Adapter)
?? throw new InvalidOperationException($"adapter not found: {info.Adapter}");
var host = address.Split(":")[0];
var port = int.Parse(address.Split(":")[1]);
Log.Information("[RconMultiConnection] Connecting to {host}:{port}", host, port);
try
{
adapter.Connect(host, port);
if (!adapter.Authenticate(password))
throw new AuthenticationException($"Server: \"{info.Name}\"\n");
OnConnected?.Invoke(info);
}
catch (AuthenticationException)
{
throw;
}
catch (Exception ex)
{
OnClosed?.Invoke(info);
messageBoxService.Show(ex, $"Server: \"{info.Name}\"");
}
//设置编码
adapter.SetEncoding(_settingsDp.Encoding.GetEncoding());
_connections.Add(info, adapter);
}
}
finally
{
_isConnecting = false;
}
}
public bool IsConnected()
=> _connections.Any(e => e.Value.IsConnected);
public bool IsConnecting()
=> _isConnecting;
public void Send(string command)
{
foreach(var value in _connections)
{
IRconAdapter connection = value.Value;
ServerInformation info = value.Key;
if (connection.IsConnected)
{
try
{
string result = connection.Run(command);
OnMessage?.Invoke(info, result);
}catch (IOException)
{
connection.Dispose(); // 内部会调用Close
OnClosed?.Invoke(info);
OnMessage?.Invoke(info, languageService.GetKey("service.rcon.offline"));
}
}
else
{
OnMessage?.Invoke(info, languageService.GetKey("service.rcon.offline"));
}
}
}
public void Dispose()
{
Close();
}
}
}
================================================
FILE: NectarRCON/Rcon/RconSingleConnection.cs
================================================
using NectarRCON.Core.Helper;
using NectarRCON.Export.Interfaces;
using NectarRCON.Interfaces;
using NectarRCON.Models;
using NectarRCON.Rcon;
using System;
using System.IO;
using System.Net.Sockets;
using System.Security.Authentication;
using System.Windows;
using NectarRCON.Dp;
using Serilog;
namespace NectarRCON.Services;
public class RconSingleConnection(
IServerPasswordService serverPasswordService,
ILanguageService languageService,
IRconConnectionInfoService rconConnectionInfoService)
: IRconConnection
{
private readonly RconSettingsDp _settingsDp = DpFile.LoadSingleton();
private IRconAdapter? _rconClient
= null;
private bool _connecting;
public event MessageEvent? OnMessage;
public event RconEvent? OnClosed;
public event RconEvent? OnConnected;
public event RconEvent? OnConnecting;
private ServerInformation _serverInformation = new();
public void Close()
{
lock (this)
{
if (IsConnected() && !IsConnecting())
{
_connecting = false;
_rconClient?.Dispose();
_rconClient = null;
OnClosed?.Invoke(_serverInformation);
}
}
}
public void Connect()
{
ServerInformation info = rconConnectionInfoService.GetLastInformation() ?? throw new ArgumentNullException("Internal error, please try again");
_connecting = true;
OnConnecting?.Invoke(info);
try
{
if (IsConnected() && _rconClient != null)
Close();
// 准备开始连接,先解析这个地址有没有SRV记录
string address = DNSHelpers.SRVQuery(info.Address);
if (string.IsNullOrEmpty(address)) // 如果没有SRV记录,就按照原来的样子设置服务器
address = $"{info.Address.Replace("localhost", "127.0.0.1")}:{info.Port}";
ServerPassword? serverPassword = serverPasswordService.Get(info);
string password = serverPassword?.Password ?? string.Empty;
// 创建对应的Rcon客户端实例
// 目前支支持了Minecraft,后期会支持更多(嘛..主要是懒)
_rconClient = AdapterHelpers.CreateAdapterInstance(info.Adapter)
?? throw new InvalidOperationException($"adapter not found: {info.Adapter}");
_rconClient.SetEncoding(_settingsDp.Encoding.GetEncoding());
var host = address.Split(":")[0];
var port = int.Parse(address.Split(":")[1]);
Log.Information("[RconSingleConnection] Connecting to {host}:{port}", host, port);
_rconClient.Connect(host, port); // 连接
if (!_rconClient.Authenticate(password))
throw new AuthenticationException();
OnConnected?.Invoke(info);
_serverInformation = info;
}
catch (FormatException ex)
{
MessageBox.Show(languageService.GetKey("ui.text.format_exception")
.Replace("%s", ex.Message), languageService.GetKey("text.error"), MessageBoxButton.OK, MessageBoxImage.Error);
}
finally
{
_connecting = false;
}
}
public bool IsConnected()
=> _rconClient?.IsConnected ?? false;
public bool IsConnecting()
=> _connecting;
public void Send(string command)
{
if (IsConnected() && _rconClient != null)
{
try
{
string result = _rconClient.Run(command);
OnMessage?.Invoke(_serverInformation, result);
}
catch (Exception ex)
{
Close();
if (ex is SocketException or IOException && _settingsDp.AutoReconnect)
{
Log.Information("Rcon连接已经断开,开始自动重连...");
try
{
Connect();
}
catch (Exception e)
{
Log.Error("自动重连失败 {err}", e);
}
if (IsConnected())
{
try
{
Log.Information("尝试重发命令...");
string result = _rconClient.Run(command);
OnMessage?.Invoke(_serverInformation, result);
Log.Information("重发命令成功 -> {command}", command);
return;
}
catch(Exception e)
{
Log.Error("尝试重发命令失败 {err}", e);
}
}
}
MessageBox.Show($"{languageService.GetKey("text.error")}\n{ex.Message}", ex.GetType().FullName, MessageBoxButton.OK, MessageBoxImage.Error);
}
}
else
{
Close();
MessageBox.Show($"{languageService.GetKey("text.server.not_connect.text")}", languageService.GetKey("text.error"), MessageBoxButton.OK, MessageBoxImage.Error);
}
}
}
================================================
FILE: NectarRCON/Resources/Languages/en_us.xaml
================================================
en_us
English US(en_us)
Server
Servers
Groups
Log
Clear
Program
Settings
About
CheckUpdate
RCONManager
UI Language
UI Settings
UI Theme
Dark Theme
Light Theme
System Theme
Rcon passwords...
Command Record Limit
Rcon
Auto Reconnect
KeepConnectionWindowOpen
Encoding
Clear Program Logs
Do you want to clear program logs?
Connect
Edit
Delete
EditPassword
Servers
Add Server
Search by server name...
This action will delete the server! (including recorded password)\nAre you sure you want to delete?
Add Server
Name
Address
Port
Add
Cancel
A server with the same name already exists, please change your name!
Server name or server address cannot be empty, please change it!
Afdian
MCBBS
Github
An error was encountered while parsing the destination address: %s\nPlease check whether the server address is legal!
Edit Password
Password
Empty Pass
Run
Successful!
Offline
All clients are offline
Reconnect
Back to Servers
Edit
An exception occurred during operation, and the application is about to end. If you cannot understand the error below, please submit an issue on Github and describe what happened in detail
Error
Warning
Information
Ok
Cancel
Not Available
Connect
Remove
Delete
Add
Connecting...
Failed to connect to the server!
Unable to connect to the remote server: %s\nPlease check that the server RCON address is configured correctly, and check that the server has enabled the RCON option!
The Rcon password is incorrect, please check whether your server password is correct!
Connected to the server and authenticated successfully!
The connection is disconnected!
Starting connection
You cannot connect to the server, so you cannot use the remote command function, please try to go back to the server list and reconnect to this server!
Unable to instantiate server list /servers.json %s\nClick "Yes" to reset the server list (you need to re-add the server, but the recorded password will not disappear)\nClick "Cancel" to close the program!
Unable to instantiate server list /servers.json %s\nClick "Yes" to close the program!
Unable to save file /servers.json\nIf you just added a server, please note that this save is not in effect!\n%s
Unable to resolve password list /passwords.json %s\nClick "Yes" to reset the password list (password needs to be reset, but the recorded server will not disappear)\nClick "Cancel" to close the program!
Unable to resolve password list /passwords.json %s\nClick "Yes" to close the program!
Unable to save file /passwords.json\nIf you have just set a password, please note that this save is not in effect!\n%s
Unable to resolve configuration file /config.json JSON parsing failed: %s\nClick "Yes" to reset the profile!\nClick "No" to exit the program!
Unable to parse configuration file /config.json: %s\nClick "Yes" to reset the profile!\nClick "No" to exit the program!
Unable to save configuration file /config.json: %s\nIf you have just changed the configuration file, please note that the save does not take effect!
An error occurred while trying to read and parse the group file "{0}", trying to delete or repair the file may resolve this error!
The same GroupId already exists (existing value: {0}, duplicate value: {1})
The same Name already exists (existing id: {0}, duplicate id: {1})
A group with the same internal ID already exists in the file, try creating it again
The {0} file name does not match the internal ID {1}
The same Name already exists
Click '+ Add' to add a group!
After adding a group, you can broadcast a command to all servers in that group simultaneously.
This group has no servers. Click '+ Add' to add a server.
Do you want to delete group {0}?
New Group
Group Name
Select Server
No server available
Offline
================================================
FILE: NectarRCON/Resources/Languages/zh_cn.xaml
================================================
zh_cn
简体中文(zh_cn)
服务器
服务器列表
服务器分组
日志
清空日志
程序
设置
关于
检查更新
RCON管理器
UI语言
界面设置
UI主题
深色主题
浅色主题
跟随系统
Rcon密码管理
服务器命令回溯记录数
Rcon全局设置
掉线自动重连
掉线不回到主页
文本编码
清理程序日志
是否清理程序日志?
连接
编辑
删除
密码
服务器列表
添加服务器
输入服务器名字来搜索...
此操作将会删除服务器! (包括记录的密码)\n您确定要删除吗?
添加服务器
服务器名称
服务器地址
服务器端口
新建
取消
相同名字的服务器已存在,请换一个名字!
服务器名称或服务器地址不可为空,请更改!
爱发电
MCBBS
Github
解析目标地址时遇到错误: %s\n请检查服务器地址是否合法!
编辑密码
密码
无需密码
执行
执行成功
已掉线
所有客户端都掉线了
重新连接
回到主页
编辑
运行过程中出现异常,应用即将结束。 如果您无法理解以下错误,请在 Github 上提交问题并详细描述发生的情况
错误
警告
信息
确定
取消
不可使用
连接
移除
删除
添加
正在连接服务器...
连接服务器失败!
无法连接到远程服务器: %s\n请检查服务器RCON地址配置是否正确,以及检查服务端是否开启了RCON选项!
Rcon密码错误,请检查您的服务器密码是否正确!
连接服务器并认证成功!
连接已断开!
开始连接
无法连接到服务器,因此无法使用远程命令功能,请尝试回到服务器列表重新连接到本服务器!
无法解析服务器列表 /servers.json %s\n点击"是"重置服务器列表(需重新添加服务器,但记录的密码不会消失)\n点击"取消"关闭程序!
无法解析服务器列表 /servers.json %s\n点击"是"关闭程序!
无法保存文件 /servers.json\n如果您刚刚进行了添加服务器操作,请注意,本次保存并未生效!\n%s
无法解析密码列表 /passwords.json %s\n点击"是"重置密码列表(需重新设置密码,但记录的服务器不会消失)\n点击"取消"关闭程序!
无法解析密码列表 /passwords.json %s\n点击"是"关闭程序!
无法保存文件 /passwords.json\n如果您刚刚进行了设置密码操作,请注意,本次保存并未生效!\n%s
无法解析配置文件 /config.json JSON解析失败: %s\n点击"是"重置配置文件!\n点击"否"退出程序!
无法解析配置文件 /config.json: %s\n点击"是"重置配置文件!\n点击"否"退出程序!
无法保存配置文件 /config.json: %s\n如果您刚刚更改了配置文件,请注意,保存并未生效!
读取和解析组文件"{0}"时发生错误,请尝试删除或修复文件以解决此错误!
相同的 GroupId 已存在(现有值: {0},重复值: {1})
相同的 Name 已存在(现有Id: {0},重复Id: {1})
文件中已存在具有相同内部 ID 的分组,请再次尝试创建
{0} 文件名与内部 ID {1} 不匹配
相同的组名已存在
单击'+ 添加'添加一个组吧!
添加组后,可以将命令同时广播到该组的所有服务器
该组没有任何服务器,请单击 '+ 添加' 添加一个服务器吧!
是否要删除组 {0}?
新建组
组名
选择服务器
没有可用服务器
离线
================================================
FILE: NectarRCON/Resources/Languages/zh_tw.xaml
================================================
zh_tw
繁体中文(zh_tw)
伺服器
伺服器列表
伺服器分組
日誌
清空日誌
程式
設定
關於
檢查更新
RCON管理器
UI語言
界面設定
UI主題
深色主題
淺色主題
跟隨系統
Rcon密碼管理
伺服器命令回溯記錄數
Rcon全局設定
斷綫自動重連
斷線不回到主頁
文本編碼
清除程式日誌
您是否要清除程式日誌?
連線
編輯
刪除
密碼
伺服器列表
新增伺服器
輸入伺服器名稱來搜尋...
此操作將會刪除伺服器! (包括記錄的密碼)\n您確定要刪除嗎?
新增伺服器
伺服器名稱
伺服器地址
伺服器端口
新增
取消
相同名稱的伺服器已經存在,請換一個名稱!
伺服器名稱或伺服器地址不可為空,請更改!
爱发电
MCBBS
Github
解析目标地址時遇到錯誤:%s\n請檢查伺服器地址是否合法!
編輯密碼
密碼
無需密碼
執行
執行成功
斷線
所有客戶端都斷線了
重新連接
返回伺服器
編輯
執行過程中出現異常,應用即將結束。如果您無法理解以下錯誤,請在 GitHub 上提交問題並詳細描述發生的情況。
錯誤
警告
資訊
確定
取消
不可使用
連接
移除
刪除
新增
正在連接伺服器...
連接伺服器失敗!
無法連接到遠程伺服器:%s\n請檢查伺服器RCON地址配置是否正確,以及檢查服務端是否開啟了RCON選項!
Rcon密碼錯誤,請檢查您的伺服器密碼是否正確!
連接伺服器並認證成功!
連接已斷開!
開始連接
無法連接到伺服器,因此無法使用遠程命令功能,請嘗試回到伺服器列表重新連接到本伺服器!
無法解析伺服器列表 /servers.json %s\n點選"是"重置伺服器列表(需重新添加伺服器,但記錄的密碼不會消失)\n點選"取消"關閉程式!
無法解析伺服器列表 /servers.json %s\n點選"是"關閉程式!
無法儲存檔案 /servers.json\n如果您剛剛進行了添加伺服器操作,請注意,本次儲存並未生效!\n%s
無法解析密碼列表 /passwords.json %s\n點選"是"重置密碼列表(需重新設定密碼,但記錄的伺服器不會消失)\n點選"取消"關閉程式!
無法解析密碼列表 /passwords.json %s\n點選"是"關閉程式!
無法儲存檔案 /passwords.json\n如果您剛剛進行了設定密碼操作,請注意,本次儲存並未生效!\n%s
無法解析配置檔案 /config.json JSON解析失敗:%s\n點選"是"重置配置檔案!\n點選"否"退出程式!
無法解析配置檔案 /config.json:%s\n點選"是"重置配置檔案!\n點選"否"退出程式!
無法儲存配置檔案 /config.json:%s\n如果您剛剛更改了配置檔案,請注意,儲存並未生效!
讀取和解析組檔案"{0}"時發生錯誤,請嘗試刪除或修復檔案以解決此錯誤!
相同的 GroupId 已存在(現有值: {0},重複值: {1})
相同的 Name 已存在(現有Id: {0},重複Id: {1})
檔案中已存在具有相同內部 ID 的分組,請再次嘗試建立
{0} 檔案名與內部 ID {1} 不匹配
相同的組名已存在
點擊「+添加」添加一個分組吧!
添加分組後,您可以將命令廣播到該分組的所有伺服器
該分組沒有任何伺服器,請點擊「+添加」添加一個伺服器吧!
是否要刪除分組 {0}?
新建分組
分組名稱
選擇伺服器
沒有可用的伺服器
離線
================================================
FILE: NectarRCON/Services/ApplicationHostService.cs
================================================
using Microsoft.Extensions.Hosting;
using NectarRCON.Core.Helper;
using NectarRCON.Interfaces;
using NectarRCON.Models;
using System;
using System.ComponentModel;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using Microsoft.Extensions.Logging;
using Serilog;
using Wpf.Ui.Mvvm.Contracts;
namespace NectarRCON.Services;
public class ApplicationHostService(
IServiceProvider serviceProvider,
INavigationService navigationService,
IThemeService themeService,
ILanguageService languageService,
IConfigService configService)
: IHostedService
{
private readonly INavigationService _navigationService = navigationService;
private INavigationWindow? _navigationWindow;
public async Task StartAsync(CancellationToken cancellationToken)
{
Log.Information("Starting NectarRCON...");
await HandleActivationAsync();
}
public async Task StopAsync(CancellationToken cancellationToken)
{
await Task.CompletedTask;
}
private void LoadConfig()
{
switch (configService.GetConfig().Theme)
{
case ETheme.System:
themeService.SetTheme(Win32Helper.GetWindowsTheme() ? Wpf.Ui.Appearance.ThemeType.Dark : Wpf.Ui.Appearance.ThemeType.Light);
break;
case ETheme.Dark:
themeService.SetTheme(Wpf.Ui.Appearance.ThemeType.Dark);
break;
case ETheme.Light:
themeService.SetTheme(Wpf.Ui.Appearance.ThemeType.Light);
break;
default:
break;
}
if (configService.GetConfig().LanguageName == string.Empty)
{
languageService.SelectLanguage();
configService.GetConfig().LanguageName = languageService.GetKey("file.language");
configService.Save();
}
else
{
languageService.SelectLanguage(configService.GetConfig().LanguageName, false);
}
}
private async Task HandleActivationAsync()
{
await Task.CompletedTask;
if (!Application.Current.Windows.OfType().Any())
{
_navigationWindow = (serviceProvider.GetService(typeof(INavigationWindow)) as INavigationWindow)!;
Log.Information("Show MainWindow...");
_navigationWindow!.ShowWindow();
}
LoadConfig();
await Task.CompletedTask;
}
}
================================================
FILE: NectarRCON/Services/ConfigService.cs
================================================
using NectarRCON.Interfaces;
using NectarRCON.Models;
using System;
using System.Diagnostics;
using System.IO;
using System.Text.Json;
using System.Windows;
namespace NectarRCON.Services;
public partial class ConfigService : IConfigService
{
private ILanguageService _languageService;
private Config _config = new();
public ConfigService(ILanguageService languageService)
{
_languageService = languageService;
if (!File.Exists("./config.json"))
Save();
try
{
_config = JsonSerializer.Deserialize(File.ReadAllText("./config.json"))
?? new Config();
}
catch (Exception ex)
{
var resutl = MessageBox.Show(_languageService.GetKey(ex.GetType() == typeof(JsonException) ? "text.config.json_error" : "text.config.error")
.Replace("%s", ex.Message), _languageService.GetKey("text.error"), MessageBoxButton.YesNo, MessageBoxImage.Error);
if (resutl == MessageBoxResult.Yes)
{
_config = new();
Save();
}
else
{
Process.GetCurrentProcess().Kill();
}
}
}
public Config GetConfig()
=> _config;
public void Save()
{
try
{
File.WriteAllText("./config.json", JsonSerializer.Serialize(_config));
}
catch (Exception ex)
{
MessageBox.Show(_languageService.GetKey("text.config.save_error")
.Replace("%s", ex.Message), _languageService.GetKey("text.error"), MessageBoxButton.OK, MessageBoxImage.Error);
}
}
}
================================================
FILE: NectarRCON/Services/ConnectingDialogService.cs
================================================
using NectarRCON.Interfaces;
using System.Windows;
using System.Windows.Controls;
namespace NectarRCON.Services;
internal class ConnectingDialogService : IConnectingDialogService
{
private Grid? _dialog;
public void Close()
{
if (null == _dialog)
return;
_dialog.Visibility = Visibility.Hidden;
}
public void SetDialog(Grid grid)
{
_dialog = grid;
}
public void Show()
{
if (null == _dialog)
return;
_dialog.Visibility = Visibility.Visible;
}
}
================================================
FILE: NectarRCON/Services/GroupService.cs
================================================
using NectarRCON.Interfaces;
using NectarRCON.Models;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.Json;
namespace NectarRCON.Services
{
public class GroupService : IGroupService
{
private static string _groupPath = Path.Combine("./groups");
private readonly IMessageBoxService _messageBoxService;
private readonly ILanguageService _languageService;
private readonly Dictionary _groupMapping = new(); // id, group
private readonly Dictionary _groupNameMapping = new(); // name, group
public GroupService(IMessageBoxService messageBoxService, ILanguageService languageService)
{
_messageBoxService = messageBoxService;
Refresh();
_languageService = languageService;
}
private void Refresh()
{
_groupMapping.Clear();
_groupNameMapping.Clear();
if (Directory.Exists(_groupPath))
{
string[] jsonFiles = Directory.GetFiles(_groupPath, "*.json");
foreach (string jsonFile in jsonFiles)
{
try
{
using (FileStream fs = File.OpenRead(jsonFile))
{
Group group = JsonSerializer.Deserialize(fs) ?? throw new InvalidDataException(jsonFile);
if (group.Id.ToLower() != Path.GetFileNameWithoutExtension(jsonFile).ToLower())
throw new InvalidDataException(string.Format(_languageService.GetKey("groups.exception.file_name_mismatch_exception"), jsonFile, group.Id));
if (_groupMapping.ContainsKey(group.Id))
throw new InvalidDataException(string.Format(_languageService.GetKey("groups.exception.same_group_id_exception"), _groupMapping[group.Id].Name, group.Name));
if (_groupNameMapping.ContainsKey(group.Name))
throw new InvalidDataException(string.Format(_languageService.GetKey("groups.exception.same_name_exception"), _groupMapping[group.Id].Id, group.Id));
_groupMapping.Add(group.Id, group);
_groupNameMapping.Add(group.Name, group);
}
}
catch (Exception ex)
{
_messageBoxService.Show(ex.Message, string.Format(_languageService.GetKey("groups.exception.invalid_data_exception"), ex.ToString()));
}
}
}
else
{
Directory.CreateDirectory(_groupPath);
}
}
private void RemoveGroupInDict(string groupId)
{
if (_groupMapping.TryGetValue(groupId, out Group? group))
{
_groupMapping.Remove(groupId);
if (_groupNameMapping.ContainsKey(group.Name))
{
_groupNameMapping.Remove(group.Name);
}
}
}
public string Add(Group group)
{
string filePath = Path.Combine(_groupPath, group.Id + ".json");
if (!File.Exists(filePath))
{
if (_groupNameMapping.ContainsKey(group.Name))
throw new InvalidOperationException(_languageService.GetKey("groups.exception.name_already_exists_exception"));
if (_groupMapping.ContainsKey(group.Id))
throw new InvalidOperationException(_languageService.GetKey("groups.exception.group_id_exists_exception"));
File.WriteAllBytes(filePath, Encoding.UTF8.GetBytes(JsonSerializer.Serialize(group)));
_groupMapping.Add(group.Id, group);
_groupNameMapping.Add(group.Name,group);
return group.Id;
}
throw new InvalidOperationException(_languageService.GetKey("groups.exception.group_id_exists_exception"));
}
public void Delete(string groupId)
{
string filePath = Path.Combine(_groupPath, groupId + ".json");
if (File.Exists(filePath))
{
File.Delete(filePath);
RemoveGroupInDict(groupId);
}
}
public Group? FindGroup(string name)
{
_groupNameMapping.TryGetValue(name, out Group? group);
return group;
}
public Group? GetGroup(string groupId)
{
_groupMapping.TryGetValue(groupId, out Group? group);
return group;
}
public IReadOnlyList GetGroups()
=> _groupMapping.Select(s => s.Value).ToList();
}
}
================================================
FILE: NectarRCON/Services/HistoryService.cs
================================================
using NectarRCON.Interfaces;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
namespace NectarRCON.Services;
public class HistoryService : IHistoryService
{
///
/// 双向链表保存历史记录
///
private HistoryNode? _head;
private HistoryNode? _tail;
private readonly Dictionary _map;
///
/// 限制记录条数
///
private int _limit = 20;
public HistoryService()
{
_map = [];
if (!Path.Exists("./logs"))
{
Directory.CreateDirectory("./logs");
}
var stream = File.Open($"./logs/history", FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read);
using StreamReader reader = new StreamReader(stream);
string? line;
while ((line = reader.ReadLine()) != null)
{
line = line.Trim();
if (!string.IsNullOrEmpty(line))
{
AppendNode(line);
}
}
}
public HistoryNode? InputCmd(string cmd)
{
var node = AppendNode(cmd);
Save();
return node;
}
private HistoryNode? AppendNode(string cmd)
{
cmd = cmd.Trim();
HistoryNode node;
if (_map.ContainsKey(cmd))
{
// 已存在的记录,将其移动到链表尾部
node = _map[cmd];
if (node == _tail)
{
return null;
}
else if (node == _head)
{
_head = _head.Next;
}
if (node.Prev != null)
{
node.Prev.Next = node.Next;
}
if (node.Next != null)
{
node.Next.Prev = node.Prev;
}
node.Prev = null;
node.Next = null;
}
else
{
node = new HistoryNode()
{
Cmd = cmd,
};
_map.Add(cmd, node);
}
if (_head == null || _tail == null)
{
_head = _tail = node;
}
else
{
_tail.Next = node;
node.Prev = _tail;
_tail = node;
}
// 超出限制,移除链表头部记录
if (_map.Count() > _limit)
{
var head = _head;
_head = _head.Next;
if (_head != null)
{
_head.Prev = null;
}
if (!string.IsNullOrEmpty(head.Cmd))
{
_map.Remove(head.Cmd);
}
}
return null;
}
public HistoryNode? Next(HistoryNode? current)
{
if (current == null)
{
return null;
}
return current.Next;
}
public HistoryNode? Prev(HistoryNode? current)
{
if (current == null)
{
return _tail;
}
else if (current.Prev == null)
{
return current;
}
return current.Prev;
}
private void Save()
{
using var stream = File.Open($"./logs/history", FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read);
stream.Seek(0, SeekOrigin.Begin);
HistoryNode? node = _head;
while (node != null) {
if (!string.IsNullOrEmpty(node.Cmd))
{
string cmd = node.Cmd + "\n";
stream.Write(Encoding.UTF8.GetBytes(cmd));
}
node = node.Next;
}
}
}
================================================
FILE: NectarRCON/Services/LanguageService.cs
================================================
using NectarRCON.Interfaces;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Windows;
namespace NectarRCON.Services;
public class LanguageService : ILanguageService
{
private readonly Dictionary _defaultLanguages = new();
private Dictionary _languages = new();
private ResourceDictionary _selectedLanguage = new ResourceDictionary()
{
Source = new Uri("pack://application:,,,/NectarRCON;component/Resources/Languages/zh_cn.xaml", UriKind.RelativeOrAbsolute)
};
private readonly ResourceDictionary _defLanguage = new ResourceDictionary()
{
Source = new Uri("pack://application:,,,/NectarRCON;component/Resources/Languages/zh_cn.xaml", UriKind.RelativeOrAbsolute)
};
public LanguageService()
{
Refresh();
}
public ResourceDictionary SelectedLanguage
{
get => _selectedLanguage;
}
public Dictionary Languages
{
get => _languages;
}
public Dictionary GetLanguages()
=> Languages;
public ResourceDictionary GetSelectedLanguage()
{
return _selectedLanguage;
}
public void Refresh()
{
_languages.Clear();
// 从本地目录获取xaml
string[] files = new string[0];
if (Directory.Exists("./languages/"))
files = Directory.GetFiles("./languages/");
foreach (string file in files)
{
if (Path.GetExtension(file).ToLower() == ".xaml" || Path.GetExtension(file).ToLower() == ".xml")
{
ResourceDictionary resourceDictionary = new ResourceDictionary();
using (FileStream fs = File.OpenRead(file))
{
resourceDictionary = (ResourceDictionary)System.Windows.Markup.XamlReader.Load(fs);
}
_languages.Add(Path.GetFileNameWithoutExtension(file), resourceDictionary);
}
}
// 从内部文件加载
_defaultLanguages.Add("zh_cn", "pack://application:,,,/NectarRCON;component/Resources/Languages/zh_cn.xaml");
_defaultLanguages.Add("zh_tw", "pack://application:,,,/NectarRCON;component/Resources/Languages/zh_tw.xaml");
_defaultLanguages.Add("en_us", "pack://application:,,,/NectarRCON;component/Resources/Languages/en_us.xaml");
foreach (KeyValuePair language in _defaultLanguages)
{
if (_languages.ContainsKey(language.Key))
_languages.Remove(language.Key);
Languages.Add(language.Key, new ResourceDictionary()
{
Source = new Uri(language.Value, UriKind.RelativeOrAbsolute)
});
}
}
public void SelectLanguage()
{
Application.Current.Resources.Remove(SelectedLanguage);
string name = System.Globalization.CultureInfo.CurrentCulture.Name;
SelectLanguage(name);
}
public void SelectLanguage(string languageName, bool name = false)
{
languageName = languageName.Replace("-", "_");
Application.Current.Resources.MergedDictionaries.Remove(SelectedLanguage);
var language = Languages.Where(l =>
{
if (name)
{
return (l.Value["file.name"].ToString() ?? "nullname") == languageName.ToLower();
}
else
{
return l.Key.ToLower() == languageName.ToLower();
}
}).FirstOrDefault();
if (null == language.Value)
{
_selectedLanguage = new ResourceDictionary()
{
Source = new Uri("pack://application:,,,/NectarRCON;component/Resources/Languages/en_us.xaml", UriKind.RelativeOrAbsolute)
};
if (Application.Current.Resources.Contains("en_us"))
{
Application.Current.Resources.Remove("en_us");
}
Application.Current.Resources.MergedDictionaries.Add(_selectedLanguage);
}
else
{
_selectedLanguage = language.Value;
Application.Current.Resources.MergedDictionaries.Add(language.Value);
}
}
public string GetKey(string key)
{
string valuel;
if (_selectedLanguage.Contains(key))
valuel = _selectedLanguage[key].ToString() ?? string.Empty;
else if (_defLanguage.Contains(key))
valuel = _defLanguage[key].ToString() ?? string.Empty;
else valuel = key;
return valuel
.Replace("\\n", "\n");
}
}
================================================
FILE: NectarRCON/Services/LogService.cs
================================================
using NectarRCON.Interfaces;
using NectarRCON.Models;
using System;
using System.IO;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.RegularExpressions;
using System.Windows;
namespace NectarRCON.Services;
public class LogService : ILogService
{
private FileStream? _logFileStream;
public LogService()
{
if (!Path.Exists("./logs"))
Directory.CreateDirectory("./logs");
}
public void Clear()
{
_logFileStream?.SetLength(0);
_logFileStream?.Flush();
}
public string GetText()
{
using (MemoryStream ms = new MemoryStream())
{
_logFileStream?.CopyTo(ms);
return Encoding.UTF8.GetString(ms.ToArray());
}
}
public string Log(string message)
{
string logText = $"[{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}] {message}\n";
_logFileStream?.Write(Encoding.UTF8.GetBytes(logText));
_logFileStream?.Flush();
return logText;
}
public void SetServer(ServerInformation server)
{
OpenFile("server_" + UrlEncoder.Default.Encode(server.Name));
}
public void SetGroup(string groupId)
{
OpenFile("group_" + UrlEncoder.Default.Encode(groupId));
}
private void OpenFile(string fileName)
{
if (!File.Exists($"./logs/{fileName}.log"))
File.Create($"./logs/{fileName}.log").Close();
_logFileStream?.Close();
_logFileStream?.Dispose();
_logFileStream = File.Open($"./logs/{fileName}.log", FileMode.Open, FileAccess.ReadWrite, FileShare.Read);
}
}
================================================
FILE: NectarRCON/Services/MessageBoxService.cs
================================================
using NectarRCON.Interfaces;
using System;
using System.Windows;
namespace NectarRCON.Services;
public class MessageBoxService : IMessageBoxService
{
private readonly ILanguageService _languageService;
public MessageBoxService(ILanguageService languageService)
{
_languageService = languageService;
}
public void Show(string message)
=> MessageBox.Show(message, "NectarRcon", MessageBoxButton.OK, MessageBoxImage.Information);
public void Show(string message, string title)
=> MessageBox.Show(message, title, MessageBoxButton.OK, MessageBoxImage.Information);
public MessageBoxResult Show(string message, string title, MessageBoxButton button)
=> MessageBox.Show(message, title, button, MessageBoxImage.Information);
public void Show(string message, string title, MessageBoxImage image)
=> MessageBox.Show(message, title, MessageBoxButton.OK, image);
public MessageBoxResult Show(string message, string title, MessageBoxButton button, MessageBoxImage image)
=> MessageBox.Show(message, title, button, image);
public void Show(Exception exception, string? information = null)
=> MessageBox.Show($"{_languageService.GetKey("text.went_wrong")}\n{information}\n{exception}", "error", MessageBoxButton.OK, MessageBoxImage.Error);
}
================================================
FILE: NectarRCON/Services/RconConnectionInfoService.cs
================================================
using NectarRCON.Interfaces;
using NectarRCON.Models;
using System;
using System.Collections.Generic;
using System.Linq;
namespace NectarRCON.Services;
///
/// Rcon连接信息管理服务
///
public class RconConnectionInfoService : IRconConnectionInfoService
{
private readonly IServerInformationService _serverInformationService;
private readonly List _serverInformation = new();
public RconConnectionInfoService(IServerInformationService serverInformationService)
{
_serverInformationService = serverInformationService;
}
public void AddInformation(string serverName)
=> _serverInformation.Add(_serverInformationService.GetServer(serverName) ?? throw new ArgumentNullException(nameof(serverName)));
public void Clear()
=> _serverInformation.Clear();
public IReadOnlyList GetInformation()
=> _serverInformation;
public ServerInformation? GetLastInformation()
=> _serverInformation.FirstOrDefault();
}
================================================
FILE: NectarRCON/Services/ServerInformationService.cs
================================================
using NectarRCON.Interfaces;
using NectarRCON.Models;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Windows;
namespace NectarRCON.Services;
public class ServerInformationService : IServerInformationService
{
private readonly ILanguageService _languageService;
private List _servers = new();
public List Servers
=> _servers;
public ServerInformationService(ILanguageService languageService)
{
_languageService = languageService;
if (!File.Exists("./servers.json"))
File.WriteAllText("./servers.json", "[]");
try
{
string jsonString = File.ReadAllText("./servers.json");
_servers = JsonSerializer.Deserialize>(jsonString)
?? new List();
}
catch (JsonException ex)
{
MessageBoxResult result = MessageBox.Show(_languageService.GetKey("text.server_information.error.json_error")
.Replace("%s", ex.Message)
.Replace("\\n", "\n"), _languageService.GetKey("text.error"), MessageBoxButton.OKCancel, MessageBoxImage.Error);
if (result == MessageBoxResult.OK)
File.WriteAllText("./servers.json", "[]");
else
Process.GetCurrentProcess().Kill();
}
catch (Exception ex)
{
MessageBox.Show(_languageService.GetKey("text.server_information.error.text")
.Replace("%s", ex.Message)
.Replace("\\n", "\n")
, _languageService.GetKey("text.error"), MessageBoxButton.OK, MessageBoxImage.Error);
Process.GetCurrentProcess().Kill();
}
}
public void AddServer(ServerInformation server)
{
if (ServerIsExist(server.Name))
return;
_servers.Add(server);
}
public ServerInformation? GetServer(string name)
=> _servers.Where(s => s.Name == name).FirstOrDefault();
public List GetServers()
{
return _servers;
}
public void RemoveServer(string name)
{
ServerInformation? server = GetServer(name);
if (null == server) return;
_servers.Remove(server);
}
public void Save()
{
try
{
File.WriteAllText("./servers.json", JsonSerializer.Serialize(_servers));
}
catch (Exception ex)
{
MessageBox.Show(_languageService.GetKey("text.server_information.error.save")
.Replace("%s", ex.Message)
.Replace("\\n", "\n")
, _languageService.GetKey("text.error"), MessageBoxButton.OK, MessageBoxImage.Error);
}
}
public bool ServerIsExist(string name)
=> GetServer(name) != null;
public void Update(string name, ServerInformation newInfo)
{
var oldInfo = GetServer(name);
if (null == oldInfo) return;
for (int i = 0; i < _servers.Count; i++)
{
if (_servers[i] == oldInfo)
{
_servers[i] = newInfo;
return;
}
}
}
}
================================================
FILE: NectarRCON/Services/ServerPasswordService.cs
================================================
using NectarRCON.Interfaces;
using NectarRCON.Models;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Windows;
namespace NectarRCON.Services;
public class ServerPasswordService : IServerPasswordService
{
private readonly ILanguageService _languageService;
private ServerInformation? _selectedServer;
private List _serverPasswordList = new();
public ServerPasswordService(ILanguageService languageService)
{
_languageService = languageService;
if (!File.Exists("./passwords.json"))
File.WriteAllText("./passwords.json", "[]");
try
{
string jsonString = File.ReadAllText("./passwords.json");
_serverPasswordList = JsonSerializer.Deserialize>(jsonString)
?? new List();
}
catch (JsonException ex)
{
MessageBoxResult result = MessageBox.Show(_languageService.GetKey("text.password.error.json_error")
.Replace("%s", ex.Message)
.Replace("\\n", "\n"), _languageService.GetKey("text.error"), MessageBoxButton.OKCancel, MessageBoxImage.Error);
if (result == MessageBoxResult.OK)
File.WriteAllText("./passwords.json", "[]");
else
Process.GetCurrentProcess().Kill();
}
catch (Exception ex)
{
MessageBox.Show(_languageService.GetKey("text.password.error.text")
.Replace("%s", ex.Message)
.Replace("\\n", "\n")
, _languageService.GetKey("text.error"), MessageBoxButton.OK, MessageBoxImage.Error);
Process.GetCurrentProcess().Kill();
}
}
public ServerPassword? Get(ServerInformation server)
{
var password = _serverPasswordList.Where(s =>
{
return s.ServerName == server.Name;
}).FirstOrDefault();
return password;
}
public ServerInformation GetSelect()
{
return _selectedServer ?? new();
}
public bool IsExist(ServerInformation server)
=> Get(server) != null;
public void Remove(ServerInformation server)
{
ServerPassword? password = Get(server);
if (password == null)
return;
_serverPasswordList.Remove(password);
}
public void Save()
{
try
{
File.WriteAllText("./passwords.json", JsonSerializer.Serialize(_serverPasswordList));
}
catch (Exception ex)
{
MessageBox.Show(_languageService.GetKey("text.password.error.save")
.Replace("%s", ex.Message)
.Replace("\\n", "\n")
, _languageService.GetKey("text.error"), MessageBoxButton.OK, MessageBoxImage.Error);
}
}
public void Select(ServerInformation server)
{
_selectedServer = server;
}
public void Set(ServerInformation server, string? password, bool? isEmpty)
{
var oldPassword = Get(server);
ServerPassword newPassword;
if (oldPassword == null)
{
newPassword = new ServerPassword()
{
Password = password ?? string.Empty,
IsEmpty = isEmpty ?? string.IsNullOrEmpty(password),
ServerName = server.Name
};
}
else
{
newPassword = new ServerPassword()
{
Password = password ?? oldPassword.Password,
IsEmpty = isEmpty ?? oldPassword.IsEmpty,
ServerName = server.Name
};
}
if (IsExist(server))
_serverPasswordList.Remove(Get(server)!);
_serverPasswordList.Add(newPassword);
}
}
================================================
FILE: NectarRCON/ViewModels/AboutPageViewModel.cs
================================================
namespace NectarRCON.ViewModels
{
class AboutPageViewModel
{
}
}
================================================
FILE: NectarRCON/ViewModels/AddGroupPageViewModel.cs
================================================
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using NectarRCON.Interfaces;
using NectarRCON.Services;
using System;
using Wpf.Ui.Mvvm.Contracts;
namespace NectarRCON.ViewModels;
public partial class AddGroupPageViewModel:ObservableObject
{
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(AddCommand))]
private string? _groupName;
private INavigationService _navigationService;
private IMessageBoxService _messageBoxService;
private IGroupService _groupService;
public AddGroupPageViewModel()
{
GroupName = string.Empty;
_navigationService = App.GetService();
_messageBoxService = App.GetService();
_groupService = App.GetService();
}
private bool CanAdd()
=> !string.IsNullOrWhiteSpace(GroupName);
[RelayCommand(CanExecute = nameof(CanAdd))]
public void Add()
{
try
{
_groupService.Add(new Models.Group()
{
Id = Guid.NewGuid().ToString(),
Name = GroupName!,
Servers = new()
});
_navigationService.Navigate(4);
}catch(Exception ex)
{
_messageBoxService.Show(ex, string.Empty);
}
}
[RelayCommand]
public void Cancel()
{
_navigationService.Navigate(4);
}
[RelayCommand]
public void Load()
{
GroupName = null;
}
}
================================================
FILE: NectarRCON/ViewModels/AddServerWindowViewModel.cs
================================================
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using NectarRCON.Interfaces;
using NectarRCON.Models;
using NectarRCON.Windows;
using System.Windows;
namespace NectarRCON.ViewModels;
public partial class AddServerWindowViewModel : ObservableObject
{
private readonly IServerInformationService _serverInformationService;
private readonly ILanguageService _languageService;
private AddServerWindow? _serverWindow;
[ObservableProperty]
private string _serverName = "Rcon";
[ObservableProperty]
private string _serverAddress = "localhost";
[ObservableProperty]
private string _serverPort = "25575";
public AddServerWindowViewModel(IServerInformationService serverInformationService, ILanguageService languageService)
{
_serverInformationService = serverInformationService;
_languageService = languageService;
}
public void SetWindow(AddServerWindow window)
{
_serverWindow = window;
}
[RelayCommand]
private void Ok()
{
ServerAddress = ServerAddress.Trim();
if (string.IsNullOrWhiteSpace(ServerName) || string.IsNullOrWhiteSpace(ServerAddress))
{
MessageBox.Show(_languageService.GetKey("ui.add_server_window.null_text"), _languageService.GetKey("text.error"), MessageBoxButton.OK, MessageBoxImage.Error);
return;
}
var information = new ServerInformation()
{
Name = ServerName,
Address = ServerAddress,
Port = ushort.Parse(ServerPort)
};
if (_serverInformationService.ServerIsExist(information.Name))
{
MessageBox
.Show(_languageService.GetKey("ui.add_server_window.already_exist")
, _languageService.GetKey("text.error"), MessageBoxButton.OK, MessageBoxImage.Error);
}
else
{
_serverInformationService.AddServer(information);
_serverInformationService.Save();
_serverWindow?.Close();
}
}
}
================================================
FILE: NectarRCON/ViewModels/EditPasswordWindowViewModel.cs
================================================
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using NectarRCON.Interfaces;
using NectarRCON.Models;
using NectarRCON.Windows;
namespace NectarRCON.ViewModels;
public partial class EditPasswordWindowViewModel : ObservableObject
{
private readonly IServerPasswordService _serverPasswordService;
private readonly ServerInformation _serverInformation;
private EditPasswordWindow? _window = null;
[ObservableProperty]
private string _password = string.Empty;
[ObservableProperty]
private bool _isEmpty = true;
public EditPasswordWindowViewModel()
{
_serverPasswordService = App.GetService();
_serverInformation = _serverPasswordService.GetSelect()!;
var password = _serverPasswordService.Get(_serverInformation);
if (password != null)
{
Password = password.Password;
IsEmpty = string.IsNullOrEmpty(Password);
}
else
{
Password = string.Empty;
IsEmpty = true;
}
}
public void SetWindow(EditPasswordWindow window)
{
_window = window;
}
[RelayCommand]
public void TextChange()
{
IsEmpty = string.IsNullOrEmpty(Password);
}
[RelayCommand]
public void SetPassword()
{
_serverPasswordService.Set(_serverInformation, Password, IsEmpty);
_serverPasswordService.Save();
_window?.Close();
}
}
================================================
FILE: NectarRCON/ViewModels/EditServerWindowViewModel.cs
================================================
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using NectarRCON.Interfaces;
using NectarRCON.Models;
using System.Windows;
using Wpf.Ui.Controls;
using MessageBox = System.Windows.MessageBox;
namespace NectarRCON.ViewModels;
public partial class EditServerWindowViewModel : ObservableObject
{
private readonly ILanguageService _languageService;
private readonly IServerPasswordService _serverPasswordService;
private readonly IServerInformationService _serverInformationService;
private UiWindow? _window;
[ObservableProperty]
private ServerInformation? _selectServer;
[ObservableProperty]
private string _port = string.Empty;
[ObservableProperty]
private string _address = string.Empty;
public EditServerWindowViewModel()
{
_languageService = App.GetService();
_serverPasswordService = App.GetService();
_serverInformationService = App.GetService();
}
[RelayCommand]
private void Load(RoutedEventArgs e)
{
_window = e.Source as UiWindow;
SelectServer = _serverPasswordService.GetSelect();
Port = _selectServer?.Port.ToString() ?? "0";
Address = _selectServer?.Address ?? "localhost";
}
[RelayCommand]
public void Exit()
{
_window?.Close();
}
[RelayCommand]
private void Ok()
{
Address = Address.Trim();
if (string.IsNullOrWhiteSpace(_selectServer?.Address) || string.IsNullOrEmpty(Address))
MessageBox.Show(_languageService.GetKey("ui.add_server_window.null_text"), _languageService.GetKey("text.error"), MessageBoxButton.OK, MessageBoxImage.Error);
else
{
_selectServer.Port = ushort.Parse(Port);
_selectServer.Address = Address;
_serverInformationService.Update(_selectServer.Name, _selectServer);
_serverInformationService.Save();
Exit();
}
}
}
================================================
FILE: NectarRCON/ViewModels/GroupPageViewModel.cs
================================================
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using NectarRCON.Interfaces;
using NectarRCON.Models;
using NectarRCON.Views.Pages;
using System.Collections.ObjectModel;
using Wpf.Ui.Mvvm.Contracts;
namespace NectarRCON.ViewModels;
public partial class GroupPageViewModel:ObservableObject
{
[ObservableProperty]
private ObservableCollection _groups;
private readonly IGroupService _groupService;
private readonly ILanguageService _languageService;
private readonly IMessageBoxService _messageBoxService;
private readonly INavigationService _navigationService;
#pragma warning disable CS8618 // 在退出构造函数时,不可为 null 的字段必须包含非 null 值。请考虑声明为可以为 null。
public GroupPageViewModel()
#pragma warning restore CS8618 // 在退出构造函数时,不可为 null 的字段必须包含非 null 值。请考虑声明为可以为 null。
{
_groupService = App.GetService();
_languageService = App.GetService();
_messageBoxService = App.GetService();
_navigationService = App.GetService();
Groups = new();
}
[RelayCommand]
public void Load()
{
Refresh();
}
private void Refresh()
{
Groups.Clear();
foreach(Group group in _groupService.GetGroups())
{
Groups.Add(new(group.Name, this));
}
}
[RelayCommand]
public void RemoveGroup(string groupId)
{
Group group = _groupService.GetGroup(groupId)!;
if (_messageBoxService.Show(string.Format(_languageService.GetKey("ui.group.delete_group"), group.Name), _languageService.GetKey("text.delete"), System.Windows.MessageBoxButton.YesNo, System.Windows.MessageBoxImage.Question)
== System.Windows.MessageBoxResult.Yes)
{
_groupService.Delete(groupId);
Refresh();
}
}
[RelayCommand]
public void NewGroup()
{
_navigationService.Navigate(typeof(AddGroupPage));
}
}
================================================
FILE: NectarRCON/ViewModels/JoinGroupWindowViewModel.cs
================================================
using CommunityToolkit.Mvvm.ComponentModel;
using NectarRCON.Interfaces;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Windows.Data;
namespace NectarRCON.ViewModels;
public partial class JoinGroupWindowViewModel:ObservableObject
{
private IServerInformationService _serverInformationService;
[ObservableProperty]
private ObservableCollection _servers = new();
[ObservableProperty]
private ListCollectionView? _serverCollectionView;
[ObservableProperty]
public ObservableCollection? _blackList;
public JoinGroupWindowViewModel()
{
_serverInformationService = App.GetService();
foreach(var server in _serverInformationService.GetServers())
{
Servers.Add(server.Name);
}
ServerCollectionView = new(Servers);
ServerCollectionView.Filter += (s) =>
{
return !BlackList?.Contains(s?.ToString() ?? string.Empty) ?? true;
};
BlackList = new();
}
partial void OnBlackListChanged(ObservableCollection? value)
{
ServerCollectionView?.Refresh();
}
}
================================================
FILE: NectarRCON/ViewModels/MainPageViewModel.cs
================================================
using System;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using NectarRCON.Interfaces;
using NectarRCON.Models;
using NectarRCON.Rcon;
using NectarRCON.Services;
using NectarRCON.Views.Pages;
using System.Net.Sockets;
using System.Security.Authentication;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
using NectarRCON.Dp;
using Serilog;
using Wpf.Ui.Mvvm.Contracts;
using MessageBox = System.Windows.MessageBox;
using TextBox = Wpf.Ui.Controls.TextBox;
namespace NectarRCON.ViewModels;
public partial class MainPageViewModel : ObservableObject
{
private static readonly RconSettingsDp RconSettings = DpFile.LoadSingleton();
private readonly ILogService _logService;
private readonly IHistoryService _historyService;
private readonly IServerPasswordService _serverPasswordService;
private IRconConnection _rconConnectService;
private readonly INavigationService _navigationService;
private readonly ILanguageService _languageService;
private readonly IRconConnectionInfoService _rconConnectionInfoService;
private readonly IConnectingDialogService _connectingDialogService;
private readonly IMessageBoxService _messageBoxService;
private MainPage? _page;
private TextBox? _logTextBox;
private HistoryNode? _historyNode;
[ObservableProperty] private string _commandText = string.Empty;
[ObservableProperty] private string _logText = string.Empty;
[ObservableProperty] private bool _isMultipleConnection;
[ObservableProperty] private bool _isDisconnection;
public MainPageViewModel()
{
_logService = App.GetService();
_historyService = App.GetService();
_serverPasswordService = App.GetService();
_navigationService = App.GetService();
_languageService = App.GetService();
_rconConnectionInfoService = App.GetService();
_messageBoxService = App.GetService();
_connectingDialogService = App.GetService();
WeakReferenceMessenger.Default.Register(this, OnClear);
// 选择连接服务
_rconConnectService = _rconConnectionInfoService.HasMultipleInformation
? App.GetService(typeof(RconMultiConnection))
: App.GetService(typeof(RconSingleConnection));
IsMultipleConnection = _rconConnectionInfoService.HasMultipleInformation;
}
private void OnClear(object sender, ClearLogValueMessage msg)
{
_logService.Clear();
LogText = string.Empty;
}
private void OnMessage(ServerInformation info, string msg)
{
Log.Information("[OnMessage] {name}({adapter}) -> {msg}", info.Name, string.IsNullOrEmpty(info.Adapter) ? "TCPRcon" : info.Adapter, string.IsNullOrEmpty(msg) ? "$empty$" : msg);
string logMsg = string.IsNullOrEmpty(msg)
? _languageService.GetKey("ui.main_page.successful")
: msg;
LogText += _logService.Log($"{info.Name}:" + logMsg);
_logTextBox?.ScrollToEnd();
}
private void OnClosed(ServerInformation info)
{
LogText += _logService.Log($"{info.Name} {_languageService.GetKey("text.server.closed")}");
IsDisconnection = !_rconConnectService.IsConnected();
}
[RelayCommand]
private async void Load(RoutedEventArgs e)
{
// GetLogs
LogText = string.Empty;
LogText = _logService.GetText();
_page = e.Source as MainPage;
await ConnectAsync();
}
[RelayCommand]
private async void ReConnect()
{
Log.Information($"[ReConnectCommand] 正在准备重连");
if (_rconConnectService.IsConnected())
_rconConnectService.Close();
IsDisconnection = false;
await ConnectAsync();
}
private async Task ConnectAsync()
{
Log.Information($"[ConnectAsync] 准备连接到服务器");
IsMultipleConnection = _rconConnectionInfoService.HasMultipleInformation;
_rconConnectService.OnConnected -= OnConnected;
_rconConnectService.OnMessage -= OnMessage;
_rconConnectService.OnClosed -= OnClosed;
await Task.CompletedTask;
try
{
_connectingDialogService.Show();
// 选择连接服务
_rconConnectService = _rconConnectionInfoService.HasMultipleInformation
? App.GetService(typeof(RconMultiConnection))
: App.GetService(typeof(RconSingleConnection));
WeakReferenceMessenger.Default.Send(new MainPageLoadValueMessage
{
IsLoaded = true,
});
Log.Information($"[ConnectAsync] 连接服务: {_rconConnectService.GetType().FullName}, 是否为多连接: {IsMultipleConnection}");
_logTextBox = (TextBox)LogicalTreeHelper.FindLogicalNode(_page, "LogText");
LogText += _logService.Log(_languageService.GetKey("text.server.start"));
_logTextBox?.ScrollToEnd();
_rconConnectService.OnConnected += OnConnected;
_rconConnectService.OnMessage += OnMessage;
_rconConnectService.OnClosed += OnClosed;
await Task.Run(_rconConnectService.Connect);
}
catch (AuthenticationException ex)
{
Log.Error($"[ConnectAsync] 认证失败: {ex.Message}");
var msg = ex.Message + _languageService.GetKey("text.server.connect.auth_fail")
.Replace("\\n", "\n");
_messageBoxService.Show(msg, _languageService.GetKey("text.error"), MessageBoxButton.OK,
MessageBoxImage.Error);
LogText += _logService.Log(msg);
// 如果认证失败 就根据当前模式返回对应页面
_navigationService.Navigate(_rconConnectionInfoService.HasMultipleInformation
? typeof(GroupPage)
: typeof(ServersPage));
}
catch (Exception ex)
{
Log.Error($"[ConnectAsync] 连接遇到错误: {ex}");
var msg = _languageService.GetKey("text.server.connect.fail.text")
.Replace("\\n", "\n")
.Replace("%s", ex.Message);
_messageBoxService.Show(msg, _languageService.GetKey("text.error"), MessageBoxButton.OK,
MessageBoxImage.Error);
LogText += _logService.Log(msg);
_logTextBox?.ScrollToEnd();
}
finally
{
_connectingDialogService.Close();
}
// 当只有一个服务器时IsConnected会返回单个客户端的连接状态
// 当有多个服务器时只要有一个客户端在线,IsConnected就会返回True
if (!_rconConnectService.IsConnected() && !RconSettings.IsKeepConnectionWindowOpen)
{
_navigationService.Navigate(2);
}
IsDisconnection = !_rconConnectService.IsConnected();
}
partial void OnIsDisconnectionChanged(bool value)
{
Log.Information("当前客户端状态: {0}", _rconConnectService.IsConnected() ? "在线" : "离线");
}
private void OnConnected(ServerInformation info)
{
Log.Information("[OnConnected] {name}({adapter})", info.Name, string.IsNullOrEmpty(info.Adapter) ? "TCPRcon" : info.Adapter);
LogText += _logService.Log($"$ {info.Name} {_languageService.GetKey("text.server.connected")}");
IsDisconnection = false;
}
[RelayCommand]
private void BackHome()
{
_navigationService.Navigate(_rconConnectionInfoService.HasMultipleInformation
? typeof(GroupPage)
: typeof(ServersPage));
}
[RelayCommand]
private void Exit()
{
WeakReferenceMessenger.Default.Send(new MainPageLoadValueMessage()
{
IsLoaded = false,
});
if (_rconConnectService.IsConnected())
_rconConnectService.Close();
_rconConnectService.OnMessage -= OnMessage;
_rconConnectService.OnClosed -= OnClosed;
_rconConnectService.OnConnected -= OnConnected;
IsDisconnection = false; // 重置状态 此时没有任何连接
}
[RelayCommand]
private void Run()
{
Log.Information("[Run] {0}", CommandText);
if (_rconConnectService.IsConnected())
{
LogText += _logService.Log($"> {CommandText}");
_logTextBox?.ScrollToEnd();
_rconConnectService.Send(CommandText);
CommandText = string.Empty;
}
else
{
IsDisconnection = true;
_rconConnectService.Close();
MessageBox.Show(_languageService.GetKey("text.server.not_connect.text"),
_languageService.GetKey("text.error"), MessageBoxButton.OK, MessageBoxImage.Error);
}
}
[RelayCommand]
private void KeyDown(KeyEventArgs e)
{
var textBox = (System.Windows.Controls.TextBox)e.Source;
if (e.Key == Key.Enter)
{
var text = textBox.Text.Trim();
if (string.IsNullOrEmpty(text))
{
return;
}
_commandText = text;
_historyNode = _historyService.InputCmd(_commandText);
Run();
}
else if (e.Key == Key.Up)
{
_historyNode = _historyService.Prev(_historyNode);
textBox.Text = _historyNode?.Cmd;
textBox.Select(textBox.Text?.Length ?? 0, 0);
}
else if (e.Key == Key.Down)
{
_historyNode = _historyService.Next(_historyNode);
textBox.Text = _historyNode?.Cmd;
textBox.Select(textBox.Text?.Length ?? 0, 0);
}
}
}
================================================
FILE: NectarRCON/ViewModels/MainWindowViewModel.cs
================================================
using System;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using NectarRCON.Interfaces;
using NectarRCON.Models;
using System.Collections.ObjectModel;
using System.IO;
using System.Windows;
using Serilog;
using Wpf.Ui.Common;
using Wpf.Ui.Controls;
using Wpf.Ui.Controls.Interfaces;
using Wpf.Ui.Mvvm.Contracts;
using MessageBox = System.Windows.MessageBox;
namespace NectarRCON.ViewModels
{
public partial class MainWindowViewModel : ObservableObject
{
[ObservableProperty]
private ObservableCollection _navigationItems = new();
private readonly INavigationService _navigationService;
private readonly ILanguageService _languageService;
[ObservableProperty]
private bool _mainPageIsLoaded;
public MainWindowViewModel(INavigationService navigationService, ILanguageService languageService)
{
NavigationItems = new ObservableCollection()
{
new NavigationItem
{
Content = "主页",
PageTag = "mainPage",
PageType = typeof(Views.Pages.MainPage)
},
new NavigationItem
{
Content = "设置",
PageTag = "settingsPage",
PageType= typeof(Views.Pages.SettingPage)
},
new NavigationItem
{
Content = "服务器...",
PageTag = "serversPage",
PageType= typeof(Views.Pages.ServersPage)
},
new NavigationItem
{
Content = "关于",
PageTag = "aboutPage",
PageType= typeof(Views.Pages.AboutPage)
},
new NavigationItem
{
Content = "群组",
PageTag = "groupPage",
PageType= typeof(Views.Pages.GroupPage)
},
new NavigationItem
{
Content = "添加服务器",
PageTag = "newGroup",
PageType= typeof(Views.Pages.AddGroupPage)
}
};
_navigationService = navigationService;
_languageService = languageService;
WeakReferenceMessenger.Default.Register(this, OnMainPageChange);
}
public void OnMainPageChange(object sender, MainPageLoadValueMessage message)
{
MainPageIsLoaded = message.IsLoaded;
}
[RelayCommand]
private void OnLoad()
{
_navigationService.Navigate(2);
}
[RelayCommand]
private void ClearButtonClick()
{
WeakReferenceMessenger.Default.Send(new ClearLogValueMessage());
}
[RelayCommand]
private void ChangePage(string index)
{
_navigationService.Navigate(int.Parse(index));
}
[RelayCommand]
private void ClearProgramLogs()
{
if (MessageBox.Show(_languageService.GetKey("ui.menu.log.clear_program.ask"), "NectarRcon", MessageBoxButton.YesNo,
MessageBoxImage.Question) != MessageBoxResult.Yes) return;
Log.Information("Clear program logs");
foreach (var logFile in Directory.GetFiles(Path.Combine(Environment.CurrentDirectory, "logs", "program"), "*.log"))
{
try
{
Log.Information("Delete log file: {0}", logFile);
File.Delete(logFile);
}
catch(Exception ex)
{
Log.Error(ex, "Delete log file failed: {0}", logFile);
}
}
}
}
}
================================================
FILE: NectarRCON/ViewModels/ServersPageViewModel.cs
================================================
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using NectarRCON.Interfaces;
using NectarRCON.Models;
using NectarRCON.Windows;
using System.Collections.ObjectModel;
using System.Windows.Controls;
using System.Windows.Data;
namespace NectarRCON.ViewModels;
public partial class ServersPageViewModel : ObservableObject
{
[ObservableProperty]
private ListCollectionView _serverCollectionView;
[ObservableProperty]
private ObservableCollection _servers;
private string _filterName = string.Empty;
#pragma warning disable CS8618 // 在退出构造函数时,不可为 null 的字段必须包含非 null 值。请考虑声明为可以为 null。
public ServersPageViewModel()
#pragma warning restore CS8618 // 在退出构造函数时,不可为 null 的字段必须包含非 null 值。请考虑声明为可以为 null。
{
Servers = new();
ServerCollectionView = new(Servers);
ServerCollectionView.Filter += (s) =>
{
if (_filterName == string.Empty)
return true;
if (s is ServerModel model)
{
return model.Name.Contains(_filterName);
}
return false;
};
Refresh();
}
public void Refresh()
{
Servers.Clear();
foreach (ServerInformation information in App.GetService().GetServers())
{
Servers.Add(new ServerModel(information, this));
}
}
[RelayCommand]
public void AddServer()
{
AddServerWindow addServer = App.GetService();
addServer.ShowDialog();
Refresh();
}
[RelayCommand]
public void FilterTextChanged(TextChangedEventArgs e)
{
var box = (System.Windows.Controls.TextBox)e.Source;
_filterName = box.Text.ToString() ?? string.Empty;
_serverCollectionView.Refresh();
}
}
================================================
FILE: NectarRCON/ViewModels/SettingPageViewModel.cs
================================================
using System;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using NectarRCON.Core.Helper;
using NectarRCON.Interfaces;
using NectarRCON.Models;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using NectarRCON.Dp;
using Wpf.Ui.Mvvm.Contracts;
namespace NectarRCON.ViewModels;
public partial class SettingPageViewModel : ObservableObject
{
private bool _isLoaded = false;
private readonly ILanguageService _languageService;
private readonly IConfigService _configService;
private readonly IThemeService _themeService;
private readonly RconSettingsDp _rconSettingsDp = DpFile.LoadSingleton();
[ObservableProperty]
private int _languageSelectedIndex = -1;
[ObservableProperty]
private int _themeSelectedIndex = -1;
[ObservableProperty]
private bool _rconAutoReconnect;
[ObservableProperty]
private bool _isKeepConnectionWindowOpen;
[ObservableProperty]
private ObservableCollection _rconEncoding = [];
[ObservableProperty]
private string _selectedRconEncoding;
[ObservableProperty]
private ObservableCollection _languages = [];
public SettingPageViewModel()
{
_languageService = App.GetService();
_configService = App.GetService();
_themeService = App.GetService();
RconAutoReconnect = _rconSettingsDp.AutoReconnect;
IsKeepConnectionWindowOpen = _rconSettingsDp.IsKeepConnectionWindowOpen;
RconEncoding.Clear();
foreach (var encoding in Enum.GetNames(typeof(RconEncoding)))
{
RconEncoding.Add(encoding);
}
SelectedRconEncoding = _rconSettingsDp.Encoding.ToString();
}
partial void OnRconAutoReconnectChanged(bool value)
{
_rconSettingsDp.AutoReconnect = value;
_rconSettingsDp.Save();
}
partial void OnIsKeepConnectionWindowOpenChanged(bool value)
{
_rconSettingsDp.IsKeepConnectionWindowOpen = value;
_rconSettingsDp.Save();
}
partial void OnSelectedRconEncodingChanged(string value)
{
_rconSettingsDp.Encoding = Enum.GetValues().FirstOrDefault(e => e.ToString() == value);
_rconSettingsDp.Save();
}
[RelayCommand]
public void PageLoad(RoutedEventArgs e)
{
Languages.Clear();
foreach (var language in _languageService.GetLanguages())
{
Languages.Add(language.Value["file.name"].ToString() ?? "NullName");
}
foreach (var language in _languageService.GetLanguages())
{
LanguageSelectedIndex++;
if (language.Key == _configService.GetConfig().LanguageName)
break;
}
ThemeSelectedIndex = (int)_configService.GetConfig().Theme;
_isLoaded = true;
}
[RelayCommand]
public void Exit()
{
_isLoaded = false;
}
[RelayCommand]
public void ThemeSelectionChange()
{
if (!_isLoaded)
return;
_configService.GetConfig().Theme = (ETheme)_themeSelectedIndex;
_configService.Save();
switch ((ETheme)_themeSelectedIndex)
{
case ETheme.System:
_themeService.SetTheme(Win32Helper.GetWindowsTheme() ? Wpf.Ui.Appearance.ThemeType.Dark : Wpf.Ui.Appearance.ThemeType.Light);
break;
case ETheme.Dark:
_themeService.SetTheme(Wpf.Ui.Appearance.ThemeType.Dark);
break;
case ETheme.Light:
_themeService.SetTheme(Wpf.Ui.Appearance.ThemeType.Light);
break;
default:
break;
}
}
[RelayCommand]
public void LanguageSelectionChange(SelectionChangedEventArgs e)
{
if (!_isLoaded)
return;
if (e.AddedItems.Count == 0 || null == e.AddedItems[0])
return;
KeyValuePair? lang = _languageService.GetLanguages().Where(l =>
{
return (l.Value["file.name"].ToString() ?? "NullName") == e.AddedItems[0]!.ToString();
}).FirstOrDefault();
if (null == lang.Value.Value) return;
_configService.GetConfig().LanguageName = lang.Value.Key;
_configService.Save();
_languageService.SelectLanguage(lang.Value.Value["file.name"].ToString() ?? "NullName", true);
}
}
================================================
FILE: NectarRCON/Views/Pages/AboutPage.xaml
================================================
================================================
FILE: NectarRCON/Views/Pages/AboutPage.xaml.cs
================================================
namespace NectarRCON.Views.Pages
{
///
/// AboutPage.xaml 的交互逻辑
///
public partial class AboutPage
{
public AboutPage()
{
InitializeComponent();
}
}
}
================================================
FILE: NectarRCON/Views/Pages/AddGroupPage.xaml
================================================
================================================
FILE: NectarRCON/Views/Pages/AddGroupPage.xaml.cs
================================================
using System;
using Wpf.Ui.Controls;
namespace NectarRCON.Views.Pages
{
///
/// AddGroupPage.xaml 的交互逻辑
///
public partial class AddGroupPage : UiPage
{
public AddGroupPage()
{
InitializeComponent();
}
}
}
================================================
FILE: NectarRCON/Views/Pages/GroupPage.xaml
================================================
================================================
FILE: NectarRCON/Views/Pages/GroupPage.xaml.cs
================================================
using System.Windows.Controls;
using Wpf.Ui.Controls;
namespace NectarRCON.Views.Pages
{
///
/// GroupPage.xaml 的交互逻辑
///
public partial class GroupPage : UiPage
{
public GroupPage()
{
InitializeComponent();
}
private void ListView_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (sender is ListView listView)
{
listView.SelectedIndex = -1;
}
}
}
}
================================================
FILE: NectarRCON/Views/Pages/MainPage.xaml
================================================
================================================
FILE: NectarRCON/Views/Pages/MainPage.xaml.cs
================================================
namespace NectarRCON.Views.Pages
{
///
/// MainPage.xaml 的交互逻辑
///
public partial class MainPage
{
public MainPage()
{
InitializeComponent();
}
}
}
================================================
FILE: NectarRCON/Views/Pages/ServersPage.xaml
================================================
================================================
FILE: NectarRCON/Views/Pages/ServersPage.xaml.cs
================================================
namespace NectarRCON.Views.Pages
{
///
/// ServersPage.xaml 的交互逻辑
///
public partial class ServersPage
{
public ServersPage()
{
InitializeComponent();
}
}
}
================================================
FILE: NectarRCON/Views/Pages/SettingPage.xaml
================================================
================================================
FILE: NectarRCON/Views/Pages/SettingPage.xaml.cs
================================================
namespace NectarRCON.Views.Pages;
public partial class SettingPage
{
public SettingPage()
{
InitializeComponent();
}
}
================================================
FILE: NectarRCON/Windows/AddServerWindow.xaml
================================================
================================================
FILE: NectarRCON/Windows/AddServerWindow.xaml.cs
================================================
using NectarRCON.ViewModels;
using System.Windows;
namespace NectarRCON.Windows
{
///
/// AddServerWindow.xaml 的交互逻辑
///
public partial class AddServerWindow
{
public AddServerWindowViewModel ViewModel
{
get;
}
public AddServerWindow(AddServerWindowViewModel viewModel)
{
ViewModel = viewModel;
viewModel.SetWindow(this);
InitializeComponent();
DataContext = this;
}
private void CancelButtonClick(object sender, RoutedEventArgs e)
=> Close();
}
}
================================================
FILE: NectarRCON/Windows/EditPasswordWindow.xaml
================================================
================================================
FILE: NectarRCON/Windows/EditPasswordWindow.xaml.cs
================================================
using NectarRCON.ViewModels;
using System.Windows;
namespace NectarRCON.Windows
{
///
/// EditPasswordWindow.xaml 的交互逻辑
///
public partial class EditPasswordWindow
{
public EditPasswordWindow()
{
InitializeComponent();
((EditPasswordWindowViewModel)this.DataContext).SetWindow(this);
}
private void CancelButtonClick(object sender, RoutedEventArgs e)
=> Close();
}
}
================================================
FILE: NectarRCON/Windows/EditServerWindow.xaml
================================================
================================================
FILE: NectarRCON/Windows/EditServerWindow.xaml.cs
================================================
namespace NectarRCON.Windows
{
///
/// EditServerWindow.xaml 的交互逻辑
///
public partial class EditServerWindow
{
public EditServerWindow()
{
InitializeComponent();
}
}
}
================================================
FILE: NectarRCON/Windows/JoinGroupWindow.xaml
================================================
================================================
FILE: NectarRCON/Windows/JoinGroupWindow.xaml.cs
================================================
using NectarRCON.ViewModels;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
using Wpf.Ui.Controls;
namespace NectarRCON.Windows
{
///
/// JoinGroupWindow.xaml 的交互逻辑
///
public partial class JoinGroupWindow : UiWindow
{
public string? SelectedServer = null;
public JoinGroupWindow()
{
InitializeComponent();
}
private void CloseButton_Click(object sender, RoutedEventArgs e)
{
Close();
}
public void AddBlackList(string value)
{
((JoinGroupWindowViewModel)DataContext).BlackList?.Add(value);
((JoinGroupWindowViewModel)DataContext).ServerCollectionView?.Refresh();
}
private void ServersListView_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if(e.AddedItems.Count > 0)
{
SelectedServer = e.AddedItems[0]?.ToString();
Close();
}
}
}
}
================================================
FILE: NectarRCON/Windows/MainWindow.xaml
================================================
================================================
FILE: NectarRCON/Windows/MainWindow.xaml.cs
================================================
using NectarRCON.Interfaces;
using NectarRCON.ViewModels;
using System;
using System.Windows.Controls;
using Wpf.Ui.Common.Interfaces;
using Wpf.Ui.Controls.Interfaces;
using Wpf.Ui.Mvvm.Contracts;
namespace NectarRCON
{
///
/// Interaction logic for MainWindow.xaml
///
public partial class MainWindow : INavigationWindow, INavigableView
{
private readonly INavigationService _navigationService;
public MainWindowViewModel ViewModel
{
get;
}
MainWindowViewModel INavigableView.ViewModel => ViewModel;
public MainWindow(INavigationService navigationService, MainWindowViewModel viewModel, IConnectingDialogService connectingDialogService)
{
InitializeComponent();
DataContext = this;
_navigationService = navigationService;
_navigationService.SetNavigationControl(RootNavigation);
ViewModel = viewModel;
connectingDialogService.SetDialog(ConnectingDialog);
}
#region INavigationWindow methods
public Frame GetFrame()
=> MainFrame;
public INavigation GetNavigation()
=> RootNavigation;
public bool Navigate(Type pageType)
=> RootNavigation.Navigate(pageType);
public void SetPageService(IPageService pageService)
=> RootNavigation.PageService = pageService;
public void ShowWindow()
=> Show();
public void CloseWindow()
=> Close();
#endregion
}
}
================================================
FILE: NectarRCON/app.manifest
================================================
================================================
FILE: NectarRCON.Adapter.Minecraft/MinecraftRconClient.cs
================================================
using NectarRCON.Export.Client;
using NectarRCON.Export.Interfaces;
using System.ComponentModel;
using System.Text;
namespace NectarRCON.Adapter.Minecraft
{
[Description("rcon.minecraft")]
public class MinecraftRconClient : BaseTcpClient, IRconAdapter
{
// https://wiki.vg/RCON #Fragmentation
private static readonly int MaxMessageSize = 4110;
private readonly MemoryStream _buffer = new();
private readonly SemaphoreSlim _semaphore = new(1);
private int _lastId;
///
/// 编码
/// 默认编码 UTF8
///
private new Encoding _encoding = Encoding.UTF8;
public void Disconnect()
{
_semaphore.Wait();
try
{
Stop();
}
finally
{
_semaphore.Release();
}
}
public override void Dispose()
{
_semaphore.Dispose();
_buffer.Dispose();
base.Dispose();
}
public override byte[] Read(StreamReader reader)
{
byte[] buffer = new byte[MaxMessageSize];
int len = reader.BaseStream.Read(buffer, 0, buffer.Length);
Array.Resize(ref buffer, len);
return buffer;
}
public string Run(string command)
{
_semaphore.Wait();
try
{
Packet response = Send(new Packet(PacketType.Command, command));
return response.Body;
}
finally
{
_semaphore.Release();
}
}
public Encoding GetEncoding()
=> _encoding;
public void SetEncoding(Encoding encoding)
{
_encoding = encoding;
}
public bool Connect(string address, int port)
{
_semaphore.Wait();
try
{
Start(address, port);
return IsConnected;
}
finally
{
_semaphore.Release();
}
}
public bool Authenticate(string password)
{
_semaphore.Wait();
try
{
Packet packet = Send(new Packet(PacketType.Authenticate, password));
return packet.Id == _lastId;
}
finally
{
_semaphore.Release();
}
}
private Packet Send(Packet packet)
{
Interlocked.Increment(ref _lastId);
packet.SetId(_lastId);
return PacketEncoder.Decode(Send(packet.Encode(_encoding)), _encoding);
}
}
}
================================================
FILE: NectarRCON.Adapter.Minecraft/NectarRCON.Adapter.Minecraft.csproj
================================================
net7.0-windows
enable
enable
================================================
FILE: NectarRCON.Adapter.Minecraft/Packet.cs
================================================
using System.Text;
namespace NectarRCON.Adapter.Minecraft;
public class Packet
{
public readonly int Length;
public int Id;
public readonly PacketType Type;
public readonly string Body;
public Packet(int length, int id, PacketType type, string body)
{
Length = length;
Id = id;
Type = type;
Body = body;
}
public Packet(PacketType type, string body)
{
Type = type;
Body = body;
}
public void SetId(int id)
{
Id = id;
}
public byte[] Encode(Encoding? encoding = null)
{
List bytes = new List();
var data = (encoding?? Encoding.UTF8).GetBytes(Body);
bytes.AddRange(BitConverter.GetBytes(PacketEncoder.HeaderLength + data.Length));
bytes.AddRange(BitConverter.GetBytes(Id));
bytes.AddRange(BitConverter.GetBytes((int)Type));
bytes.AddRange(data);
bytes.AddRange(new byte[] { 0, 0 });
return bytes.ToArray();
}
}
================================================
FILE: NectarRCON.Adapter.Minecraft/PacketEncoder.cs
================================================
using System.Text;
namespace NectarRCON.Adapter.Minecraft
{
public enum PacketType : int
{
Response, // 0: Command response
_,
Command, // 2: Command
Authenticate // 3: Login
}
public abstract class PacketEncoder
{
public const int HeaderLength = 10;
public static byte[] Encode(Packet msg, Encoding? encoding = null)
{
List bytes = new List();
bytes.AddRange(BitConverter.GetBytes(msg.Length));
bytes.AddRange(BitConverter.GetBytes(msg.Id));
bytes.AddRange(BitConverter.GetBytes((int)msg.Type));
bytes.AddRange((encoding??Encoding.UTF8).GetBytes(msg.Body));
bytes.AddRange(new byte[] { 0, 0 });
return bytes.ToArray();
}
public static Packet Decode(byte[] bytes, Encoding? encoding = null)
{
if (bytes.Length < HeaderLength) { throw new ArgumentException("packet length too short"); }
int len = BitConverter.ToInt32(bytes, 0);
int id = BitConverter.ToInt32(bytes, 4);
int type = BitConverter.ToInt32(bytes, 8);
int bodyLen = bytes.Length - (HeaderLength + 4);
string body = string.Empty;
if (bodyLen > 0)
{
body = (encoding??Encoding.UTF8).GetString(bytes, 12, bodyLen);
}
return new Packet(len, id, (PacketType)type, body);
}
}
}
================================================
FILE: NectarRCON.Adapter.Minecraft/README.md
================================================
# Adapter
MinecraftRcon实现
================================================
FILE: NectarRCON.Core/Helper/AdapterHelpers.cs
================================================
using NectarRCON.Adapter.Minecraft;
using NectarRCON.Export.Interfaces;
namespace NectarRCON.Core.Helper
{
public static class AdapterHelpers
{
public static IRconAdapter? CreateAdapterInstance(string adapter)
{
return new MinecraftRconClient(); // 暂时这么写
}
}
}
================================================
FILE: NectarRCON.Core/Helper/DNSHelpers.cs
================================================
using DnsClient;
namespace NectarRCON.Core.Helper
{
public class DNSHelpers
{
public static string AQuery(string host)
{
var lookup = new LookupClient();
var result = lookup.Query(host, QueryType.A);
var record = result.Answers.ARecords().FirstOrDefault();
return record?.Address.ToString() ?? string.Empty;
}
///
/// MinecraftSRV解析
///
/// 成功返回ip:port
public static string SRVQuery(string host)
{
// Minecraft特有的srv
string srvAddress = $"_minecraft._tcp.{host}";
var lookup = new LookupClient();
var result = lookup.Query(srvAddress, QueryType.SRV);
var record = result.Answers.SrvRecords().FirstOrDefault();
if (record != null)
{
return $"{record.Target.Value}:{record.Port}";
}
return string.Empty;
}
}
}
================================================
FILE: NectarRCON.Core/Helper/Win32Helper.cs
================================================
namespace NectarRCON.Core.Helper;
public static class Win32Helper
{
public static bool GetWindowsTheme()
{
const string RegistryKeyPath = @"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize";
const string RegistryValueName = "AppsUseLightTheme";
// 这里也可能是LocalMachine(HKEY_LOCAL_MACHINE)
// see "https://www.addictivetips.com/windows-tips/how-to-enable-the-dark-theme-in-windows-10/"
object? registryValueObject = Microsoft.Win32.Registry.CurrentUser.OpenSubKey(RegistryKeyPath)?.GetValue(RegistryValueName);
if (registryValueObject is null) return false;
return (int)registryValueObject > 0 ? false : true;
}
}
================================================
FILE: NectarRCON.Core/NectarRCON.Core.csproj
================================================
net7.0-windows
enable
enable
================================================
FILE: NectarRCON.Export/Client/BaseTcpClient.cs
================================================
using NectarRCON.Export.Interfaces;
using System.Net.Sockets;
using System.Text;
namespace NectarRCON.Export.Client;
///
/// 简单的TCP客户端实现
///
public abstract class BaseTcpClient : IRconClient
{
private TcpClient? _tcpClient;
private NetworkStream? _networkStream;
private StreamReader? _reader;
protected Encoding? _encoding;
public bool IsConnected { get; private set; }
public virtual void Dispose()
{
Stop();
}
public byte[] Send(byte[] bytes)
{
using MemoryStream response = new();
if (IsConnected)
{
_networkStream!.Write(bytes, 0, bytes.Length);
return Read(_reader!);
}
return response.ToArray();
}
public abstract byte[] Read(StreamReader reader);
public void Start(string address, int port)
{
if (IsConnected)
{
throw new InvalidOperationException("Client is already connected");
}
try
{
_tcpClient = new(address, port);
_networkStream = _tcpClient.GetStream();
_reader = new StreamReader(_networkStream, _encoding ?? Encoding.UTF8);
IsConnected = true;
}
catch
{
throw;
}
}
public void Stop()
{
if (!IsConnected)
{
return;
}
_tcpClient?.Dispose();
_reader?.Dispose();
IsConnected = false;
}
}
================================================
FILE: NectarRCON.Export/Interfaces/IRconAdapter.cs
================================================
using System.Text;
namespace NectarRCON.Export.Interfaces;
///
/// Rcon协议兼容接口
///
public interface IRconAdapter : IDisposable
{
///
/// 已经连接
///
bool IsConnected { get; }
///
/// 连接
///
bool Connect(string address, int port);
///
/// 验证
///
bool Authenticate(string password);
///
/// 断开连接
///
void Disconnect();
///
/// 执行命令
///
string Run(string command);
Encoding GetEncoding();
void SetEncoding(Encoding encoding);
}
================================================
FILE: NectarRCON.Export/Interfaces/IRconClient.cs
================================================
namespace NectarRCON.Export.Interfaces;
///
/// Rcon客户端接口
///
public interface IRconClient : IDisposable
{
///
/// 启动连接
///
///
/// 地址
///
/// localhost:11451
/// mc.xxxx.net:11451
///
///
///
void Start(string address, int port);
///
/// 关闭连接
///
void Stop();
///
/// 执行命令
///
/// 原始字节
/// 服务端返回
byte[] Send(byte[] bytes);
///
/// 是否已连接
///
bool IsConnected { get; }
}
================================================
FILE: NectarRCON.Export/NectarRCON.Export.csproj
================================================
net7.0-windows
enable
enable
================================================
FILE: NectarRCON.Tests/Adapter/MinecraftRconTest.cs
================================================
using NectarRCON.Adapter.Minecraft;
using NectarRCON.Export.Interfaces;
namespace NectarRCON.Tests.Adapter
{
[TestClass]
public class MinecraftRconTest
{
private static readonly IRconAdapter _client = new MinecraftRconClient();
private bool _isAuthenticated = false;
///
/// 此单元测试请自行搭建服务器测试(没必要=3=)
///
private void connectAndAuthenticate()
{
if (!_client.IsConnected)
{
_client.Connect("127.0.0.1", 25575);
_isAuthenticated = _client.Authenticate("123");
}
}
//[TestMethod]
public void Connect()
{
connectAndAuthenticate();
Assert.IsTrue(_client.IsConnected);
}
//[TestMethod]
public void Authenticate()
{
connectAndAuthenticate();
Assert.IsTrue(_isAuthenticated);
}
//[TestMethod]
public void Run()
{
connectAndAuthenticate();
string response = _client.Run("list");
Console.WriteLine(response);
Assert.IsTrue(response != string.Empty);
_client.Run("say 你好"); // UTF-8 测试
}
}
}
================================================
FILE: NectarRCON.Tests/DNSHelperTests.cs
================================================
using NectarRCON.Core.Helper;
namespace NectarRCON.Tests
{
[TestClass]
public class DNSHelperTests
{
///
/// A解析测试
///
[TestMethod]
public void ATest()
{
string? ip = DNSHelpers.AQuery("hypixel.net");
Console.WriteLine(ip);
Assert.IsNotNull(string.IsNullOrEmpty(ip));
}
///
/// SRV解析测试
///
[TestMethod]
public void SRVTest()
{
string? ip = DNSHelpers.SRVQuery("mc.125nf.com");
Console.WriteLine(ip);
Assert.IsFalse(string.IsNullOrEmpty(ip));
}
}
}
================================================
FILE: NectarRCON.Tests/GroupServiceTests.cs
================================================
using NectarRCON.Interfaces;
using NectarRCON.Models;
using NectarRCON.Services;
using System.Windows;
namespace NectarRCON.Tests
{
[TestClass]
public class GroupServiceTests
{
private class MyLanguageService : ILanguageService
{
public string GetKey(string key)
{
return key;
}
public Dictionary GetLanguages()
{
throw new NotImplementedException();
}
public ResourceDictionary GetSelectedLanguage()
{
throw new NotImplementedException();
}
public void Refresh()
{
throw new NotImplementedException();
}
public void SelectLanguage()
{
throw new NotImplementedException();
}
public void SelectLanguage(string languageName, bool name)
{
throw new NotImplementedException();
}
}
private readonly IGroupService _groupService = new GroupService(new MessageBoxService(new MyLanguageService()), new MyLanguageService());
[TestMethod]
public void Dump()
{
_groupService.GetGroups();
}
[TestMethod]
public void TestAll()
{
Group group = new Group()
{
Id = Guid.NewGuid().ToString(),
Name = "Test",
Servers = new(),
};
_groupService.Add(group);
Assert.ThrowsException(() =>
{
_groupService.Add(group); // 相同的Id 和Name
});
Assert.IsTrue(File.Exists("./groups/" + group.Id + ".json"));
_groupService.Delete(group.Id);
Assert.IsFalse(File.Exists("./groups/" + group.Id + ".json"));
Assert.IsNull(_groupService.FindGroup(group.Name));
Assert.IsNull(_groupService.GetGroup(group.Id));
Assert.IsTrue(_groupService.GetGroups().Count == 0);
}
}
}
================================================
FILE: NectarRCON.Tests/MessageBoxServiceTests.cs
================================================
using NectarRCON.Interfaces;
using NectarRCON.Services;
using System.Windows;
namespace NectarRCON.Tests
{
[TestClass]
public class MessageBoxServiceTests
{
private class MyLanguageService : ILanguageService
{
public string GetKey(string key)
{
return key;
}
public Dictionary GetLanguages()
{
throw new NotImplementedException();
}
public ResourceDictionary GetSelectedLanguage()
{
throw new NotImplementedException();
}
public void Refresh()
{
throw new NotImplementedException();
}
public void SelectLanguage()
{
throw new NotImplementedException();
}
public void SelectLanguage(string languageName, bool name)
{
throw new NotImplementedException();
}
}
private readonly IMessageBoxService _service = new MessageBoxService(new MyLanguageService());
[TestMethod]
public void ExceptionTest()
{
/// 我为什么要把这东西写进单元测试啊!!!!
//_service.Show(new Exception("测试异常"), "我故意哒!");
}
}
}
================================================
FILE: NectarRCON.Tests/NectarRCON.Tests.csproj
================================================
net7.0-windows
enable
enable
false
true
================================================
FILE: NectarRCON.Tests/UpdaterTests.cs
================================================
using NectarRCON.Updater;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace NectarRCON.Tests
{
[TestClass]
public class UpdaterTests
{
[TestMethod]
public void Github()
{
IUpdater updater = new GithubUpdater();
updater.SetVersion("NectarRcon-x86-1.0.0");
updater.IsLatestVersion();
}
[TestMethod]
public void AppVersionTest()
{
AppVersion versionA = AppVersion.ParseVersion("TestApp-x64-1.0.0-beta1");
AppVersion versionB = AppVersion.ParseVersion("TestApp-x64-1.0.0-beta2");
Assert.IsTrue(versionA.Equals(versionA));
Assert.IsFalse(versionA.Equals(versionB));
#pragma warning disable CS1718 // 对同一变量进行了比较
Assert.IsTrue(versionA == versionA);
Assert.IsFalse(versionA != versionA);
Assert.IsFalse(versionA > versionA);
#pragma warning restore CS1718 // 对同一变量进行了比较
Assert.IsTrue(versionB > versionA);
Assert.IsFalse(versionB < versionA);
}
}
}
================================================
FILE: NectarRCON.Tests/Usings.cs
================================================
global using Microsoft.VisualStudio.TestTools.UnitTesting;
================================================
FILE: NectarRCON.Updater/AppVersion.cs
================================================
using System.Runtime.CompilerServices;
using System.Text.RegularExpressions;
namespace NectarRCON.Updater
{
public class AppVersion
{
public string AppName { get; set; } = string.Empty;
public int Version { get; set; }
public int Major { get;set; }
public int Minor { get;set; }
public int Patch { get;set; }
public int? Build { get; set; }
public string PreReleaseType { get; set; } = string.Empty;
public string Platform { get; set; } = string.Empty;
public bool IsPreRelease
=> !string.IsNullOrEmpty(PreReleaseType);
public override string ToString()
{
return $"{AppName}-{Platform}-{Major}.{Minor}.{Patch}" + (IsPreRelease ? $"-{PreReleaseType}{Build}" : string.Empty);
}
public override bool Equals(object? obj)
{
return obj?.ToString() == ToString();
}
public static bool operator <(AppVersion a, AppVersion b)
{
return a.Version < b.Version || (a.Build ?? 0) < (b.Build ?? 0);
}
public static bool operator >(AppVersion a, AppVersion b)
{
return a.Version > b.Version || (a.Build ?? 0) > (b.Build ?? 0);
}
public static bool operator ==(AppVersion a, AppVersion b)
{
return a.Version == b.Version && (a.Build ?? 0) == (b.Build ?? 0);
}
public static bool operator !=(AppVersion a, AppVersion b)
{
return a.Version != b.Version || (a.Build ?? 0) != (b.Build ?? 0);
}
private AppVersion() { }
public static AppVersion ParseVersion(string version)
{
string[] versionParts = version.Split("-");
if (versionParts.Length > 2)
{
AppVersion result = new();
string name = versionParts[0];
string platform = versionParts[1];
string ver = versionParts[2];
string preRelease = string.Empty;
if (versionParts.Length > 3)
{
preRelease = versionParts[3];
}
Regex versionRegex = new(@"(?\d+)\.(?\d+)\.(?\d+)");
Match versionMatch = versionRegex.Match(ver);
if (versionMatch.Success)
{
result.Version = int.Parse(versionMatch.Groups["major"].Value + versionMatch.Groups["minor"].Value + versionMatch.Groups["patch"].Value);
result.Major = int.Parse(versionMatch.Groups["major"].Value);
result.Minor = int.Parse(versionMatch.Groups["minor"].Value);
result.Patch = int.Parse(versionMatch.Groups["patch"].Value);
}
Regex preReleaseRegex = new(@"(?[a-zA-Z]+)(?\d+)");
Match preReleaseMatch = preReleaseRegex.Match(preRelease);
if (preReleaseMatch.Success)
{
if (preReleaseMatch.Groups["build"].Success)
{
result.Build = int.Parse(preReleaseMatch.Groups["build"].Value);
}
if (preReleaseMatch.Groups["preRelease"].Success)
{
result.PreReleaseType = preReleaseMatch.Groups["preRelease"].Value;
}
}
result.Platform = platform;
result.AppName = name;
return result;
}
throw new InvalidOperationException("Invalid version format");
}
public override int GetHashCode()
{
return RuntimeHelpers.GetHashCode(ToString());
}
}
}
================================================
FILE: NectarRCON.Updater/GithubUpdater.cs
================================================
using NectarRCON.Updater.Model;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
namespace NectarRCON.Updater
{
public class GithubUpdater : IUpdater
{
private static readonly HttpClient _client = new()
{
BaseAddress = new Uri("https://api.github.com/repos/zkhssb/NectarRcon/")
};
private bool _preEnable = false;
private AppVersion? _version;
///
/// 获取最新版本, null为没找到
///
/// 是否允许pre版本
private AppVersion? GetLatestVersion(bool enablePre)
{
if (_version is null)
return null;
using(HttpRequestMessage request = new(HttpMethod.Get, "releases/latest"))
{
request.Headers.Add("User-Agent", $"{_version.AppName}-AppUpdater");
using(HttpResponseMessage response = _client.Send(request))
{
if (!response.IsSuccessStatusCode)
throw new HttpRequestException(response.StatusCode.ToString());
string resultString = string.Empty;
Task.Run(async () =>
{
resultString = await response.Content.ReadAsStringAsync();
}).Wait();
Release release = JsonSerializer.Deserialize(resultString) ?? throw new JsonException();
foreach(var asset in release.Assets)
{
string fileName = Path.GetFileNameWithoutExtension(asset.Name);
try
{
fileName = "NectarRcon-x86-1.0.0-beta2";
AppVersion version = AppVersion.ParseVersion(fileName);
if(version.AppName.ToLower() == _version.AppName.ToLower() && version.Platform.ToLower() == _version.Platform.ToLower())
{
if (version.IsPreRelease && !enablePre)
continue;
if (version > _version)
{
return version;
}
}
}
catch (InvalidOperationException) { } // Invalid version format
}
return null;
}
}
}
public bool IsLatestVersion()
{
GetLatestVersion(_preEnable);
return true;
}
public void Setup()
{
throw new NotImplementedException();
}
public void SetVersion(string version)
{
_version = AppVersion.ParseVersion(version);
}
public void SetPreEnable(bool value)
{
_preEnable = value;
}
public AppVersion GetLatestVersion()
{
throw new NotImplementedException();
}
}
}
================================================
FILE: NectarRCON.Updater/IUpdater.cs
================================================
namespace NectarRCON.Updater
{
public interface IUpdater
{
///
/// 设置版本
///
void SetVersion(string version);
///
/// 是最新版
///
bool IsLatestVersion();
///
/// 获取最新版本
///
AppVersion GetLatestVersion();
///
/// 开始安装
///
void Setup();
///
/// 设置是否启用获取预发布版本更新
///
void SetPreEnable(bool value);
}
}
================================================
FILE: NectarRCON.Updater/Model/Asset.cs
================================================
using System.Text.Json.Serialization;
namespace NectarRCON.Updater.Model
{
public class Asset
{
[JsonPropertyName("name")]
public required string Name { get; set; }
[JsonPropertyName("url")]
public required string Url { get; set; }
[JsonPropertyName("created_at")]
public required DateTime CreatedAt { get; set; }
}
}
================================================
FILE: NectarRCON.Updater/Model/Release.cs
================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
namespace NectarRCON.Updater.Model
{
public class Release
{
[JsonPropertyName("tag_name")]
public required string TagName { get; set; }
[JsonPropertyName("name")]
public required string Name { get; set; }
[JsonPropertyName("created_at")]
public required DateTime CreatedAt { get; set; }
[JsonPropertyName("assets")]
public required IEnumerable Assets { get; set; }
[JsonPropertyName("body")]
public required string Body { get; set; }
}
}
================================================
FILE: NectarRCON.Updater/NectarRCON.Updater.csproj
================================================
net7.0-windows
enable
enable
================================================
FILE: NectarRCON.sln
================================================
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.4.33205.214
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NectarRCON", "NectarRCON\NectarRCON.csproj", "{66C6A8ED-0D8D-43E9-86CD-03E823468761}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NectarRCON.Tests", "NectarRCON.Tests\NectarRCON.Tests.csproj", "{3363EC0E-04D8-4A7C-9A8E-ABF42B6FE431}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NectarRCON.Export", "NectarRCON.Export\NectarRCON.Export.csproj", "{49A2E218-1DED-4DEF-BD97-CCDE90C55630}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NectarRCON.Adapter.Minecraft", "NectarRCON.Adapter.Minecraft\NectarRCON.Adapter.Minecraft.csproj", "{D4B97850-FF59-4AA1-A19F-2C22F80A8B20}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NectarRCON.Core", "NectarRCON.Core\NectarRCON.Core.csproj", "{8C15668B-69F3-4138-BCE6-0BB6A65F3B2F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NectarRCON.Updater", "NectarRCON.Updater\NectarRCON.Updater.csproj", "{D6C910A7-3590-492B-9CA1-C3D586FF2C41}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{66C6A8ED-0D8D-43E9-86CD-03E823468761}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{66C6A8ED-0D8D-43E9-86CD-03E823468761}.Debug|Any CPU.Build.0 = Debug|Any CPU
{66C6A8ED-0D8D-43E9-86CD-03E823468761}.Release|Any CPU.ActiveCfg = Release|Any CPU
{66C6A8ED-0D8D-43E9-86CD-03E823468761}.Release|Any CPU.Build.0 = Release|Any CPU
{3363EC0E-04D8-4A7C-9A8E-ABF42B6FE431}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3363EC0E-04D8-4A7C-9A8E-ABF42B6FE431}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3363EC0E-04D8-4A7C-9A8E-ABF42B6FE431}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3363EC0E-04D8-4A7C-9A8E-ABF42B6FE431}.Release|Any CPU.Build.0 = Release|Any CPU
{49A2E218-1DED-4DEF-BD97-CCDE90C55630}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{49A2E218-1DED-4DEF-BD97-CCDE90C55630}.Debug|Any CPU.Build.0 = Debug|Any CPU
{49A2E218-1DED-4DEF-BD97-CCDE90C55630}.Release|Any CPU.ActiveCfg = Release|Any CPU
{49A2E218-1DED-4DEF-BD97-CCDE90C55630}.Release|Any CPU.Build.0 = Release|Any CPU
{D4B97850-FF59-4AA1-A19F-2C22F80A8B20}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D4B97850-FF59-4AA1-A19F-2C22F80A8B20}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D4B97850-FF59-4AA1-A19F-2C22F80A8B20}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D4B97850-FF59-4AA1-A19F-2C22F80A8B20}.Release|Any CPU.Build.0 = Release|Any CPU
{8C15668B-69F3-4138-BCE6-0BB6A65F3B2F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8C15668B-69F3-4138-BCE6-0BB6A65F3B2F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8C15668B-69F3-4138-BCE6-0BB6A65F3B2F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8C15668B-69F3-4138-BCE6-0BB6A65F3B2F}.Release|Any CPU.Build.0 = Release|Any CPU
{D6C910A7-3590-492B-9CA1-C3D586FF2C41}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D6C910A7-3590-492B-9CA1-C3D586FF2C41}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D6C910A7-3590-492B-9CA1-C3D586FF2C41}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D6C910A7-3590-492B-9CA1-C3D586FF2C41}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {459208B6-E3FC-4AE1-B1D4-DCE3E0C7DC6E}
EndGlobalSection
EndGlobal
================================================
FILE: README.md
================================================
NectarRCON
使用
WPF
开发的Rcon管理器
♥ 此项目长期维护,如有任何BUG或奇妙的想法都可以提交Issue!
## 支持的游戏
- 和Minecraft使用同一个Rcon协议的游戏或应用
## 能力
- 连接单个服务器
- 分组服务器 (将命令广播到组内的所有服务器)
- 多语言支持(目前内部支持中文和英文,支持自定义其他语言)
- 支持解析SRV服务器地址
- 内置两种主题(这不算能力吧喂!!!)
## 后续想法
没错!!! 我们的项目正在蒸蒸日上(bushi)
- 其他Rcon协议支持 (最优先)
- 日志部分重构
- 自动更新支持 (有想法)
- 自动脚本执行 (有想法但暂无需求)
- 基于Rcon的插件 (有需求但懒)
## 关于本项目
学习 WPF 时突发奇想做的一个工具,代码可能不太美观(各种乱七八糟,一锅乱炖的写法)
## 项目依赖
| 名字 | 地址 |
| ------------------------- | ------------------------------- |
| WPF-UI (作者已废弃的版本) | https://github.com/lepoco/wpfui |
## 部分截图
点击展开
主页面 / Home Page

需要密码 / PasswordPage

设置页面 / Settings page

连接成功/Command page

================================================
FILE: README_EN.md
================================================
NectarRCON
An Rcon manager developed using WPF
♥ This project is maintained long-term. Feel free to submit any bugs or wonderful ideas as issues!
## Supported Games
- Games or applications that use the same Rcon protocol as Minecraft
## Capabilities
- Connect to a single server
- Group servers (broadcast commands to all servers in the group)
- Multi-language support (currently supports Chinese and English, with support for customizing other languages)
- Support for SRV server addresses
- Two built-in themes
## Future Plans
Yes!!! Our project is thriving (Haha)
- Support for other Rcon protocols (top priority)
- Reconstruction of the log section
- Automatic update support (ideas in mind)
- Automatic script execution (ideas in mind but no current demand)
- Rcon-based plugins (demand exists but I'm lazy)
## About This Project
A tool I came up with while learning WPF. The code may not be aesthetically pleasing (a mishmash of various things, written in a haphazard manner)
## Project Dependencies
| Name | Address |
| --------------------------- | ------------------------------- |
| WPF-UI (Deprecated Version) | https://github.com/lepoco/wpfui |
## Screenshots
Click to expand
Home Page

Password Page

Settings Page

Command Page (Connection Successful)
