chore: initial commit

This commit is contained in:
Anna 2020-10-23 17:24:32 -04:00
commit b934dedcfb
Signed by: anna
GPG Key ID: 0B391D8F06FCD9E0
36 changed files with 4295 additions and 0 deletions

203
.editorconfig Normal file
View File

@ -0,0 +1,203 @@
# Remove the line below if you want to inherit .editorconfig settings from higher directories
root = true
# C# files
[*.{cs,json}]
#### Core EditorConfig Options ####
# Indentation and spacing
indent_size = 4
indent_style = space
tab_width = 4
# New line preferences
end_of_line = lf
insert_final_newline = true
[*.cs]
#### .NET Coding Conventions ####
# Organize usings
dotnet_separate_import_directive_groups = false
dotnet_sort_system_directives_first = false
file_header_template = unset
# this. and Me. preferences
dotnet_style_qualification_for_event = true:error
dotnet_style_qualification_for_field = true:error
dotnet_style_qualification_for_method = true:error
dotnet_style_qualification_for_property = true:error
# Language keywords vs BCL types preferences
dotnet_style_predefined_type_for_locals_parameters_members = true:silent
dotnet_style_predefined_type_for_member_access = true:silent
# Parentheses preferences
dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent
dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent
dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent
dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent
# Modifier preferences
dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent
# Expression-level preferences
dotnet_style_coalesce_expression = true:suggestion
dotnet_style_collection_initializer = true:suggestion
dotnet_style_explicit_tuple_names = true:suggestion
dotnet_style_null_propagation = true:suggestion
dotnet_style_object_initializer = true:suggestion
dotnet_style_operator_placement_when_wrapping = beginning_of_line
dotnet_style_prefer_auto_properties = true:silent
dotnet_style_prefer_compound_assignment = true:suggestion
dotnet_style_prefer_conditional_expression_over_assignment = true:silent
dotnet_style_prefer_conditional_expression_over_return = true:silent
dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
dotnet_style_prefer_inferred_tuple_names = true:suggestion
dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion
dotnet_style_prefer_simplified_boolean_expressions = true:suggestion
dotnet_style_prefer_simplified_interpolation = true:suggestion
# Field preferences
dotnet_style_readonly_field = true:suggestion
# Parameter preferences
dotnet_code_quality_unused_parameters = all:suggestion
#### C# Coding Conventions ####
# var preferences
csharp_style_var_elsewhere = false:silent
csharp_style_var_for_built_in_types = false:silent
csharp_style_var_when_type_is_apparent = false:silent
# Expression-bodied members
csharp_style_expression_bodied_accessors = true:silent
csharp_style_expression_bodied_constructors = false:silent
csharp_style_expression_bodied_indexers = true:silent
csharp_style_expression_bodied_lambdas = true:silent
csharp_style_expression_bodied_local_functions = false:silent
csharp_style_expression_bodied_methods = false:silent
csharp_style_expression_bodied_operators = false:silent
csharp_style_expression_bodied_properties = true:silent
# Pattern matching preferences
csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
csharp_style_prefer_switch_expression = true:suggestion
# Null-checking preferences
csharp_style_conditional_delegate_call = true:suggestion
# Modifier preferences
csharp_prefer_static_local_function = true:suggestion
csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:silent
# Code-block preferences
csharp_prefer_braces = true:error
csharp_prefer_simple_using_statement = true:suggestion
# Expression-level preferences
csharp_prefer_simple_default_expression = true:suggestion
csharp_style_deconstructed_variable_declaration = true:suggestion
csharp_style_inlined_variable_declaration = true:suggestion
csharp_style_pattern_local_over_anonymous_function = true:suggestion
csharp_style_prefer_index_operator = true:suggestion
csharp_style_prefer_range_operator = true:suggestion
csharp_style_throw_expression = true:suggestion
csharp_style_unused_value_assignment_preference = discard_variable:suggestion
csharp_style_unused_value_expression_statement_preference = discard_variable:silent
# 'using' directive preferences
csharp_using_directive_placement = outside_namespace:silent
#### C# Formatting Rules ####
# New line preferences
csharp_new_line_before_catch = false
csharp_new_line_before_else = false
csharp_new_line_before_finally = false
csharp_new_line_before_members_in_anonymous_types = true
csharp_new_line_before_members_in_object_initializers = true
csharp_new_line_before_open_brace = none
csharp_new_line_between_query_expression_clauses = true
# Indentation preferences
csharp_indent_block_contents = true
csharp_indent_braces = false
csharp_indent_case_contents = true
csharp_indent_case_contents_when_block = true
csharp_indent_labels = one_less_than_current
csharp_indent_switch_labels = true
# Space preferences
csharp_space_after_cast = false
csharp_space_after_colon_in_inheritance_clause = true
csharp_space_after_comma = true
csharp_space_after_dot = false
csharp_space_after_keywords_in_control_flow_statements = true
csharp_space_after_semicolon_in_for_statement = true
csharp_space_around_binary_operators = before_and_after
csharp_space_around_declaration_statements = false
csharp_space_before_colon_in_inheritance_clause = true
csharp_space_before_comma = false
csharp_space_before_dot = false
csharp_space_before_open_square_brackets = false
csharp_space_before_semicolon_in_for_statement = false
csharp_space_between_empty_square_brackets = false
csharp_space_between_method_call_empty_parameter_list_parentheses = false
csharp_space_between_method_call_name_and_opening_parenthesis = false
csharp_space_between_method_call_parameter_list_parentheses = false
csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
csharp_space_between_method_declaration_name_and_open_parenthesis = false
csharp_space_between_method_declaration_parameter_list_parentheses = false
csharp_space_between_parentheses = false
csharp_space_between_square_brackets = false
# Wrapping preferences
csharp_preserve_single_line_blocks = true
csharp_preserve_single_line_statements = true
#### Naming styles ####
# Naming rules
dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion
dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface
dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i
dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.types_should_be_pascal_case.symbols = types
dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case
dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members
dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case
# Symbol specifications
dotnet_naming_symbols.interface.applicable_kinds = interface
dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.interface.required_modifiers =
dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum
dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.types.required_modifiers =
dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.non_field_members.required_modifiers =
# Naming styles
dotnet_naming_style.pascal_case.required_prefix =
dotnet_naming_style.pascal_case.required_suffix =
dotnet_naming_style.pascal_case.word_separator =
dotnet_naming_style.pascal_case.capitalization = pascal_case
dotnet_naming_style.begins_with_i.required_prefix = I
dotnet_naming_style.begins_with_i.required_suffix =
dotnet_naming_style.begins_with_i.word_separator =
dotnet_naming_style.begins_with_i.capitalization = pascal_case

2
.gitattributes vendored Normal file
View File

@ -0,0 +1,2 @@
* text eol=lf
*.wav binary

362
.gitignore vendored Normal file
View File

@ -0,0 +1,362 @@
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Mono auto generated files
mono_crash.*
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
[Ll]ogs/
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUnit
*.VisualState.xml
TestResult.xml
nunit-*.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
# ASP.NET Scaffolding
ScaffoldingReadMe.txt
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.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

9
XIVChat Desktop/App.xaml Normal file
View File

@ -0,0 +1,9 @@
<Application x:Class="XIVChat_Desktop.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:XIVChat_Desktop"
StartupUri="MainWindow.xaml">
<Application.Resources>
</Application.Resources>
</Application>

View File

@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Data;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
namespace XIVChat_Desktop {
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application {
}
}

View File

@ -0,0 +1,10 @@
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)
)]

View File

@ -0,0 +1,25 @@
<Window x:Class="XIVChat_Desktop.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:XIVChat_Desktop"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Menu Grid.Row="1">
<MenuItem Header="XIVChat">
<MenuItem Header="Connect"/>
<MenuItem Header="Disonnect" IsEnabled="False"/>
</MenuItem>
</Menu>
<ListView Grid.Row="2"></ListView>
<TextBox Grid.Row="3" TextWrapping="Wrap"/>
</Grid>
</Window>

View File

@ -0,0 +1,25 @@
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.Navigation;
using System.Windows.Shapes;
namespace XIVChat_Desktop {
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window {
public MainWindow() {
InitializeComponent();
}
}
}

View File

@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
<RootNamespace>XIVChat_Desktop</RootNamespace>
<UseWPF>true</UseWPF>
</PropertyGroup>
</Project>

42
XIVChat.sln Normal file
View File

@ -0,0 +1,42 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.30503.244
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XIVChatPlugin", "XIVChatPlugin\XIVChatPlugin.csproj", "{9AEB2B77-C127-4BA8-B9AE-FB3CA1649FDB}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{9F0334E6-42D0-4CCE-888B-64DBE39D8407}"
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XIVChatCommon", "XIVChatCommon\XIVChatCommon.csproj", "{6F426EAB-15D4-48AC-80D2-2D2DF2CE5EE0}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XIVChat Desktop", "XIVChat Desktop\XIVChat Desktop.csproj", "{D2773EAE-6B9E-4017-9681-585AAB5A99F4}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{9AEB2B77-C127-4BA8-B9AE-FB3CA1649FDB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9AEB2B77-C127-4BA8-B9AE-FB3CA1649FDB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9AEB2B77-C127-4BA8-B9AE-FB3CA1649FDB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9AEB2B77-C127-4BA8-B9AE-FB3CA1649FDB}.Release|Any CPU.Build.0 = Release|Any CPU
{6F426EAB-15D4-48AC-80D2-2D2DF2CE5EE0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6F426EAB-15D4-48AC-80D2-2D2DF2CE5EE0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6F426EAB-15D4-48AC-80D2-2D2DF2CE5EE0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6F426EAB-15D4-48AC-80D2-2D2DF2CE5EE0}.Release|Any CPU.Build.0 = Release|Any CPU
{D2773EAE-6B9E-4017-9681-585AAB5A99F4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D2773EAE-6B9E-4017-9681-585AAB5A99F4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D2773EAE-6B9E-4017-9681-585AAB5A99F4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D2773EAE-6B9E-4017-9681-585AAB5A99F4}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {5833B759-E940-40DC-8768-B20D1257FF86}
EndGlobalSection
EndGlobal

View File

@ -0,0 +1,101 @@
using Sodium;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace XIVChatCommon {
public static class KeyExchange {
public static SessionKeys ClientSessionKeys(KeyPair client, byte[] serverPublic) {
var secret = ScalarMult.Mult(client.PrivateKey, serverPublic);
var combined = secret
.Concat(client.PublicKey)
.Concat(serverPublic)
.ToArray();
var hash = GenericHash.Hash(combined, null, 64);
byte[] rx = new byte[32];
byte[] tx = new byte[32];
for (int i = 0; i < 32; i++) {
rx[i] = hash[i];
tx[i] = hash[i + 32];
}
return new SessionKeys(rx, tx);
}
public static SessionKeys ServerSessionKeys(KeyPair server, byte[] clientPublic) {
var secret = ScalarMult.Mult(server.PrivateKey, clientPublic);
var combined = secret
.Concat(clientPublic)
.Concat(server.PublicKey)
.ToArray();
var hash = GenericHash.Hash(combined, null, 64);
byte[] rx = new byte[32];
byte[] tx = new byte[32];
for (int i = 0; i < 32; i++) {
tx[i] = hash[i];
rx[i] = hash[i + 32];
}
return new SessionKeys(rx, tx);
}
public async static Task<HandshakeInfo> ServerHandshake(KeyPair server, Stream stream) {
// get client public key
byte[] clientPublic = new byte[32];
await stream.ReadAsync(clientPublic, 0, clientPublic.Length);
// send our public key
await stream.WriteAsync(server.PublicKey, 0, server.PublicKey.Length);
// get shared secret and derive keys
var keys = ServerSessionKeys(server, clientPublic);
return new HandshakeInfo(clientPublic, keys);
}
public async static Task<HandshakeInfo> ClientHandshake(KeyPair client, Stream stream) {
// send our public key
await stream.WriteAsync(client.PublicKey, 0, client.PublicKey.Length);
// get server public key
byte[] serverPublic = new byte[32];
await stream.ReadAsync(serverPublic, 0, serverPublic.Length);
// get shared secret and derive keys
var keys = ClientSessionKeys(client, serverPublic);
return new HandshakeInfo(serverPublic, keys);
}
}
public class SessionKeys {
public readonly byte[] rx;
public readonly byte[] tx;
internal SessionKeys(byte[] rx, byte[] tx) {
this.rx = rx;
this.tx = tx;
}
}
public class HandshakeInfo {
public byte[] RemotePublicKey { get; private set; }
public SessionKeys Keys { get; private set; }
internal HandshakeInfo(byte[] remote, SessionKeys keys) {
this.RemotePublicKey = remote;
this.Keys = keys;
}
}
}

805
XIVChatCommon/Message.cs Normal file
View File

@ -0,0 +1,805 @@
using MessagePack;
using MessagePack.Formatters;
using System;
using System.Collections.Generic;
namespace XIVChatCommon {
[MessagePackObject]
public class ServerMessage : IEncodable {
[MessagePackFormatter(typeof(MillisecondsDateTimeFormatter))]
[Key(0)]
public DateTime Timestamp { get; set; }
[Key(1)]
public ChatType Channel { get; set; }
[Key(2)]
public byte[] Sender { get; set; }
[Key(3)]
public byte[] Content { get; set; }
[Key(4)]
public List<Chunk> Chunks { get; set; }
[IgnoreMember]
public string ContentText => XivString.GetText(this.Content);
[IgnoreMember]
public string SenderText => XivString.GetText(this.Sender);
[IgnoreMember]
protected override byte Code => (byte)ServerOperation.Message;
public static ServerMessage Decode(byte[] bytes) {
return MessagePackSerializer.Deserialize<ServerMessage>(bytes);
}
protected override byte[] PayloadEncode() {
return MessagePackSerializer.Serialize(this);
}
}
[Union(1, typeof(TextChunk))]
[Union(2, typeof(IconChunk))]
[MessagePackObject]
public abstract class Chunk {
}
[MessagePackObject]
public class TextChunk : Chunk {
[Key(0)]
public uint? FallbackColour { get; set; }
[Key(1)]
public uint? Foreground { get; set; }
[Key(2)]
public uint? Glow { get; set; }
[Key(3)]
public bool Italic { get; set; }
[Key(4)]
public string Content { get; set; }
}
[MessagePackObject]
public class IconChunk : Chunk {
[Key(0)]
public byte Index;
}
public class NameFormatting {
public string Before { get; private set; } = string.Empty;
public string After { get; private set; } = string.Empty;
public bool IsPresent { get; private set; } = true;
public static NameFormatting Empty() {
return new NameFormatting {
IsPresent = false,
};
}
public static NameFormatting Of(string before, string after) {
return new NameFormatting {
Before = before,
After = after,
};
}
public static NameFormatting Basic() {
return new NameFormatting {
Before = "",
After = ": ",
};
}
}
public class ChatCode {
private const ushort CLEAR_7 = ~(~0 << 7);
private readonly ushort code;
public ChatType Type => (ChatType)(this.code & CLEAR_7);
public ChatSource Source => this.SourceFrom(11);
public ChatSource Target => this.SourceFrom(7);
private ChatSource SourceFrom(ushort shift) => (ChatSource)(1 << ((this.code >> shift) & 0xF));
public ChatCode(ushort code) {
this.code = code;
}
public NameFormatting NameFormat() {
switch (this.Type) {
case ChatType.Say:
case ChatType.Shout:
case ChatType.Yell:
case ChatType.NpcAnnouncement:
case ChatType.NpcDialogue:
return NameFormatting.Of("", ": ");
case ChatType.TellOutgoing:
return NameFormatting.Of(">> ", ": ");
case ChatType.TellIncoming:
return NameFormatting.Of("", " >> ");
case ChatType.GmTell:
return NameFormatting.Of("[GM]", " >> ");
case ChatType.GmSay:
case ChatType.GmShout:
case ChatType.GmYell:
return NameFormatting.Of("[GM]", ": ");
case ChatType.GmParty:
return NameFormatting.Of("([GM]", ") ");
case ChatType.GmFreeCompany:
return NameFormatting.Of("[FC]<[GM]", "> ");
case ChatType.GmLinkshell1:
return NameFormatting.Of("[1]<[GM]", "> ");
case ChatType.GmLinkshell2:
return NameFormatting.Of("[2]<[GM]", "> ");
case ChatType.GmLinkshell3:
return NameFormatting.Of("[3]<[GM]", "> ");
case ChatType.GmLinkshell4:
return NameFormatting.Of("[4]<[GM]", "> ");
case ChatType.GmLinkshell5:
return NameFormatting.Of("[5]<[GM]", "> ");
case ChatType.GmLinkshell6:
return NameFormatting.Of("[6]<[GM]", "> ");
case ChatType.GmLinkshell7:
return NameFormatting.Of("[7]<[GM]", "> ");
case ChatType.GmLinkshell8:
return NameFormatting.Of("[8]<[GM]", "> ");
case ChatType.GmNoviceNetwork:
return NameFormatting.Of("[NOVICE][GM]", ": ");
case ChatType.Party:
case ChatType.CrossParty:
return NameFormatting.Of("(", ") ");
case ChatType.Alliance:
return NameFormatting.Of("((", ")) ");
case ChatType.PvpTeam:
return NameFormatting.Of("[PVP]<", "> ");
case ChatType.FreeCompany:
return NameFormatting.Of("[FC]<", "> ");
case ChatType.Linkshell1:
return NameFormatting.Of("[1]<", "> ");
case ChatType.Linkshell2:
return NameFormatting.Of("[2]<", "> ");
case ChatType.Linkshell3:
return NameFormatting.Of("[3]<", "> ");
case ChatType.Linkshell4:
return NameFormatting.Of("[4]<", "> ");
case ChatType.Linkshell5:
return NameFormatting.Of("[5]<", "> ");
case ChatType.Linkshell6:
return NameFormatting.Of("[6]<", "> ");
case ChatType.Linkshell7:
return NameFormatting.Of("[7]<", "> ");
case ChatType.Linkshell8:
return NameFormatting.Of("[8]<", "> ");
case ChatType.StandardEmote:
return NameFormatting.Empty();
case ChatType.CustomEmote:
return NameFormatting.Of("", "");
case ChatType.CrossLinkshell1:
return NameFormatting.Of("[CWLS1]<", "> ");
case ChatType.CrossLinkshell2:
return NameFormatting.Of("[CWLS2]<", "> ");
case ChatType.CrossLinkshell3:
return NameFormatting.Of("[CWLS3]<", "> ");
case ChatType.CrossLinkshell4:
return NameFormatting.Of("[CWLS4]<", "> ");
case ChatType.CrossLinkshell5:
return NameFormatting.Of("[CWLS5]<", "> ");
case ChatType.CrossLinkshell6:
return NameFormatting.Of("[CWLS6]<", "> ");
case ChatType.CrossLinkshell7:
return NameFormatting.Of("[CWLS7]<", "> ");
case ChatType.CrossLinkshell8:
return NameFormatting.Of("[CWLS8]<", "> ");
case ChatType.NoviceNetwork:
return NameFormatting.Of("[NOVICE]", ": ");
default:
return null;
}
}
public bool IsBattle() {
switch (this.Type) {
case ChatType.Damage:
case ChatType.Miss:
case ChatType.Action:
case ChatType.Item:
case ChatType.Healing:
case ChatType.GainBuff:
case ChatType.LoseBuff:
case ChatType.GainDebuff:
case ChatType.LoseDebuff:
case ChatType.BattleSystem:
return true;
default:
return false;
}
}
public uint? DefaultColour() {
switch (this.Type) {
case ChatType.Debug:
return Rgba(204, 204, 204);
case ChatType.Urgent:
return Rgba(255, 127, 127);
case ChatType.Notice:
return Rgba(179, 140, 255);
case ChatType.Say:
return Rgba(247, 247, 247);
case ChatType.Shout:
return Rgba(255, 166, 102);
case ChatType.TellIncoming:
case ChatType.TellOutgoing:
case ChatType.GmTell:
return Rgba(255, 184, 222);
case ChatType.Party:
case ChatType.CrossParty:
return Rgba(102, 229, 255);
case ChatType.Alliance:
return Rgba(255, 127, 0);
case ChatType.NoviceNetwork:
case ChatType.NoviceNetworkSystem:
return Rgba(212, 255, 125);
case ChatType.Linkshell1:
case ChatType.Linkshell2:
case ChatType.Linkshell3:
case ChatType.Linkshell4:
case ChatType.Linkshell5:
case ChatType.Linkshell6:
case ChatType.Linkshell7:
case ChatType.Linkshell8:
case ChatType.CrossLinkshell1:
case ChatType.CrossLinkshell2:
case ChatType.CrossLinkshell3:
case ChatType.CrossLinkshell4:
case ChatType.CrossLinkshell5:
case ChatType.CrossLinkshell6:
case ChatType.CrossLinkshell7:
case ChatType.CrossLinkshell8:
return Rgba(212, 255, 125);
case ChatType.StandardEmote:
return Rgba(186, 255, 240);
case ChatType.CustomEmote:
return Rgba(186, 255, 240);
case ChatType.Yell:
return Rgba(255, 255, 0);
case ChatType.Echo:
return Rgba(204, 204, 204);
case ChatType.System:
case ChatType.GatheringSystem:
case ChatType.PeriodicRecruitmentNotification:
case ChatType.Orchestrion:
case ChatType.Alarm:
case ChatType.RetainerSale:
case ChatType.Sign:
case ChatType.MessageBook:
return Rgba(204, 204, 204);
case ChatType.NpcAnnouncement:
case ChatType.NpcDialogue:
return Rgba(171, 214, 71);
case ChatType.Error:
return Rgba(255, 74, 74);
case ChatType.FreeCompany:
case ChatType.FreeCompanyAnnouncement:
case ChatType.FreeCompanyLoginLogout:
return Rgba(171, 219, 229);
case ChatType.PvpTeam:
return Rgba(171, 219, 229);
case ChatType.PvpTeamAnnouncement:
case ChatType.PvpTeamLoginLogout:
return Rgba(171, 219, 229);
case ChatType.Action:
case ChatType.Item:
case ChatType.LootNotice:
return Rgba(255, 255, 176);
case ChatType.Progress:
return Rgba(255, 222, 115);
case ChatType.LootRoll:
case ChatType.RandomNumber:
return Rgba(199, 191, 158);
case ChatType.Crafting:
case ChatType.Gathering:
return Rgba(222, 191, 247);
case ChatType.Damage:
return Rgba(255, 125, 125);
case ChatType.Miss:
return Rgba(204, 204, 204);
case ChatType.Healing:
return Rgba(212, 255, 125);
case ChatType.GainBuff:
case ChatType.LoseBuff:
return Rgba(148, 191, 255);
case ChatType.GainDebuff:
case ChatType.LoseDebuff:
return Rgba(255, 138, 196);
case ChatType.BattleSystem:
return Rgba(204, 204, 204);
default:
return null;
}
}
private static uint Rgba(byte red, byte green, byte blue, byte alpha = 0xFF) => alpha
| (uint)(red << 24)
| (uint)(green << 16)
| (uint)(blue << 8);
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1028:Enum Storage should be Int32")]
public enum ChatType : ushort {
Debug = 1,
Urgent = 2,
Notice = 3,
Say = 10,
Shout = 11,
TellOutgoing = 12,
TellIncoming = 13,
Party = 14,
Alliance = 15,
Linkshell1 = 16,
Linkshell2 = 17,
Linkshell3 = 18,
Linkshell4 = 19,
Linkshell5 = 20,
Linkshell6 = 21,
Linkshell7 = 22,
Linkshell8 = 23,
FreeCompany = 24,
NoviceNetwork = 27,
CustomEmote = 28,
StandardEmote = 29,
Yell = 30,
// 31 - also party?
CrossParty = 32,
PvpTeam = 36,
CrossLinkshell1 = 37,
Damage = 41,
Miss = 42,
Action = 43,
Item = 44,
Healing = 45,
GainBuff = 46,
GainDebuff = 47,
LoseBuff = 48,
LoseDebuff = 49,
Alarm = 55,
Echo = 56,
System = 57,
BattleSystem = 58,
GatheringSystem = 59,
Error = 60,
NpcDialogue = 61,
LootNotice = 62,
Progress = 64,
LootRoll = 65,
Crafting = 66,
Gathering = 67,
NpcAnnouncement = 68,
FreeCompanyAnnouncement = 69,
FreeCompanyLoginLogout = 70,
RetainerSale = 71,
PeriodicRecruitmentNotification = 72,
Sign = 73,
RandomNumber = 74,
NoviceNetworkSystem = 75,
Orchestrion = 76,
PvpTeamAnnouncement = 77,
PvpTeamLoginLogout = 78,
MessageBook = 79,
GmTell = 80,
GmSay = 81,
GmShout = 82,
GmYell = 83,
GmParty = 84,
GmFreeCompany = 85,
GmLinkshell1 = 86,
GmLinkshell2 = 87,
GmLinkshell3 = 88,
GmLinkshell4 = 89,
GmLinkshell5 = 90,
GmLinkshell6 = 91,
GmLinkshell7 = 92,
GmLinkshell8 = 93,
GmNoviceNetwork = 94,
CrossLinkshell2 = 101,
CrossLinkshell3 = 102,
CrossLinkshell4 = 103,
CrossLinkshell5 = 104,
CrossLinkshell6 = 105,
CrossLinkshell7 = 106,
CrossLinkshell8 = 107,
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1028:Enum Storage should be Int32")]
public enum ChatSource : ushort {
Self = 2,
PartyMember = 4,
AllianceMember = 8,
Other = 16,
EngagedEnemy = 32,
UnengagedEnemy = 64,
FriendlyNpc = 128,
SelfPet = 256,
PartyPet = 512,
AlliancePet = 1024,
OtherPet = 2048,
}
[MessagePackObject]
public class ClientMessage : IEncodable {
[Key(0)]
public string Content { get; set; }
[IgnoreMember]
protected override byte Code => (byte)ClientOperation.Message;
public static ClientMessage Decode(byte[] bytes) {
return MessagePackSerializer.Deserialize<ClientMessage>(bytes);
}
protected override byte[] PayloadEncode() {
return MessagePackSerializer.Serialize(this);
}
}
public enum ServerOperation : byte {
/// <summary>
/// Sent in response to a client ping. Has no payload.
/// </summary>
Pong = 1,
/// <summary>
/// A message was sent in game and is being relayed to the client.
/// </summary>
Message = 2,
/// <summary>
/// The server is shutting down. Clients should send no response and close their sockets. Has no payload.
/// </summary>
Shutdown = 3,
PlayerData = 4,
Availability = 5,
Channel = 6,
Backlog = 7,
PlayerList = 8,
LinkshellList = 9,
}
[MessagePackObject]
public class PlayerData : IEncodable {
[Key(0)]
public readonly string homeWorld;
[Key(1)]
public readonly string currentWorld;
[Key(2)]
public readonly string location;
[Key(3)]
public readonly string name;
public PlayerData(string homeWorld, string currentWorld, string location, string name) {
this.homeWorld = homeWorld;
this.currentWorld = currentWorld;
this.location = location;
this.name = name;
}
[IgnoreMember]
protected override byte Code => (byte)ServerOperation.PlayerData;
public static PlayerData Decode(byte[] bytes) {
return MessagePackSerializer.Deserialize<PlayerData>(bytes);
}
protected override byte[] PayloadEncode() {
return MessagePackSerializer.Serialize(this);
}
}
public class EmptyPlayerData : IEncodable {
public static EmptyPlayerData Instance { get; } = new EmptyPlayerData();
[IgnoreMember]
protected override byte Code => (byte)ServerOperation.PlayerData;
protected override byte[] PayloadEncode() {
return new byte[0];
}
}
[MessagePackObject]
public class Availability : IEncodable {
[Key(0)]
public readonly bool available;
public Availability(bool available) {
this.available = available;
}
[IgnoreMember]
protected override byte Code => (byte)ServerOperation.Availability;
public static Availability Decode(byte[] bytes) {
return MessagePackSerializer.Deserialize<Availability>(bytes);
}
protected override byte[] PayloadEncode() {
return MessagePackSerializer.Serialize(this);
}
}
[MessagePackObject]
public class ServerChannel : IEncodable {
[Key(0)]
public readonly byte channel;
[Key(1)]
public readonly string name;
[IgnoreMember]
public InputChannel InputChannel => (InputChannel)this.channel;
protected override byte Code => (byte)ServerOperation.Channel;
public ServerChannel(InputChannel channel, string name) : this((byte)channel, name) { }
public ServerChannel(byte channel, string name) {
this.channel = channel;
this.name = name;
}
public static ServerChannel Decode(byte[] bytes) {
return MessagePackSerializer.Deserialize<ServerChannel>(bytes);
}
protected override byte[] PayloadEncode() {
return MessagePackSerializer.Serialize(this);
}
}
public enum InputChannel : byte {
Tell = 0,
Say = 1,
Party = 2,
Alliance = 3,
Yell = 4,
Shout = 5,
FreeCompany = 6,
PvpTeam = 7,
NoviceNetwork = 8,
CrossLinkshell1 = 9,
CrossLinkshell2 = 10,
CrossLinkshell3 = 11,
CrossLinkshell4 = 12,
CrossLinkshell5 = 13,
CrossLinkshell6 = 14,
CrossLinkshell7 = 15,
CrossLinkshell8 = 16,
// 17 - unused?
// 18 - unused?
Linkshell1 = 19,
Linkshell2 = 20,
Linkshell3 = 21,
Linkshell4 = 22,
Linkshell5 = 23,
Linkshell6 = 24,
Linkshell7 = 25,
Linkshell8 = 26,
}
public class Pong : IEncodable {
public static Pong Instance { get; } = new Pong();
[IgnoreMember]
protected override byte Code => (byte)ServerOperation.Pong;
protected override byte[] PayloadEncode() {
return new byte[0];
}
}
public class Ping : IEncodable {
public static Ping Instance { get; } = new Ping();
[IgnoreMember]
protected override byte Code => (byte)ClientOperation.Ping;
protected override byte[] PayloadEncode() {
return new byte[0];
}
}
public class ServerShutdown : IEncodable {
public static ServerShutdown Instance { get; } = new ServerShutdown();
[IgnoreMember]
protected override byte Code => (byte)ServerOperation.Shutdown;
protected override byte[] PayloadEncode() {
return new byte[0];
}
}
public class ClientShutdown : IEncodable {
public static ClientShutdown Instance { get; } = new ClientShutdown();
[IgnoreMember]
protected override byte Code => (byte)ClientOperation.Shutdown;
protected override byte[] PayloadEncode() {
return new byte[0];
}
}
[MessagePackObject]
public class ServerBacklog : IEncodable {
[Key(0)]
public readonly ServerMessage[] messages;
protected override byte Code => (byte)ServerOperation.Backlog;
public ServerBacklog(ServerMessage[] messages) {
this.messages = messages;
}
public static ServerBacklog Decode(byte[] bytes) {
return MessagePackSerializer.Deserialize<ServerBacklog>(bytes);
}
protected override byte[] PayloadEncode() {
return MessagePackSerializer.Serialize(this);
}
}
[MessagePackObject]
public class ClientBacklog : IEncodable {
[Key(0)]
public ushort Amount { get; set; }
protected override byte Code => (byte)ClientOperation.Backlog;
public static ClientBacklog Decode(byte[] bytes) {
return MessagePackSerializer.Deserialize<ClientBacklog>(bytes);
}
protected override byte[] PayloadEncode() {
return MessagePackSerializer.Serialize(this);
}
}
[MessagePackObject]
public class ClientCatchUp : IEncodable {
[MessagePackFormatter(typeof(MillisecondsDateTimeFormatter))]
[Key(0)]
public DateTime After { get; set; }
protected override byte Code => (byte)ClientOperation.CatchUp;
public static ClientCatchUp Decode(byte[] bytes) {
return MessagePackSerializer.Deserialize<ClientCatchUp>(bytes);
}
protected override byte[] PayloadEncode() {
return MessagePackSerializer.Serialize(this);
}
}
[MessagePackObject]
public class ServerPlayerList : IEncodable {
[Key(0)]
public PlayerListType Type { get; set; }
[Key(1)]
public Player[] Players { get; set; }
protected override byte Code => (byte)ServerOperation.PlayerList;
public static ServerPlayerList Decode(byte[] bytes) {
return MessagePackSerializer.Deserialize<ServerPlayerList>(bytes);
}
protected override byte[] PayloadEncode() {
return MessagePackSerializer.Serialize(this);
}
}
[MessagePackObject]
public class ClientPlayerList : IEncodable {
[Key(0)]
public PlayerListType Type { get; set; }
protected override byte Code => (byte)ClientOperation.PlayerList;
public static ClientPlayerList Decode(byte[] bytes) {
return MessagePackSerializer.Deserialize<ClientPlayerList>(bytes);
}
protected override byte[] PayloadEncode() {
return MessagePackSerializer.Serialize(this);
}
}
public enum PlayerListType : byte {
Party = 1,
Friend = 2,
Linkshell = 3,
CrossLinkshell = 4,
}
[MessagePackObject]
public class Player {
[Key(0)]
public string Name { get; set; }
[Key(1)]
public string FreeCompany { get; set; }
[Key(2)]
public ulong Status { get; set; }
[Key(3)]
public ushort CurrentWorld { get; set; }
[Key(4)]
public string CurrentWorldName { get; set; }
[Key(5)]
public ushort HomeWorld { get; set; }
[Key(6)]
public string HomeWorldName { get; set; }
[Key(7)]
public ushort Territory { get; set; }
[Key(8)]
public string TerritoryName { get; set; }
[Key(9)]
public byte Job { get; set; }
[Key(10)]
public string JobName { get; set; }
[Key(11)]
public byte GrandCompany { get; set; }
[Key(12)]
public string GrandCompanyName { get; set; }
[Key(13)]
public byte Languages { get; set; }
[Key(14)]
public byte MainLanguage { get; set; }
}
public abstract class IEncodable {
protected abstract byte Code { get; }
protected abstract byte[] PayloadEncode();
public byte[] Encode() {
byte[] payload = this.PayloadEncode();
if (payload.Length == 0) {
return new byte[] { this.Code };
}
byte[] bytes = new byte[1 + payload.Length];
bytes[0] = this.Code;
Array.Copy(payload, 0, bytes, 1, payload.Length);
return bytes;
}
}
public enum ClientOperation : byte {
/// <summary>
/// The client is sending data to the server to keep the socket alive. Has no payload.
/// </summary>
Ping = 1,
/// <summary>
/// The client has a message to be sent in the game and is relaying it to the server.
/// </summary>
Message = 2,
/// <summary>
/// The client is shutting down. Clients should send this and close their socket for a clean shutdown.
/// </summary>
Shutdown = 3,
Backlog = 4,
CatchUp = 5,
PlayerList = 6,
LinkshellList = 7,
}
public class MillisecondsDateTimeFormatter : IMessagePackFormatter<DateTime> {
private static readonly DateTime Epoch = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
public DateTime Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options) {
var millis = reader.ReadInt64();
return Epoch.AddMilliseconds(millis);
}
public void Serialize(ref MessagePackWriter writer, DateTime value, MessagePackSerializerOptions options) {
var millis = (long)(value.ToUniversalTime() - Epoch).TotalMilliseconds;
writer.WriteInt64(millis);
}
}
}

View File

@ -0,0 +1,36 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("XIVChatCommon")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("XIVChatCommon")]
[assembly: AssemblyCopyright("Copyright © 2020")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("6f426eab-15d4-48ac-80d2-2d2df2ce5ee0")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

View File

@ -0,0 +1,56 @@
using Sodium;
using System;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace XIVChatCommon {
public static class SecretMessage {
private const uint MAX_MESSAGE_LEN = 128_000;
public async static Task<byte[]> ReadSecretMessage(Stream s, byte[] key, CancellationToken token = default) {
int read = 0;
byte[] header = new byte[4 + 24];
while (read < header.Length) {
read += await s.ReadAsync(header, read, header.Length - read, token);
}
uint length = BitConverter.ToUInt32(header, 0);
byte[] nonce = header.Skip(4).ToArray();
if (length > MAX_MESSAGE_LEN) {
throw new ArgumentOutOfRangeException($"Encrypted message specified a size of {length}, which is greater than the limit of {MAX_MESSAGE_LEN}");
}
byte[] ciphertext = new byte[length];
read = 0;
while (read < ciphertext.Length) {
read += await s.ReadAsync(ciphertext, read, ciphertext.Length - read, token);
}
return SecretBox.Open(ciphertext, nonce, key);
}
public async static Task SendSecretMessage(Stream s, byte[] key, byte[] message, CancellationToken token = default) {
byte[] nonce = SecretBox.GenerateNonce();
byte[] ciphertext = SecretBox.Create(message, nonce, key);
byte[] len = BitConverter.GetBytes((uint)ciphertext.Length);
if (ciphertext.Length > MAX_MESSAGE_LEN) {
throw new ArgumentOutOfRangeException($"Encrypted message would be {len} bytes long, which is larger than the limit of {MAX_MESSAGE_LEN}");
}
await s.WriteAsync(len, 0, len.Length, token);
await s.WriteAsync(nonce, 0, nonce.Length, token);
await s.WriteAsync(ciphertext, 0, ciphertext.Length, token);
}
public async static Task SendSecretMessage(Stream s, byte[] key, IEncodable message, CancellationToken token = default) {
await SendSecretMessage(s, key, message.Encode(), token);
}
public static int MacSize() => 16;
}
}

View File

@ -0,0 +1,88 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{6F426EAB-15D4-48AC-80D2-2D2DF2CE5EE0}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>XIVChatCommon</RootNamespace>
<AssemblyName>XIVChatCommon</AssemblyName>
<TargetFrameworkVersion>v4.8</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<Deterministic>true</Deterministic>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<LangVersion>8.0</LangVersion>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<LangVersion>8.0</LangVersion>
</PropertyGroup>
<ItemGroup>
<Reference Include="MessagePack, Version=2.1.0.0, Culture=neutral, PublicKeyToken=b4a0369545f0a1be, processorArchitecture=MSIL">
<HintPath>..\packages\MessagePack.2.1.194\lib\netstandard2.0\MessagePack.dll</HintPath>
</Reference>
<Reference Include="MessagePack.Annotations, Version=2.1.0.0, Culture=neutral, PublicKeyToken=b4a0369545f0a1be, processorArchitecture=MSIL">
<HintPath>..\packages\MessagePack.Annotations.2.1.194\lib\netstandard2.0\MessagePack.Annotations.dll</HintPath>
</Reference>
<Reference Include="Microsoft.Bcl.AsyncInterfaces, Version=1.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
<HintPath>..\packages\Microsoft.Bcl.AsyncInterfaces.1.0.0\lib\net461\Microsoft.Bcl.AsyncInterfaces.dll</HintPath>
</Reference>
<Reference Include="Sodium.Core, Version=1.2.3.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\Sodium.Core.1.2.3\lib\netstandard2.0\Sodium.Core.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.Buffers, Version=4.0.2.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
<HintPath>..\packages\System.Buffers.4.4.0\lib\netstandard2.0\System.Buffers.dll</HintPath>
</Reference>
<Reference Include="System.Core" />
<Reference Include="System.Memory, Version=4.0.1.1, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
<HintPath>..\packages\System.Memory.4.5.3\lib\netstandard2.0\System.Memory.dll</HintPath>
</Reference>
<Reference Include="System.Numerics" />
<Reference Include="System.Numerics.Vectors, Version=4.1.3.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>..\packages\System.Numerics.Vectors.4.4.0\lib\net46\System.Numerics.Vectors.dll</HintPath>
</Reference>
<Reference Include="System.Runtime.CompilerServices.Unsafe, Version=4.0.4.1, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>..\packages\System.Runtime.CompilerServices.Unsafe.4.5.2\lib\netstandard2.0\System.Runtime.CompilerServices.Unsafe.dll</HintPath>
</Reference>
<Reference Include="System.Threading.Tasks.Extensions, Version=4.2.0.1, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
<HintPath>..\packages\System.Threading.Tasks.Extensions.4.5.3\lib\netstandard2.0\System.Threading.Tasks.Extensions.dll</HintPath>
</Reference>
<Reference Include="System.ValueTuple, Version=4.0.3.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
<HintPath>..\packages\System.ValueTuple.4.5.0\lib\net47\System.ValueTuple.dll</HintPath>
</Reference>
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="KeyExchange.cs" />
<Compile Include="Message.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="SecretMessage.cs" />
<Compile Include="XivString.cs" />
</ItemGroup>
<ItemGroup>
<None Include="app.config" />
<None Include="packages.config" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>

216
XIVChatCommon/XivString.cs Normal file
View File

@ -0,0 +1,216 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
namespace XIVChatCommon {
public static class XivString {
private const byte START = 2;
private const byte END = 3;
public static List<Chunk> ToChunks(byte[] bytes) {
var chunks = new List<Chunk>();
var stringBytes = new List<byte>();
var italic = false;
uint? foreground = null;
uint? glow = null;
Action<bool> appendCurrent = (bool clear) => {
var text = Encoding.UTF8.GetString(stringBytes.ToArray());
chunks.Add(new TextChunk {
Foreground = foreground,
Glow = glow,
Italic = italic,
Content = text,
});
if (clear) {
stringBytes.Clear();
}
};
var reader = new BinaryReader(new MemoryStream(bytes));
while (reader.BaseStream.Position < reader.BaseStream.Length) {
var b = reader.ReadByte();
if (b == START) {
var kind = reader.ReadByte(); // kind
var len = GetInteger(reader); // data length
var data = new BinaryReader(new MemoryStream(reader.ReadBytes((int)len))); // data
var end = reader.ReadByte(); // end
if (end != END) {
throw new ArgumentException("Input was not a valid XivString");
}
switch (kind) {
// icon processing
case 0x12:
var spriteIndex = GetInteger(data);
chunks.Add(new IconChunk {
Index = (byte)spriteIndex,
});
break;
// italics processing
case 0x1a:
var newStatus = GetInteger(data) == 1;
var appendNow = (italic && !newStatus) || (!italic && newStatus);
if (!appendNow) {
break;
}
appendCurrent(true);
italic = newStatus;
break;
// foreground
case 0x48:
break;
// glow
case 0x49:
break;
}
continue;
}
stringBytes.Add(b);
}
return chunks;
}
public static string GetText(byte[] bytes) {
var stringBytes = new List<byte>();
var reader = new BinaryReader(new MemoryStream(bytes));
while (reader.BaseStream.Position < reader.BaseStream.Length) {
var b = reader.ReadByte();
if (b == START) {
reader.ReadByte(); // kind
var len = GetInteger(reader); // data length
reader.ReadBytes((int)len); // data
var end = reader.ReadByte(); // end
if (end != END) {
throw new ArgumentException("Input was not a valid XivString");
}
continue;
}
stringBytes.Add(b);
}
return Encoding.UTF8.GetString(stringBytes.ToArray());
}
// Thanks, Dalamud
protected enum IntegerType {
// used as an internal marker; sometimes single bytes are bare with no marker at all
None = 0,
Byte = 0xF0,
ByteTimes256 = 0xF1,
Int16 = 0xF2,
ByteSHL16 = 0xF3,
Int16Packed = 0xF4, // seen in map links, seemingly 2 8-bit values packed into 2 bytes with only one marker
Int16SHL8 = 0xF5,
Int24Special = 0xF6, // unsure how different form Int24 - used for hq items that add 1 million, also used for normal 24-bit values in map links
Int8SHL24 = 0xF7,
Int8SHL8Int8 = 0xF8,
Int8SHL8Int8SHL8 = 0xF9,
Int24 = 0xFA,
Int16SHL16 = 0xFB,
Int24Packed = 0xFC, // used in map links- sometimes short+byte, sometimes... not??
Int16Int8SHL8 = 0xFD,
Int32 = 0xFE
}
private static uint GetInteger(BinaryReader input) {
var t = input.ReadByte();
var type = (IntegerType)t;
return GetInteger(input, type);
}
private static uint GetInteger(BinaryReader input, IntegerType type) {
const byte ByteLengthCutoff = 0xF0;
var t = (byte)type;
if (t < ByteLengthCutoff) {
return (uint)(t - 1);
}
switch (type) {
case IntegerType.Byte:
return input.ReadByte();
case IntegerType.ByteTimes256:
return input.ReadByte() * (uint)256;
case IntegerType.ByteSHL16:
return (uint)(input.ReadByte() << 16);
case IntegerType.Int8SHL24:
return (uint)(input.ReadByte() << 24);
case IntegerType.Int8SHL8Int8: {
var v = 0;
v |= input.ReadByte() << 24;
v |= input.ReadByte();
return (uint)v;
}
case IntegerType.Int8SHL8Int8SHL8: {
var v = 0;
v |= input.ReadByte() << 24;
v |= input.ReadByte() << 8;
return (uint)v;
}
case IntegerType.Int16:
// fallthrough - same logic
case IntegerType.Int16Packed: {
var v = 0;
v |= input.ReadByte() << 8;
v |= input.ReadByte();
return (uint)v;
}
case IntegerType.Int16SHL8: {
var v = 0;
v |= input.ReadByte() << 16;
v |= input.ReadByte() << 8;
return (uint)v;
}
case IntegerType.Int16SHL16: {
var v = 0;
v |= input.ReadByte() << 24;
v |= input.ReadByte() << 16;
return (uint)v;
}
case IntegerType.Int24Special:
// Fallthrough - same logic
case IntegerType.Int24Packed:
// fallthrough again
case IntegerType.Int24: {
var v = 0;
v |= input.ReadByte() << 16;
v |= input.ReadByte() << 8;
v |= input.ReadByte();
return (uint)v;
}
case IntegerType.Int16Int8SHL8: {
var v = 0;
v |= input.ReadByte() << 24;
v |= input.ReadByte() << 16;
v |= input.ReadByte() << 8;
return (uint)v;
}
case IntegerType.Int32: {
var v = 0;
v |= input.ReadByte() << 24;
v |= input.ReadByte() << 16;
v |= input.ReadByte() << 8;
v |= input.ReadByte();
return (uint)v;
}
default:
throw new NotSupportedException();
}
}
}
}

19
XIVChatCommon/app.config Normal file
View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Runtime.CompilerServices.Unsafe" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-4.0.6.0" newVersion="4.0.6.0" />
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="Numbers" publicKeyToken="9cd62db60ea5554c" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-1.7.4.0" newVersion="1.7.4.0" />
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="System.Threading.Tasks.Extensions" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-4.2.0.1" newVersion="4.2.0.1" />
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="libsodium" version="1.0.18" targetFramework="net48" />
<package id="MessagePack" version="2.1.194" targetFramework="net48" />
<package id="MessagePack.Annotations" version="2.1.194" targetFramework="net48" />
<package id="Microsoft.Bcl.AsyncInterfaces" version="1.0.0" targetFramework="net48" />
<package id="Microsoft.NETCore.Platforms" version="3.1.3" targetFramework="net48" />
<package id="Sodium.Core" version="1.2.3" targetFramework="net48" />
<package id="System.Buffers" version="4.4.0" targetFramework="net48" />
<package id="System.Memory" version="4.5.3" targetFramework="net48" />
<package id="System.Numerics.Vectors" version="4.4.0" targetFramework="net48" />
<package id="System.Reflection.Emit" version="4.6.0" targetFramework="net48" />
<package id="System.Reflection.Emit.Lightweight" version="4.6.0" targetFramework="net48" />
<package id="System.Runtime.CompilerServices.Unsafe" version="4.5.2" targetFramework="net48" />
<package id="System.Threading.Tasks.Extensions" version="4.5.3" targetFramework="net48" />
<package id="System.ValueTuple" version="4.5.0" targetFramework="net48" />
</packages>

View File

@ -0,0 +1,29 @@
using Dalamud.Configuration;
using Sodium;
using System;
using System.Collections.Generic;
namespace XIVChatPlugin {
public class Configuration : IPluginConfiguration {
private Plugin plugin;
public int Version { get; set; } = 1;
public ushort Port { get; set; } = 14777;
public bool BacklogEnabled { get; set; } = true;
public ushort BacklogCount { get; set; } = 100;
public bool SendBattle { get; set; } = true;
public Dictionary<Guid, Tuple<string, byte[]>> TrustedKeys { get; set; } = new Dictionary<Guid, Tuple<string, byte[]>>();
public KeyPair KeyPair { get; set; } = null;
public void Initialise(Plugin plugin) {
this.plugin = plugin ?? throw new ArgumentNullException(nameof(plugin), "Plugin cannot be null");
}
public void Save() {
this.plugin.Interface.SavePluginConfig(this);
}
}
}

View File

@ -0,0 +1,65 @@
using Dalamud.Plugin;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
namespace XIVChatPlugin {
public static class Extensions {
public static string ToHexString(this byte[] bytes, bool upper = false, string separator = "") {
return string.Join(separator, bytes.Select(b => b.ToString(upper ? "X2" : "x2")));
}
//public static List<uint> ToColours(this byte[] bytes) {
// var colours = new List<uint>();
// uint colour = 0xFF;
// for (int i = 0; i < bytes.Length; i++) {
// var idx = i % 3;
// if (i != 0 && idx == 0) {
// colours.Add(colour);
// colour = 0xFF;
// }
// colour |= (uint)bytes[i] << ((4 - idx - 1) * 8);
// }
// colours.Add(colour);
// return colours;
//}
public static List<Vector4> ToColours(this byte[] bytes) {
var colours = new List<Vector4>();
var colour = new Vector4(0f, 0f, 0f, 1f);
for (int i = 0; i < bytes.Length; i++) {
var idx = i % 3;
if (i != 0 && idx == 0) {
colours.Add(colour);
colour = new Vector4(0f, 0f, 0f, 1f);
}
switch (idx) {
case 0:
colour.X = bytes[i] / 255f;
break;
case 1:
colour.Y = bytes[i] / 255f;
break;
case 2:
colour.Z = bytes[i] / 255f;
break;
default:
throw new ApplicationException("unreachable code reached");
}
}
colours.Add(colour);
return colours;
}
}
}

View File

@ -0,0 +1,230 @@
using Dalamud.Hooking;
using Lumina.Excel.GeneratedSheets;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using XIVChatCommon;
namespace XIVChatPlugin {
public class GameFunctions : IDisposable {
private readonly Plugin plugin;
private delegate IntPtr GetUIBaseDelegate();
private delegate IntPtr GetUIModuleDelegate(IntPtr basePtr);
private delegate void EasierProcessChatBoxDelegate(IntPtr uiModule, IntPtr message, IntPtr unused, byte a4);
private delegate byte RequestFriendListDelegate(IntPtr manager);
private delegate int FormatFriendListNameDelegate(long a1, long a2, long a3, int a4, IntPtr data, long a6);
private delegate IntPtr OnReceiveFriendListChunkDelegate(IntPtr a1, IntPtr data);
private readonly Hook<RequestFriendListDelegate> friendListHook;
private readonly Hook<FormatFriendListNameDelegate> formatHook;
private readonly Hook<OnReceiveFriendListChunkDelegate> receiveChunkHook;
private readonly GetUIModuleDelegate GetUIModule;
private readonly EasierProcessChatBoxDelegate _EasierProcessChatBox;
private readonly IntPtr uiModulePtr;
private IntPtr friendListManager = IntPtr.Zero;
private bool requestingFriendList = false;
public bool RequestingFriendList => this.requestingFriendList;
private readonly List<Player> friends = new List<Player>();
public delegate void ReceiveFriendListHandler(List<Player> friends);
public event ReceiveFriendListHandler ReceiveFriendList;
public GameFunctions(Plugin plugin) {
this.plugin = plugin ?? throw new ArgumentNullException(nameof(plugin), "Plugin cannot be null");
var getUIModulePtr = this.plugin.Interface.TargetModuleScanner.ScanText("E8 ?? ?? ?? ?? 48 83 7F ?? 00 48 8B F0");
var easierProcessChatBoxPtr = this.plugin.Interface.TargetModuleScanner.ScanText("48 89 5C 24 ?? 57 48 83 EC 20 48 8B FA 48 8B D9 45 84 C9");
var friendListPtr = this.plugin.Interface.TargetModuleScanner.ScanText("40 53 48 81 EC 80 0F 00 00 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 84 24 ?? ?? ?? ?? 48 8B D9 48 8B 0D ?? ?? ?? ?? E8 ?? ?? ?? ?? 48 85 C0 0F 84 ?? ?? ?? ?? 44 0F B6 43 ?? 33 C9");
var formatPtr = this.plugin.Interface.TargetModuleScanner.ScanText("48 89 5C 24 ?? 48 89 6C 24 ?? 48 89 74 24 ?? 41 56 48 83 EC 30 48 8B 6C 24 ??");
var recvChunkPtr = this.plugin.Interface.TargetModuleScanner.ScanText("48 89 5C 24 ?? 56 48 83 EC 20 48 8B 0D ?? ?? ?? ?? 48 8B F2");
this.uiModulePtr = this.plugin.Interface.TargetModuleScanner.GetStaticAddressFromSig("48 8B 0D ?? ?? ?? ?? 48 8D 54 24 ?? 48 83 C1 10 E8 ?? ?? ?? ??");
this.GetUIModule = Marshal.GetDelegateForFunctionPointer<GetUIModuleDelegate>(getUIModulePtr);
this._EasierProcessChatBox = Marshal.GetDelegateForFunctionPointer<EasierProcessChatBoxDelegate>(easierProcessChatBoxPtr);
this.friendListHook = new Hook<RequestFriendListDelegate>(friendListPtr, new RequestFriendListDelegate(this.OnRequestFriendList));
this.formatHook = new Hook<FormatFriendListNameDelegate>(formatPtr, new FormatFriendListNameDelegate(this.OnFormatFriendList));
this.receiveChunkHook = new Hook<OnReceiveFriendListChunkDelegate>(recvChunkPtr, new OnReceiveFriendListChunkDelegate(this.OnReceiveFriendList));
this.friendListHook.Enable();
this.formatHook.Enable();
this.receiveChunkHook.Enable();
}
public void ProcessChatBox(string message) {
IntPtr uiModule = this.GetUIModule(Marshal.ReadIntPtr(this.uiModulePtr));
if (uiModule == IntPtr.Zero) {
throw new ApplicationException("uiModule was null");
}
using (var payload = new ChatPayload(message)) {
IntPtr mem1 = Marshal.AllocHGlobal(400);
Marshal.StructureToPtr(payload, mem1, false);
this._EasierProcessChatBox(uiModule, mem1, IntPtr.Zero, 0);
Marshal.FreeHGlobal(mem1);
}
}
public bool RequestFriendList() {
if (this.friendListManager == IntPtr.Zero || this.friendListHook == null) {
return false;
}
this.requestingFriendList = true;
this.friendListHook.Original(this.friendListManager);
return true;
}
private byte OnRequestFriendList(IntPtr manager) {
this.friendListManager = manager;
return this.friendListHook.Original(manager);
}
private int OnFormatFriendList(long a1, long a2, long a3, int a4, IntPtr data, long a6) {
// have to call this first to populate cross-world info
var ret = this.formatHook.Original(a1, a2, a3, a4, data, a6);
if (!this.RequestingFriendList) {
return ret;
}
var entry = Marshal.PtrToStructure<FriendListEntryRaw>(data);
string jobName = null;
if (entry.job > 0) {
jobName = this.plugin.Interface.Data.GetExcelSheet<ClassJob>().GetRow(entry.job)?.Name;
}
var player = new Player {
Name = entry.Name(),
FreeCompany = entry.FreeCompany(),
Status = entry.flags,
CurrentWorld = entry.currentWorldId,
CurrentWorldName = this.plugin.Interface.Data.GetExcelSheet<World>().GetRow(entry.currentWorldId)?.Name,
HomeWorld = entry.homeWorldId,
HomeWorldName = this.plugin.Interface.Data.GetExcelSheet<World>().GetRow(entry.homeWorldId)?.Name,
Territory = entry.territoryId,
TerritoryName = this.plugin.Interface.Data.GetExcelSheet<TerritoryType>().GetRow(entry.territoryId)?.PlaceName?.Value?.Name,
Job = entry.job,
JobName = jobName,
GrandCompany = entry.grandCompany,
GrandCompanyName = this.plugin.Interface.Data.GetExcelSheet<GrandCompany>().GetRow(entry.grandCompany)?.Name,
Languages = entry.langsEnabled,
MainLanguage = entry.mainLanguage,
};
this.friends.Add(player);
return ret;
}
private IntPtr OnReceiveFriendList(IntPtr a1, IntPtr data) {
var ret = this.receiveChunkHook.Original(a1, data);
// + 0xc
// 1 = party
// 2 = friends
// 3 = linkshell
// doesn't run (though same memory gets updated) for cwl or blacklist
// + 0x8 is current number of results returned or 0 when end of list
if (!this.RequestingFriendList) {
goto Return;
}
if (Marshal.ReadByte(data + 0xc) != 2 || Marshal.ReadInt16(data + 0x8) != 0) {
goto Return;
}
this.ReceiveFriendList(this.friends);
this.friends.Clear();
this.requestingFriendList = false;
Return:
return ret;
}
public void Dispose() {
this.friendListHook?.Dispose();
this.formatHook?.Dispose();
this.receiveChunkHook?.Dispose();
}
}
[StructLayout(LayoutKind.Explicit)]
struct ChatPayload : IDisposable {
[FieldOffset(0)]
readonly IntPtr textPtr;
[FieldOffset(16)]
readonly ulong textLen;
[FieldOffset(8)]
readonly ulong unk1;
[FieldOffset(24)]
readonly ulong unk2;
internal ChatPayload(string text) {
byte[] stringBytes = Encoding.UTF8.GetBytes(text);
this.textPtr = Marshal.AllocHGlobal(stringBytes.Length + 30);
Marshal.Copy(stringBytes, 0, this.textPtr, stringBytes.Length);
Marshal.WriteByte(this.textPtr + stringBytes.Length, 0);
this.textLen = (ulong)(stringBytes.Length + 1);
this.unk1 = 64;
this.unk2 = 0;
}
public void Dispose() {
Marshal.FreeHGlobal(this.textPtr);
}
}
[StructLayout(LayoutKind.Sequential)]
struct FriendListEntryRaw {
readonly ulong unk1;
internal ulong flags;
readonly uint unk2;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 2)]
readonly byte[] unk3;
internal readonly ushort currentWorldId;
internal readonly ushort homeWorldId;
internal readonly ushort territoryId;
internal readonly byte grandCompany;
internal readonly byte mainLanguage;
internal readonly byte langsEnabled;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 2)]
readonly byte[] unk4;
internal readonly byte job;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 32)]
readonly byte[] name;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 5)]
readonly byte[] fc;
private static string HandleString(byte[] bytes) {
byte[] nonNull = bytes.TakeWhile(b => b != 0).ToArray();
if (nonNull.Length == 0) {
return null;
}
return Encoding.UTF8.GetString(nonNull);
}
public string Name() => HandleString(this.name);
public string FreeCompany() => HandleString(this.fc);
}
}

View File

@ -0,0 +1,67 @@
<?xml version="1.0" encoding="utf-8" ?>
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<!-- -->
<!-- ILMerge project-specific settings. Almost never need to be set explicitly. -->
<!-- for details, see http://research.microsoft.com/en-us/people/mbarnett/ilmerge.aspx -->
<!-- -->
<!-- *** set this file to Type=None, CopyToOutput=Never *** -->
<!-- If True, all copy local dependencies will also be merged from referenced projects whether they are referenced in the current project explicitly or not -->
<ILMergeTransitive>false</ILMergeTransitive>
<!-- Extra ILMerge library paths (semicolon-separated). Dont put your package dependencies here, they will be added automagically -->
<ILMergeLibraryPath></ILMergeLibraryPath>
<!-- The solution NuGet package directory if not standard 'SOLUTION\packages' -->
<ILMergePackagesPath></ILMergePackagesPath>
<!-- The merge order file name if differs from standard 'ILMergeOrder.txt' -->
<ILMergeOrderFile></ILMergeOrderFile>
<!-- The strong key file name if not specified in the project -->
<ILMergeKeyFile></ILMergeKeyFile>
<!-- The assembly version if differs for the version of the main assembly -->
<ILMergeAssemblyVersion></ILMergeAssemblyVersion>
<!-- added in Version 1.0.4 -->
<ILMergeFileAlignment></ILMergeFileAlignment>
<!-- added in Version 1.0.4, default=none -->
<ILMergeAllowDuplicateType></ILMergeAllowDuplicateType>
<!-- If the <see cref="CopyAttributes"/> is also set, any assembly-level attributes names that have the same type are copied over into the target assembly -->
<ILMergeAllowMultipleAssemblyLevelAttributes></ILMergeAllowMultipleAssemblyLevelAttributes>
<!-- See ILMerge documentation -->
<ILMergeAllowZeroPeKind></ILMergeAllowZeroPeKind>
<!-- The assembly level attributes of each input assembly are copied over into the target assembly -->
<ILMergeCopyAttributes></ILMergeCopyAttributes>
<!-- Creates a .pdb file for the output assembly and merges into it any .pdb files found for input assemblies, default=true -->
<ILMergeDebugInfo></ILMergeDebugInfo>
<!-- Target assembly will be delay signed -->
<ILMergeDelaySign></ILMergeDelaySign>
<!-- Types in assemblies other than the primary assembly have their visibility modified -->
<ILMergeInternalize></ILMergeInternalize>
<!-- The path name of the file that will be used to identify types that are not to have their visibility modified -->
<ILMergeInternalizeExcludeFile></ILMergeInternalizeExcludeFile>
<!-- XML documentation files are merged to produce an XML documentation file for the target assembly -->
<ILMergeXmlDocumentation></ILMergeXmlDocumentation>
<!-- External assembly references in the manifest of the target assembly will use full public keys (false) or public key tokens (true, default value) -->
<ILMergePublicKeyTokens></ILMergePublicKeyTokens>
<!-- Types with the same name are all merged into a single type in the target assembly -->
<ILMergeUnionMerge></ILMergeUnionMerge>
<!-- The version of the target framework, default 40 (works for 45 too) -->
<ILTargetPlatform></ILTargetPlatform>
</PropertyGroup>
</Project>

View File

@ -0,0 +1,4 @@
# this file contains the partial list of the merged assemblies in the merge order
# you can fill it from the obj\CONFIG\PROJECT.ilmerge generated on every build
# and finetune merge order to your satisfaction

106
XIVChatPlugin/Plugin.cs Normal file
View File

@ -0,0 +1,106 @@
using Dalamud.Game.Command;
using Dalamud.Hooking;
using Dalamud.Plugin;
using System;
using System.IO;
using System.Reflection;
namespace XIVChatPlugin {
public class Plugin : IDalamudPlugin {
private bool disposedValue;
public string Name => "XIVChat";
internal string Location { get; private set; } = Assembly.GetExecutingAssembly().Location;
[System.Diagnostics.CodeAnalysis.SuppressMessage("CodeQuality", "IDE0051:Remove unused private members", Justification = "LivePluginLoader")]
private void SetLocation(string path) {
this.Location = path;
}
public DalamudPluginInterface Interface { get; private set; }
public Configuration Config { get; private set; }
public PluginUI Ui { get; private set; }
public Server Server { get; private set; }
public GameFunctions Functions { get; private set; }
private delegate byte ChatChannelChangeDelegate(IntPtr a1, uint channel);
private Hook<ChatChannelChangeDelegate> chatChannelChangeHook;
public void Initialize(DalamudPluginInterface pluginInterface) {
this.Interface = pluginInterface ?? throw new ArgumentNullException(nameof(pluginInterface), "DalamudPluginInterface cannot be null");
// load libsodium.so from debug location if in debug mode
#if DEBUG
string path = Environment.GetEnvironmentVariable("PATH");
string newPath = Path.GetDirectoryName(this.Location);
Environment.SetEnvironmentVariable("PATH", $"{path};{newPath}");
#endif
this.Config = (Configuration)this.Interface.GetPluginConfig() ?? new Configuration();
this.Config.Initialise(this);
this.Functions = new GameFunctions(this);
var funcPtr = this.Interface.TargetModuleScanner.ScanText("40 55 48 8D 6C 24 ?? 48 81 EC A0 00 00 00 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 45 ?? 48 8B 0D ?? ?? ?? ?? 33 C0 48 83 C1 10 89 45 ?? C7 45 ?? 01 00 00 00");
if (funcPtr == IntPtr.Zero) {
PluginLog.LogError("Could not sig chat channel change function");
} else {
this.chatChannelChangeHook = new Hook<ChatChannelChangeDelegate>(funcPtr, new ChatChannelChangeDelegate(this.ChangeChatChannelDetour));
this.chatChannelChangeHook.Enable();
}
this.Ui = new PluginUI(this);
this.Server = new Server(this);
this.Server.Spawn();
this.Interface.UiBuilder.OnBuildUi += this.Ui.Draw;
this.Interface.UiBuilder.OnOpenConfigUi += this.Ui.OpenSettings;
this.Interface.Framework.OnUpdateEvent += this.Server.OnFrameworkUpdate;
this.Interface.Framework.Gui.Chat.OnChatMessage += this.Server.OnChat;
this.Interface.ClientState.OnLogin += this.Server.OnLogIn;
this.Interface.ClientState.OnLogout += this.Server.OnLogOut;
this.Interface.ClientState.TerritoryChanged += this.Server.OnTerritoryChange;
this.Interface.CommandManager.AddHandler("/xivchat", new CommandInfo(this.OnCommand) {
HelpMessage = "Opens the config for the XIVChat plugin",
});
}
private byte ChangeChatChannelDetour(IntPtr a1, uint channel) {
// a1 + 0xfd0 is the chat channel byte (including for when clicking on shout)
this.Server.OnChatChannelChange(channel);
return this.chatChannelChangeHook.Original(a1, channel);
}
private void OnCommand(string command, string args) {
this.Ui.OpenSettings(null, null);
}
protected virtual void Dispose(bool disposing) {
if (!this.disposedValue) {
if (disposing) {
this.Server.Dispose();
this.Interface.UiBuilder.OnBuildUi -= this.Ui.Draw;
this.Interface.UiBuilder.OnOpenConfigUi -= this.Ui.OpenSettings;
this.Interface.Framework.OnUpdateEvent -= this.Server.OnFrameworkUpdate;
this.Interface.Framework.Gui.Chat.OnChatMessage -= this.Server.OnChat;
this.Interface.ClientState.OnLogin -= this.Server.OnLogIn;
this.Interface.ClientState.OnLogout -= this.Server.OnLogOut;
this.Interface.ClientState.TerritoryChanged -= this.Server.OnTerritoryChange;
this.Interface.CommandManager.RemoveHandler("/xivchat");
this.chatChannelChangeHook?.Dispose();
}
this.disposedValue = true;
}
}
public void Dispose() {
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
this.Dispose(disposing: true);
GC.SuppressFinalize(this);
}
}
}

345
XIVChatPlugin/PluginUI.cs Normal file
View File

@ -0,0 +1,345 @@
using ImGuiNET;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Numerics;
using System.Threading.Channels;
namespace XIVChatPlugin {
public class PluginUI {
private readonly Plugin plugin;
private bool _showSettings = false;
public bool ShowSettings { get => this._showSettings; set => this._showSettings = value; }
private readonly Dictionary<Guid, Tuple<Client, Channel<bool>>> pending = new Dictionary<Guid, Tuple<Client, Channel<bool>>>();
private readonly Dictionary<Guid, string> pendingNames = new Dictionary<Guid, string>(0);
public PluginUI(Plugin plugin) {
this.plugin = plugin ?? throw new ArgumentNullException(nameof(plugin), "Plugin cannot be null");
}
private static class Colours {
public static readonly Vector4 primary = new Vector4(2 / 255f, 204 / 255f, 238 / 255f, 1.0f);
public static readonly Vector4 primaryDark = new Vector4(2 / 255f, 180 / 255f, 211 / 255f, 1.0f);
public static readonly Vector4 background = new Vector4(46 / 255f, 46 / 255f, 46 / 255f, 1.0f);
public static readonly Vector4 text = new Vector4(190 / 255f, 190 / 255f, 190 / 255f, 1.0f);
public static readonly Vector4 button = new Vector4(90 / 255f, 89 / 255f, 90 / 255f, 1.0f);
public static readonly Vector4 buttonActive = new Vector4(123 / 255f, 122 / 255f, 124 / 255f, 1.0f);
public static readonly Vector4 buttonHovered = new Vector4(108 / 255f, 107 / 255f, 109 / 255f, 1.0f);
public static readonly Vector4 white = new Vector4(1f, 1f, 1f, 1f);
}
public void Draw() {
ImGui.PushStyleColor(ImGuiCol.TitleBg, Colours.primaryDark);
ImGui.PushStyleColor(ImGuiCol.TitleBgActive, Colours.primary);
ImGui.PushStyleColor(ImGuiCol.TitleBgCollapsed, Colours.primaryDark);
ImGui.PushStyleColor(ImGuiCol.WindowBg, Colours.background);
ImGui.PushStyleColor(ImGuiCol.Text, Colours.text);
ImGui.PushStyleColor(ImGuiCol.Button, Colours.button);
ImGui.PushStyleColor(ImGuiCol.ButtonActive, Colours.buttonActive);
ImGui.PushStyleColor(ImGuiCol.ButtonHovered, Colours.buttonHovered);
this.DrawInner();
ImGui.PopStyleColor(8);
}
private static V WithWhiteText<V>(Func<V> func) {
ImGui.PushStyleColor(ImGuiCol.Text, Colours.white);
var ret = func();
ImGui.PopStyleColor();
return ret;
}
private static void WithWhiteText(Action func) {
ImGui.PushStyleColor(ImGuiCol.Text, Colours.white);
func();
ImGui.PopStyleColor();
}
private static bool Begin(string name, ImGuiWindowFlags flags) {
return WithWhiteText(() => ImGui.Begin(name, flags));
}
private static bool Begin(string name, ref bool showSettings, ImGuiWindowFlags flags) {
ImGui.PushStyleColor(ImGuiCol.Text, Colours.white);
var result = ImGui.Begin(name, ref showSettings, flags);
ImGui.PopStyleColor();
return result;
}
private static void TextWhite(string text) => WithWhiteText(() => ImGui.TextUnformatted(text));
private void DrawInner() {
this.AcceptPending();
foreach (var item in this.pending.ToList()) {
if (this.DrawPending(item.Key, item.Value.Item1, item.Value.Item2)) {
this.pending.Remove(item.Key);
}
}
if (!this.ShowSettings || !Begin(this.plugin.Name, ref this._showSettings, ImGuiWindowFlags.AlwaysAutoResize)) {
return;
}
if (WithWhiteText(() => ImGui.CollapsingHeader("Server public key"))) {
string serverPublic = this.plugin.Config.KeyPair.PublicKey.ToHexString(upper: true);
ImGui.TextUnformatted(serverPublic);
this.DrawColours(this.plugin.Config.KeyPair.PublicKey, serverPublic);
if (WithWhiteText(() => ImGui.Button("Regenerate"))) {
this.plugin.Server.RegenerateKeyPair();
}
}
if (WithWhiteText(() => ImGui.CollapsingHeader("Settings", ImGuiTreeNodeFlags.DefaultOpen))) {
TextWhite("Port");
int port = this.plugin.Config.Port;
if (WithWhiteText(() => ImGui.InputInt("##port", ref port))) {
ushort realPort = (ushort)Math.Min(ushort.MaxValue, Math.Max(1, port));
this.plugin.Config.Port = realPort;
this.plugin.Config.Save();
}
ImGui.Spacing();
bool backlogEnabled = this.plugin.Config.BacklogEnabled;
if (WithWhiteText(() => ImGui.Checkbox("Enable backlog", ref backlogEnabled))) {
this.plugin.Config.BacklogEnabled = backlogEnabled;
this.plugin.Config.Save();
}
int backlogCount = this.plugin.Config.BacklogCount;
if (WithWhiteText(() => ImGui.DragInt("Backlog messages", ref backlogCount, 1f, 0, ushort.MaxValue))) {
this.plugin.Config.BacklogCount = (ushort)Math.Max(0, Math.Min(ushort.MaxValue, backlogCount));
this.plugin.Config.Save();
}
ImGui.Spacing();
bool sendBattle = this.plugin.Config.SendBattle;
if (WithWhiteText(() => ImGui.Checkbox("Send battle messages", ref sendBattle))) {
this.plugin.Config.SendBattle = sendBattle;
this.plugin.Config.Save();
}
ImGui.TextUnformatted("Changing this setting will not affect messages already in the backlog.");
}
if (WithWhiteText(() => ImGui.CollapsingHeader("Trusted keys"))) {
if (this.plugin.Config.TrustedKeys.Count == 0) {
ImGui.TextUnformatted("None");
}
ImGui.Columns(2);
var maxKeyLength = 0f;
foreach (var entry in this.plugin.Config.TrustedKeys.ToList()) {
var name = entry.Value.Item1;
var key = entry.Value.Item2;
var hex = key.ToHexString(upper: true);
maxKeyLength = Math.Max(maxKeyLength, ImGui.CalcTextSize(name).X);
ImGui.TextUnformatted(name);
if (ImGui.IsItemHovered()) {
ImGui.BeginTooltip();
ImGui.TextUnformatted(hex);
this.DrawColours(key, hex);
ImGui.EndTooltip();
}
ImGui.NextColumn();
if (WithWhiteText(() => ImGui.Button($"Untrust##{entry.Key}"))) {
this.plugin.Config.TrustedKeys.Remove(entry.Key);
this.plugin.Config.Save();
}
ImGui.NextColumn();
}
ImGui.SetColumnWidth(0, maxKeyLength + ImGui.GetStyle().ItemSpacing.X * 2);
ImGui.Columns(1);
}
if (WithWhiteText(() => ImGui.CollapsingHeader("Connected clients"))) {
if (this.plugin.Server.Clients.Count == 0) {
ImGui.TextUnformatted("None");
} else {
ImGui.Columns(3);
TextWhite("IP");
ImGui.NextColumn();
TextWhite("Key");
ImGui.NextColumn();
ImGui.NextColumn();
foreach (var client in this.plugin.Server.Clients) {
EndPoint remote;
try {
remote = client.Value.Conn.Client.RemoteEndPoint;
} catch (ObjectDisposedException) {
continue;
}
string ipAddress;
if (remote is IPEndPoint ip) {
ipAddress = ip.Address.ToString();
} else {
ipAddress = "Unknown";
}
ImGui.TextUnformatted(ipAddress);
ImGui.NextColumn();
var trustedKey = this.plugin.Config.TrustedKeys.Values.FirstOrDefault(entry => entry.Item2.SequenceEqual(client.Value.Handshake.RemotePublicKey));
if (trustedKey != default(Tuple<string, byte[]>)) {
ImGui.TextUnformatted(trustedKey.Item1);
if (ImGui.IsItemHovered()) {
ImGui.BeginTooltip();
var hex = trustedKey.Item2.ToHexString(upper: true);
ImGui.TextUnformatted(hex);
this.DrawColours(trustedKey.Item2, hex);
ImGui.EndTooltip();
}
}
ImGui.NextColumn();
if (WithWhiteText(() => ImGui.Button($"Disconnect##{client.Key}"))) {
client.Value.Disconnect();
}
ImGui.NextColumn();
}
ImGui.Columns(1);
}
}
if (WithWhiteText(() => ImGui.CollapsingHeader("ACT/Teamcraft issues?"))) {
ImGui.PushTextWrapPos(ImGui.GetFontSize() * 20);
ImGui.TextUnformatted("Click on the button below to visit a website showing a workaround for ACT and Teamcraft having issues.");
ImGui.PopTextWrapPos();
if (WithWhiteText(() => ImGui.Button("Open website"))) {
System.Diagnostics.Process.Start("https://xiv.chat/server/workaround");
}
}
ImGui.End();
}
private void DrawColours(byte[] bytes, string widthOf) {
this.DrawColours(bytes, ImGui.CalcTextSize(widthOf).X);
}
private void DrawColours(byte[] bytes, float width = 0f) {
var pos = ImGui.GetCursorScreenPos();
var spacing = ImGui.GetStyle().ItemSpacing;
var colours = bytes.ToColours();
float sizeX = width == 0f ? 32f : width / colours.Count;
for (int i = 0; i < colours.Count; i++) {
var topLeft = new Vector2(
pos.X + (sizeX * i),
pos.Y + spacing.Y
);
var bottomRight = new Vector2(
pos.X + (sizeX * (i + 1)),
pos.Y + spacing.Y + 16
);
ImGui.GetWindowDrawList().AddRectFilled(
topLeft,
bottomRight,
ImGui.GetColorU32(colours[i])
);
}
// create a spacing for 32px and spacing
ImGui.Dummy(new Vector2(0, 16 + spacing.Y * 2));
}
public void OpenSettings(object sender, EventArgs args) {
this.ShowSettings = true;
}
private void AcceptPending() {
while (this.plugin.Server.pendingClients.Reader.TryRead(out var item)) {
this.pending[Guid.NewGuid()] = item;
}
}
private bool DrawPending(Guid id, Client client, Channel<bool> accepted) {
bool ret = false;
var clientPublic = client.Handshake.RemotePublicKey;
var clientPublicHex = clientPublic.ToHexString(upper: true);
var serverPublic = this.plugin.Config.KeyPair.PublicKey;
var serverPublicHex = serverPublic.ToHexString(upper: true);
var width = Math.Max(ImGui.CalcTextSize(clientPublicHex).X, ImGui.CalcTextSize(serverPublicHex).X) + (ImGui.GetStyle().WindowPadding.X * 2);
if (!Begin($"Incoming XIVChat connection##{clientPublic}", ImGuiWindowFlags.AlwaysAutoResize)) {
return ret;
}
ImGui.PushTextWrapPos(width);
ImGui.TextUnformatted("A client that has not previously connected is attempting to connect to XIVChat. If this is you, please check the two keys below and make sure that they match what is displayed by the client.");
ImGui.Separator();
TextWhite("Server");
ImGui.TextUnformatted(serverPublicHex);
this.DrawColours(serverPublic, serverPublicHex);
ImGui.Spacing();
TextWhite("Client");
ImGui.TextUnformatted(clientPublicHex);
this.DrawColours(clientPublic, clientPublicHex);
ImGui.Separator();
ImGui.TextUnformatted("Give this client a name to remember it more easily if you trust it.");
ImGui.PopTextWrapPos();
if (!this.pendingNames.TryGetValue(id, out string name)) {
name = "No name";
}
if (WithWhiteText(() => ImGui.InputText("Client name", ref name, 100, ImGuiInputTextFlags.AutoSelectAll))) {
this.pendingNames[id] = name;
}
ImGui.Separator();
ImGui.TextUnformatted("Do both keys match?");
if (WithWhiteText(() => ImGui.Button("Yes"))) {
accepted.Writer.TryWrite(true);
this.plugin.Config.TrustedKeys[Guid.NewGuid()] = Tuple.Create(name, client.Handshake.RemotePublicKey);
this.plugin.Config.Save();
this.pendingNames.Remove(id);
ret = true;
}
ImGui.SameLine();
if (WithWhiteText(() => ImGui.Button("No"))) {
accepted.Writer.TryWrite(false);
this.pendingNames.Remove(id);
ret = true;
}
ImGui.End();
return ret;
}
}
}

View File

@ -0,0 +1,36 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("XIVChatPlugin")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("XIVChatPlugin")]
[assembly: AssemblyCopyright("Copyright © 2020")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("9aeb2b77-c127-4ba8-b9ae-fb3ca1649fdb")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0")]
[assembly: AssemblyFileVersion("1.0.0")]

Binary file not shown.

769
XIVChatPlugin/Server.cs Normal file
View File

@ -0,0 +1,769 @@
using Dalamud.Game.Chat;
using Dalamud.Game.Chat.SeStringHandling;
using Dalamud.Game.Chat.SeStringHandling.Payloads;
using Dalamud.Game.Internal;
using Dalamud.Plugin;
using Lumina.Data;
using Lumina.Excel;
using Lumina.Excel.GeneratedSheets;
using MessagePack;
using Sodium;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
using XIVChatCommon;
namespace XIVChatPlugin {
public class Server : IDisposable {
private readonly Plugin plugin;
private readonly CancellationTokenSource tokenSource = new CancellationTokenSource();
private readonly ConcurrentQueue<string> toGame = new ConcurrentQueue<string>();
private readonly ConcurrentDictionary<Guid, Client> clients = new ConcurrentDictionary<Guid, Client>();
public IReadOnlyDictionary<Guid, Client> Clients => this.clients;
public readonly Channel<Tuple<Client, Channel<bool>>> pendingClients = Channel.CreateUnbounded<Tuple<Client, Channel<bool>>>();
private readonly HashSet<Guid> WaitingForFriendList = new HashSet<Guid>();
private readonly LinkedList<ServerMessage> Backlog = new LinkedList<ServerMessage>();
private TcpListener listener;
private bool sendPlayerData = false;
private volatile bool _running = false;
public bool Running { get => this._running; set => this._running = value; }
private InputChannel currentChannel = InputChannel.Say;
private const int MAX_MESSAGE_SIZE = 128_000;
public Server(Plugin plugin) {
this.plugin = plugin ?? throw new ArgumentNullException(nameof(plugin), "Plugin cannot be null");
if (this.plugin.Config.KeyPair == null) {
this.RegenerateKeyPair();
}
this.plugin.Functions.ReceiveFriendList += this.OnReceiveFriendList;
}
private async void OnReceiveFriendList(List<Player> friends) {
var msg = new ServerPlayerList {
Type = PlayerListType.Friend,
Players = friends.ToArray(),
};
foreach (var id in this.WaitingForFriendList) {
if (!this.Clients.TryGetValue(id, out var client)) {
continue;
}
try {
await SecretMessage.SendSecretMessage(client.Conn.GetStream(), client.Handshake.Keys.tx, msg, client.TokenSource.Token);
} catch (Exception ex) {
PluginLog.LogError($"Could not send message: {ex.Message}");
}
}
this.WaitingForFriendList.Clear();
}
public void Spawn() {
var ip = IPAddress.Parse("0.0.0.0");
var port = this.plugin.Config.Port;
Task.Run(async () => {
this.listener = new TcpListener(ip, port);
this.listener.Start();
this._running = true;
PluginLog.Log("Running...");
while (!this.tokenSource.IsCancellationRequested) {
var conn = await this.listener.GetTcpClient(this.tokenSource);
this.SpawnClientTask(conn);
}
this._running = false;
});
}
public void RegenerateKeyPair() {
this.plugin.Config.KeyPair = PublicKeyBox.GenerateKeyPair();
this.plugin.Config.Save();
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "delegate")]
public void OnChat(XivChatType type, uint senderId, ref SeString sender, ref SeString message, ref bool isHandled) {
if (isHandled) {
return;
}
var chatCode = new ChatCode((ushort)type);
if (!this.plugin.Config.SendBattle && chatCode.IsBattle()) {
return;
}
var chunks = new List<Chunk>();
if (sender.Payloads.Count > 0) {
// FIXME: can't get format straight from game until Lumina stops returning LogKind.Format as a string (it's an SeString)
// var format = this.FormatFor(chatCode.Type);
var format = chatCode.NameFormat();
if (format != null && format.IsPresent) {
chunks.Add(new TextChunk {
FallbackColour = chatCode.DefaultColour(),
Content = format.Before,
});
chunks.AddRange(ToChunks(sender, chatCode.DefaultColour()));
chunks.Add(new TextChunk {
FallbackColour = chatCode.DefaultColour(),
Content = format.After,
});
}
}
chunks.AddRange(ToChunks(message, chatCode.DefaultColour()));
var msg = new ServerMessage {
Timestamp = DateTime.UtcNow,
Channel = (ChatType)type,
Sender = sender.Encode(),
Content = message.Encode(),
Chunks = chunks,
};
this.Backlog.AddLast(msg);
while (this.Backlog.Count > this.plugin.Config.BacklogCount) {
this.Backlog.RemoveFirst();
}
foreach (var client in this.clients.Values) {
client.Queue.Writer.TryWrite(msg);
}
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "delegate")]
public void OnFrameworkUpdate(Framework framework) {
if (this.sendPlayerData && this.plugin.Interface.ClientState.LocalPlayer != null) {
this.BroadcastPlayerData();
this.sendPlayerData = false;
}
if (!this.toGame.TryDequeue(out var message)) {
return;
}
this.plugin.Functions.ProcessChatBox(message);
}
private void SpawnClientTask(TcpClient conn) {
if (conn == null) {
return;
}
Task.Run(async () => {
var stream = conn.GetStream();
var handshake = await KeyExchange.ServerHandshake(this.plugin.Config.KeyPair, stream);
var newClient = new Client(conn) {
Handshake = handshake,
};
// if this public key isn't trusted, prompt first
if (!this.plugin.Config.TrustedKeys.Values.Any(entry => entry.Item2.SequenceEqual(handshake.RemotePublicKey))) {
var accepted = Channel.CreateBounded<bool>(1);
await this.pendingClients.Writer.WriteAsync(Tuple.Create(newClient, accepted));
if (!await accepted.Reader.ReadAsync()) {
return;
}
}
var id = Guid.NewGuid();
newClient.Connected = true;
this.clients[id] = newClient;
// send availability
var available = this.plugin.Interface.ClientState.LocalPlayer != null;
try {
await SecretMessage.SendSecretMessage(stream, handshake.Keys.tx, new Availability(available));
} catch (Exception ex) {
PluginLog.LogError($"Could not send message: {ex.Message}");
}
// send player data
try {
await this.SendPlayerData(newClient);
} catch (Exception ex) {
PluginLog.LogError($"Could not send message: {ex.Message}");
}
// send current channel
try {
var channel = this.currentChannel;
await SecretMessage.SendSecretMessage(stream, handshake.Keys.tx, new ServerChannel(
channel,
this.LocalisedChannelName(channel)
));
} catch (Exception ex) {
PluginLog.LogError($"Could not send message: {ex.Message}");
}
var listen = Task.Run(async () => {
conn.ReceiveTimeout = 5_000;
while (this.clients.TryGetValue(id, out var client) && client.Connected && !client.TokenSource.IsCancellationRequested && conn.Connected) {
byte[] msg;
try {
msg = await SecretMessage.ReadSecretMessage(stream, handshake.Keys.rx, client.TokenSource.Token);
} catch (SocketException ex) when (ex.SocketErrorCode == SocketError.TimedOut) {
continue;
} catch (Exception ex) {
PluginLog.LogError($"Could not read message: {ex.Message}");
continue;
}
var op = (ClientOperation)msg[0];
var payload = new byte[msg.Length - 1];
Array.Copy(msg, 1, payload, 0, payload.Length);
switch (op) {
case ClientOperation.Ping:
try {
await SecretMessage.SendSecretMessage(stream, handshake.Keys.tx, Pong.Instance, client.TokenSource.Token);
} catch (Exception ex) {
PluginLog.LogError($"Could not send message: {ex.Message}");
}
break;
case ClientOperation.Message:
var clientMessage = ClientMessage.Decode(payload);
foreach (var part in Wrap(clientMessage.Content)) {
this.toGame.Enqueue(part);
}
break;
case ClientOperation.Shutdown:
client.Disconnect();
break;
case ClientOperation.Backlog:
var backlog = ClientBacklog.Decode(payload);
var backlogMessages = new List<ServerMessage>();
var node = this.Backlog.Last;
while (node != null) {
if (backlogMessages.Count >= backlog.Amount) {
break;
}
backlogMessages.Add(node.Value);
node = node.Previous;
}
backlogMessages.Reverse();
await this.SendBacklogs(backlogMessages.ToArray(), stream, client);
break;
case ClientOperation.CatchUp:
var catchUp = ClientCatchUp.Decode(payload);
// I'm not sure why this needs to be done, but apparently it does
var after = catchUp.After.AddMilliseconds(1);
var msgs = this.MessagesAfter(after);
await this.SendBacklogs(msgs, stream, client);
break;
case ClientOperation.PlayerList:
var playerList = ClientPlayerList.Decode(payload);
if (playerList.Type == PlayerListType.Friend) {
this.WaitingForFriendList.Add(id);
if (!this.plugin.Functions.RequestingFriendList && !this.plugin.Functions.RequestFriendList()) {
this.plugin.Interface.Framework.Gui.Chat.PrintError($"[{this.plugin.Name}] Please open your friend list to enable friend list support. You should only need to do this on initial install or after updates.");
}
}
break;
}
}
});
while (this.clients.TryGetValue(id, out var client) && client.Connected && !client.TokenSource.IsCancellationRequested && conn.Connected) {
try {
var msg = await client.Queue.Reader.ReadAsync(client.TokenSource.Token);
await SecretMessage.SendSecretMessage(stream, handshake.Keys.tx, msg, client.TokenSource.Token);
} catch (Exception ex) {
PluginLog.LogError($"Could not send message: {ex.Message}");
}
}
try {
conn.Close();
} catch (ObjectDisposedException) { }
await listen;
this.clients.TryRemove(id, out var _);
PluginLog.Log($"Client thread ended: {id}");
});
}
private static readonly Regex colorRegex = new Regex(@"<Color\(.+?\)>", RegexOptions.Compiled);
private Dictionary<ChatType, NameFormatting> Formats { get; } = new Dictionary<ChatType, NameFormatting>();
[Sheet("LogKind")]
class LogKind : IExcelRow {
public byte Unknown0;
public byte[] Format;
public bool Unknown2;
public uint RowId { get; set; }
public uint SubRowId { get; set; }
public void PopulateData(RowParser parser, Lumina.Lumina lumina, Language language) {
this.RowId = parser.Row;
this.SubRowId = parser.SubRow;
this.Unknown0 = parser.ReadColumn<byte>(0);
this.Format = parser.ReadColumn<byte[]>(1);
this.Unknown2 = parser.ReadColumn<bool>(2);
}
}
private NameFormatting FormatFor(ChatType type) {
if (this.Formats.TryGetValue(type, out var cached)) {
return cached;
}
var logKind = this.plugin.Interface.Data.GetExcelSheet<LogKind>().GetRow((ushort)type);
if (logKind == null) {
return null;
}
var sestring = this.plugin.Interface.SeStringManager.Parse(logKind.Format);
//PluginLog.Log(string.Join("", Encoding.ASCII.GetBytes(logKind.Format).Select(b => b.ToString("x2"))));
//var sestring = this.plugin.Interface.SeStringManager.Parse(Encoding.ASCII.GetBytes(logKind.Format));
//var format = colorRegex.Replace(logKind.Format, "")
// .Replace("</Color>", "")
// .Replace("<Highlight>", "")
// .Replace("</Highlight>", "");
return NameFormatting.Empty();
//if (format.IndexOf("StringParameter(1)") == -1) {
// return NameFormatting.Empty();
//}
//var parts = format.Split(new string[] { "StringParameter(1)" }, StringSplitOptions.None);
//var nameFormatting = NameFormatting.Of(
// before: parts[0],
// after: parts[1].Split(new string[] { "StringParameter(2)" }, StringSplitOptions.None)[0]
//);
//this.Formats[type] = nameFormatting;
//return nameFormatting;
}
private async Task SendBacklogs(ServerMessage[] messages, NetworkStream stream, Client client) {
var size = 5 + SecretMessage.MacSize(); // assume 5 bytes for payload lead-in, although it's likely to be less
var responseMessages = new List<ServerMessage>();
async Task SendBacklog() {
var resp = new ServerBacklog(responseMessages.ToArray());
try {
await SecretMessage.SendSecretMessage(stream, client.Handshake.Keys.tx, resp, client.TokenSource.Token);
} catch (Exception ex) {
PluginLog.LogError($"Could not send backlog: {ex.Message}");
}
}
foreach (var catchUpMessage in messages) {
// FIXME: this is very gross
var len = MessagePackSerializer.Serialize(catchUpMessage).Length;
// send message if it would've gone over length
if (size + len >= MAX_MESSAGE_SIZE) {
await SendBacklog();
size = 5 + SecretMessage.MacSize();
responseMessages.Clear();
}
size += len;
responseMessages.Add(catchUpMessage);
}
if (responseMessages.Count > 0) {
await SendBacklog();
}
}
private static List<Chunk> ToChunks(SeString msg, uint? defaultColour) {
var chunks = new List<Chunk>();
var italic = false;
var foreground = new Stack<uint>();
var glow = new Stack<uint>();
void Append(string text) {
chunks.Add(new TextChunk {
FallbackColour = defaultColour,
Foreground = foreground.Count > 0 ? foreground.Peek() : (uint?)null,
Glow = glow.Count > 0 ? glow.Peek() : (uint?)null,
Italic = italic,
Content = text,
});
}
foreach (var payload in msg.Payloads) {
switch (payload.Type) {
case PayloadType.EmphasisItalic:
var newStatus = ((EmphasisItalicPayload)payload).IsEnabled;
italic = newStatus;
break;
case PayloadType.UIForeground:
var foregroundPayload = (UIForegroundPayload)payload;
if (foregroundPayload.IsEnabled) {
foreground.Push(foregroundPayload.UIColor.UIForeground);
} else if (foreground.Count > 0) {
foreground.Pop();
}
break;
case PayloadType.UIGlow:
var glowPayload = (UIGlowPayload)payload;
if (glowPayload.IsEnabled) {
glow.Push(glowPayload.UIColor.UIGlow);
} else if (glow.Count > 0) {
glow.Pop();
}
break;
case PayloadType.AutoTranslateText:
chunks.Add(new IconChunk { Index = 54 });
var autoText = ((AutoTranslatePayload)payload).Text;
Append(autoText.Substring(2, autoText.Length - 4));
chunks.Add(new IconChunk { Index = 55 });
break;
case PayloadType.Icon:
var index = ((IconPayload)payload).IconIndex;
chunks.Add(new IconChunk { Index = (byte)index });
break;
// FIXME: use ITextProvider directly once it's exposed
case PayloadType.RawText:
Append(((TextPayload)payload).Text);
break;
case PayloadType.Unknown:
var rawPayload = (RawPayload)payload;
if (rawPayload.Data[1] == 0x13) {
foreground.Pop();
glow.Pop();
}
break;
//default:
// var textProviderType = typeof(SeString).Assembly.GetType("Dalamud.Game.Chat.SeStringHandling.ITextProvider");
// var textProp = textProviderType.GetProperty("Text", BindingFlags.NonPublic | BindingFlags.Instance);
// var text = (string)textProp.GetValue(payload);
// append(text);
// break;
}
}
return chunks;
}
private ServerMessage[] MessagesAfter(DateTime time) => this.Backlog.Where(msg => msg.Timestamp > time).ToArray();
private static string[] Wrap(string input) {
const int LIMIT = 500;
if (input.Length <= LIMIT) {
return new string[] { input };
}
string prefix = string.Empty;
if (input.StartsWith("/")) {
var space = input.IndexOf(' ');
if (space != -1) {
prefix = input.Substring(0, space);
input = input.Substring(space + 1);
}
}
var parts = new List<string>();
var builder = new StringBuilder(LIMIT);
foreach (var word in input.Split(' ')) {
if (word.Length > LIMIT) {
int wordParts = (int)Math.Ceiling((float)word.Length / LIMIT);
for (int i = 0; i < wordParts; i++) {
var start = i == 0 ? 0 : (i * LIMIT);
var partLength = LIMIT;
if (prefix.Length != 0) {
start = start == 0 ? 0 : (start - (prefix.Length + 1) * i);
partLength = partLength - prefix.Length - 1;
}
var part = word.Length - start < partLength ? word.Substring(start) : word.Substring(start, partLength);
if (part.Length == 0) {
continue;
}
if (prefix.Length != 0) {
part = prefix + " " + part;
}
parts.Add(part);
}
continue;
}
if (builder.Length + word.Length > LIMIT) {
parts.Add(builder.ToString().TrimEnd(' '));
builder.Clear();
}
if (builder.Length == 0 && prefix.Length != 0) {
builder.Append(prefix);
builder.Append(' ');
}
builder.Append(word);
builder.Append(' ');
}
if (builder.Length != 0) {
parts.Add(builder.ToString().TrimEnd(' '));
}
return parts.ToArray();
}
private void BroadcastMessage(IEncodable message) {
foreach (var client in this.Clients.Values) {
if (client.Handshake == null || client.Conn == null) {
continue;
}
Task.Run(async () => {
await SecretMessage.SendSecretMessage(client.Conn.GetStream(), client.Handshake.Keys.tx, message);
});
}
}
private string LocalisedChannelName(InputChannel channel) {
uint rowId;
switch (channel) {
case InputChannel.Tell:
rowId = 3;
break;
case InputChannel.Say:
rowId = 1;
break;
case InputChannel.Party:
rowId = 4;
break;
case InputChannel.Alliance:
rowId = 17;
break;
case InputChannel.Yell:
rowId = 16;
break;
case InputChannel.Shout:
rowId = 2;
break;
case InputChannel.FreeCompany:
rowId = 7;
break;
case InputChannel.PvpTeam:
rowId = 19;
break;
case InputChannel.NoviceNetwork:
rowId = 18;
break;
case InputChannel.CrossLinkshell1:
rowId = 20;
break;
case InputChannel.CrossLinkshell2:
rowId = 300;
break;
case InputChannel.CrossLinkshell3:
rowId = 301;
break;
case InputChannel.CrossLinkshell4:
rowId = 302;
break;
case InputChannel.CrossLinkshell5:
rowId = 303;
break;
case InputChannel.CrossLinkshell6:
rowId = 304;
break;
case InputChannel.CrossLinkshell7:
rowId = 305;
break;
case InputChannel.CrossLinkshell8:
rowId = 306;
break;
case InputChannel.Linkshell1:
rowId = 8;
break;
case InputChannel.Linkshell2:
rowId = 9;
break;
case InputChannel.Linkshell3:
rowId = 10;
break;
case InputChannel.Linkshell4:
rowId = 11;
break;
case InputChannel.Linkshell5:
rowId = 12;
break;
case InputChannel.Linkshell6:
rowId = 13;
break;
case InputChannel.Linkshell7:
rowId = 14;
break;
case InputChannel.Linkshell8:
rowId = 15;
break;
default:
rowId = 0;
break;
};
return this.plugin.Interface.Data.GetExcelSheet<LogFilter>().GetRow(rowId).Name;
}
public void OnChatChannelChange(uint channel) {
var inputChannel = (InputChannel)channel;
this.currentChannel = inputChannel;
var localisedName = this.LocalisedChannelName(inputChannel);
var msg = new ServerChannel(inputChannel, localisedName);
this.BroadcastMessage(msg);
}
private void BroadcastAvailability(bool available) {
this.BroadcastMessage(new Availability(available));
}
private PlayerData GeneratePlayerData() {
var player = this.plugin.Interface.ClientState.LocalPlayer;
if (player == null) {
return null;
}
var homeWorld = player.HomeWorld.GameData.Name;
var currentWorld = player.CurrentWorld.GameData.Name;
var territory = this.plugin.Interface.Data.GetExcelSheet<TerritoryType>().GetRow(this.plugin.Interface.ClientState.TerritoryType);
var location = territory.PlaceName.Value.Name;
var name = player.Name;
return new PlayerData(homeWorld, currentWorld, location, name);
}
private async Task SendPlayerData(Client client) {
var playerData = this.GeneratePlayerData();
if (playerData == null) {
return;
}
await SecretMessage.SendSecretMessage(client.Conn.GetStream(), client.Handshake.Keys.tx, playerData);
}
private void BroadcastPlayerData() {
var playerData = this.GeneratePlayerData();
if (playerData == null) {
this.BroadcastMessage(EmptyPlayerData.Instance);
return;
}
this.BroadcastMessage(playerData);
}
public void OnLogIn(object sender, EventArgs e) {
this.BroadcastAvailability(true);
// send player data on next framework update
this.sendPlayerData = true;
}
public void OnLogOut(object sender, EventArgs e) {
this.BroadcastAvailability(false);
this.BroadcastPlayerData();
}
public void OnTerritoryChange(object sender, ushort territoryId) => this.BroadcastPlayerData();
public void Dispose() {
// stop accepting new clients
this.tokenSource.Cancel();
foreach (var client in this.clients.Values) {
Task.Run(async () => {
// tell clients we're shutting down
if (client.Handshake != null) {
try {
// time out after 5 seconds
client.Conn.SendTimeout = 5_000;
await SecretMessage.SendSecretMessage(client.Conn.GetStream(), client.Handshake.Keys.tx, ServerShutdown.Instance);
} catch (Exception) { }
}
// cancel threads for open clients
client.TokenSource.Cancel();
});
}
this.plugin.Functions.ReceiveFriendList -= this.OnReceiveFriendList;
}
}
public class Client {
public bool Connected { get; set; } = false;
public TcpClient Conn { get; private set; }
public HandshakeInfo Handshake { get; set; }
public CancellationTokenSource TokenSource { get; } = new CancellationTokenSource();
public Channel<ServerMessage> Queue { get; } = Channel.CreateUnbounded<ServerMessage>();
public Client(TcpClient conn) {
this.Conn = conn;
}
public void Disconnect() {
this.Connected = false;
this.TokenSource.Cancel();
this.Conn.Close();
}
}
internal static class TcpListenerExt {
public static async Task<TcpClient> GetTcpClient(this TcpListener listener, CancellationTokenSource source) {
using (source.Token.Register(listener.Stop)) {
try {
var client = await listener.AcceptTcpClientAsync().ConfigureAwait(false);
return client;
} catch (ObjectDisposedException ex) {
// Token was canceled - swallow the exception and return null
if (source.Token.IsCancellationRequested) {
return null;
}
throw ex;
}
}
}
}
}

View File

@ -0,0 +1,137 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="..\packages\MSBuild.ILMerge.Task.1.1.3\build\MSBuild.ILMerge.Task.props" Condition="Exists('..\packages\MSBuild.ILMerge.Task.1.1.3\build\MSBuild.ILMerge.Task.props')" />
<Import Project="..\packages\ILMerge.3.0.41\build\ILMerge.props" Condition="Exists('..\packages\ILMerge.3.0.41\build\ILMerge.props')" />
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{9AEB2B77-C127-4BA8-B9AE-FB3CA1649FDB}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>XIVChatPlugin</RootNamespace>
<AssemblyName>XIVChatPlugin</AssemblyName>
<TargetFrameworkVersion>v4.8</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<Deterministic>true</Deterministic>
<NuGetPackageImportStamp>
</NuGetPackageImportStamp>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="Dalamud">
<HintPath%appdata%\XIVLauncher\addon\Hooks\Dalamud.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="ImGui.NET">
<HintPath>%appdata%\XIVLauncher\addon\Hooks\ImGui.NET.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="ImGuiScene">
<HintPath>%appdata%\XIVLauncher\addon\Hooks\ImGuiScene.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Lumina">
<HintPath>%appdata%\XIVLauncher\addon\Hooks\Lumina.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Lumina.Generated">
<HintPath>%appdata%\XIVLauncher\addon\Hooks\Lumina.Generated.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="MessagePack, Version=2.1.0.0, Culture=neutral, PublicKeyToken=b4a0369545f0a1be, processorArchitecture=MSIL">
<HintPath>..\packages\MessagePack.2.1.194\lib\netstandard2.0\MessagePack.dll</HintPath>
</Reference>
<Reference Include="MessagePack.Annotations, Version=2.1.0.0, Culture=neutral, PublicKeyToken=b4a0369545f0a1be, processorArchitecture=MSIL">
<HintPath>..\packages\MessagePack.Annotations.2.1.194\lib\netstandard2.0\MessagePack.Annotations.dll</HintPath>
</Reference>
<Reference Include="Microsoft.Bcl.AsyncInterfaces, Version=1.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
<HintPath>..\packages\Microsoft.Bcl.AsyncInterfaces.1.0.0\lib\net461\Microsoft.Bcl.AsyncInterfaces.dll</HintPath>
</Reference>
<Reference Include="Sodium.Core, Version=1.2.3.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\Sodium.Core.1.2.3\lib\netstandard2.0\Sodium.Core.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.Buffers, Version=4.0.2.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
<HintPath>..\packages\System.Buffers.4.4.0\lib\netstandard2.0\System.Buffers.dll</HintPath>
</Reference>
<Reference Include="System.Core" />
<Reference Include="System.Memory, Version=4.0.1.1, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
<HintPath>..\packages\System.Memory.4.5.3\lib\netstandard2.0\System.Memory.dll</HintPath>
</Reference>
<Reference Include="System.Numerics" />
<Reference Include="System.Numerics.Vectors, Version=4.1.3.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>..\packages\System.Numerics.Vectors.4.4.0\lib\net46\System.Numerics.Vectors.dll</HintPath>
</Reference>
<Reference Include="System.Runtime.CompilerServices.Unsafe, Version=4.0.6.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>..\packages\System.Runtime.CompilerServices.Unsafe.4.7.1\lib\net461\System.Runtime.CompilerServices.Unsafe.dll</HintPath>
</Reference>
<Reference Include="System.Threading.Channels, Version=4.0.2.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
<HintPath>..\packages\System.Threading.Channels.4.7.1\lib\net461\System.Threading.Channels.dll</HintPath>
</Reference>
<Reference Include="System.Threading.Tasks.Extensions, Version=4.2.0.1, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
<HintPath>..\packages\System.Threading.Tasks.Extensions.4.5.4\lib\net461\System.Threading.Tasks.Extensions.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="System.ValueTuple, Version=4.0.3.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
<HintPath>..\packages\System.ValueTuple.4.5.0\lib\net47\System.ValueTuple.dll</HintPath>
</Reference>
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="Configuration.cs" />
<Compile Include="Extensions.cs" />
<Compile Include="GameFunctions.cs" />
<Compile Include="Plugin.cs" />
<Compile Include="PluginUI.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Server.cs" />
</ItemGroup>
<ItemGroup>
<None Include="app.config" />
<None Include="ILMerge.props" />
<None Include="packages.config" />
<None Include="XIVChatPlugin.json" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\XIVChatCommon\XIVChatCommon.csproj">
<Project>{6f426eab-15d4-48ac-80d2-2d2df2ce5ee0}</Project>
<Name>XIVChatCommon</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<Content Include="ILMergeOrder.txt" />
<Content Include="Resources\lib\libsodium.dll" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
<ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('..\packages\ILMerge.3.0.41\build\ILMerge.props')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\ILMerge.3.0.41\build\ILMerge.props'))" />
<Error Condition="!Exists('..\packages\MSBuild.ILMerge.Task.1.1.3\build\MSBuild.ILMerge.Task.props')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\MSBuild.ILMerge.Task.1.1.3\build\MSBuild.ILMerge.Task.props'))" />
<Error Condition="!Exists('..\packages\MSBuild.ILMerge.Task.1.1.3\build\MSBuild.ILMerge.Task.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\MSBuild.ILMerge.Task.1.1.3\build\MSBuild.ILMerge.Task.targets'))" />
</Target>
<Import Project="..\packages\MSBuild.ILMerge.Task.1.1.3\build\MSBuild.ILMerge.Task.targets" Condition="Exists('..\packages\MSBuild.ILMerge.Task.1.1.3\build\MSBuild.ILMerge.Task.targets')" />
</Project>

View File

@ -0,0 +1,9 @@
{
"Author": "ascclemens",
"Name": "XIVChat Server",
"Description": "Allows you to use various XIVChat clients to chat in FFXIV from anywhere!",
"InternalName": "XIVChatPlugin",
"AssemblyVersion": "1.0.0",
"ApplicableVersion": "any",
"DalamudApiLevel": 1
}

27
XIVChatPlugin/app.config Normal file
View File

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Runtime.CompilerServices.Unsafe" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-4.0.6.0" newVersion="4.0.6.0" />
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="System.Threading.Tasks.Extensions" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-4.2.0.1" newVersion="4.2.0.1" />
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="System.Numerics.Vectors" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-4.1.4.0" newVersion="4.1.4.0" />
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="System.Buffers" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-4.0.3.0" newVersion="4.0.3.0" />
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="Numbers" publicKeyToken="9cd62db60ea5554c" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-1.7.4.0" newVersion="1.7.4.0" />
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="ILMerge" version="3.0.41" targetFramework="net48" />
<package id="libsodium" version="1.0.18" targetFramework="net48" />
<package id="MessagePack" version="2.1.194" targetFramework="net48" />
<package id="MessagePack.Annotations" version="2.1.194" targetFramework="net48" />
<package id="Microsoft.Bcl.AsyncInterfaces" version="1.0.0" targetFramework="net48" />
<package id="Microsoft.NETCore.Platforms" version="3.1.3" targetFramework="net48" />
<package id="MSBuild.ILMerge.Task" version="1.1.3" targetFramework="net48" />
<package id="Sodium.Core" version="1.2.3" targetFramework="net48" />
<package id="System.Buffers" version="4.4.0" targetFramework="net48" />
<package id="System.Memory" version="4.5.3" targetFramework="net48" />
<package id="System.Numerics.Vectors" version="4.4.0" targetFramework="net48" />
<package id="System.Reflection.Emit" version="4.6.0" targetFramework="net48" />
<package id="System.Reflection.Emit.Lightweight" version="4.6.0" targetFramework="net48" />
<package id="System.Runtime.CompilerServices.Unsafe" version="4.7.1" targetFramework="net48" />
<package id="System.Threading.Channels" version="4.7.1" targetFramework="net48" />
<package id="System.Threading.Tasks.Extensions" version="4.5.4" targetFramework="net48" />
<package id="System.ValueTuple" version="4.5.0" targetFramework="net48" />
</packages>

84
logo-no-background.svg Normal file
View File

@ -0,0 +1,84 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="black"
width="24px"
height="24px"
version="1.1"
id="svg26">
<metadata
id="metadata32">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs30" />
<g
id="layer3"
style="display:none">
<rect
style="fill:#02ccee;fill-opacity:1"
id="rect73"
width="24"
height="24"
x="0"
y="0" />
</g>
<path
d="M0 0h24v24H0V0z"
fill="none"
id="path22" />
<g
id="layer1"
style="display:inline">
<path
d="M 20,2 H 4 C 2.9,2 2,2.9 2,4 v 18 l 4,-4 h 14 c 1.1,0 2,-0.9 2,-2 V 4 C 22,2.9 21.1,2 20,2 Z m 0,14 H 6 L 4,18 V 4 h 16 z"
id="path24" />
<rect
id="rect42"
width="16"
height="12"
x="4"
y="4"
style="fill:#ffffff" />
<path
id="path54"
style="fill:#ffffff"
d="M 4,16 H 6 L 4,18 Z" />
</g>
<g
id="layer2">
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:11.8744px;line-height:0;font-family:sans-serif;letter-spacing:1.51102px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:8.90583"
x="4.1792307"
y="14.229125"
id="text36"><tspan
id="tspan34"
x="4.1792307"
y="14.229125"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:11.8744px;font-family:'Noto Sans';-inkscape-font-specification:'Noto Sans';stroke-width:8.90583"
rotate="0 0 0">XV</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;line-height:1.25;font-family:sans-serif;fill:#000000;fill-opacity:1;stroke:none"
x="9.0432262"
y="13.407496"
id="text40"><tspan
id="tspan38"
x="9.0432262"
y="13.407496">|</tspan></text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -0,0 +1,122 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
viewBox="0 0 24 24"
fill="black"
width="24px"
height="24px"
version="1.1"
id="svg26"
sodipodi:docname="logo-no-outer-background.svg"
inkscape:export-filename="D:\Downloads\logo.png"
inkscape:export-xdpi="2048"
inkscape:export-ydpi="2048"
inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07)">
<metadata
id="metadata32">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs30" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1017"
id="namedview28"
showgrid="false"
inkscape:zoom="34.458333"
inkscape:cx="12.671857"
inkscape:cy="11.591307"
inkscape:window-x="1592"
inkscape:window-y="32"
inkscape:window-maximized="1"
inkscape:current-layer="layer1"
inkscape:document-rotation="0" />
<g
inkscape:groupmode="layer"
id="layer3"
inkscape:label="Background"
style="display:none">
<rect
style="fill:#02ccee;fill-opacity:1"
id="rect73"
width="24"
height="24"
x="0"
y="0" />
</g>
<path
d="M0 0h24v24H0V0z"
fill="none"
id="path22" />
<g
inkscape:groupmode="layer"
id="layer1"
inkscape:label="Chat bubble"
style="display:inline">
<path
d="M 20,2 H 4 C 2.9,2 2,2.9 2,4 v 18 l 4,-4 h 14 c 1.1,0 2,-0.9 2,-2 V 4 C 22,2.9 21.1,2 20,2 Z m 0,14 H 6 L 4,18 V 4 h 16 z"
id="path24" />
<rect
id="rect42"
width="16"
height="12"
x="4"
y="4"
style="fill:#ffffff" />
<path
id="path54"
style="fill:#ffffff;stroke-width:1.09024"
inkscape:transform-center-x="0.13553394"
inkscape:transform-center-y="-0.19403318"
d="M 4,15.622733 H 6 L 4,18 Z"
sodipodi:nodetypes="cccc" />
</g>
<g
inkscape:groupmode="layer"
id="layer2"
inkscape:label="Text">
<g
aria-label="XV"
id="text36"
style="font-style:normal;font-weight:normal;font-size:11.8744px;line-height:0;font-family:sans-serif;letter-spacing:1.51102px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:8.90583">
<path
d="M 11.090132,14.229125 H 9.8789428 L 7.6346811,10.548061 5.3547963,14.229125 H 4.2267283 L 7.0409611,9.8118482 4.4048443,5.7508033 h 1.18744 l 2.07802,3.3248321 2.0898945,-3.3248321 H 10.888267 L 8.2640243,9.7880994 Z"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:11.8744px;font-family:'Noto Sans';-inkscape-font-specification:'Noto Sans';stroke-width:8.90583"
id="path869" />
<path
d="M 19.773272,5.7508033 16.7453,14.229125 H 15.676604 L 12.648632,5.7508033 h 1.116194 l 1.911778,5.4384757 q 0.18999,0.522473 0.308734,0.985575 0.130619,0.451227 0.225614,0.866831 0.08312,-0.415604 0.213739,-0.878706 0.130619,-0.463101 0.320609,-0.997449 l 1.899904,-5.4147267 z"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:11.8744px;font-family:'Noto Sans';-inkscape-font-specification:'Noto Sans';stroke-width:8.90583"
id="path871" />
</g>
<g
aria-label="|"
id="text40"
style="font-style:normal;font-weight:normal;line-height:1.25;font-family:sans-serif;fill:#000000;fill-opacity:1;stroke:none">
<path
d="M 12.277601,15.704371 H 11.25807 V 4.290309 h 1.019531 z"
id="path866" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.2 KiB

83
logo-plain.svg Normal file
View File

@ -0,0 +1,83 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="black"
width="24px"
height="24px"
version="1.1"
id="svg26">
<metadata
id="metadata32">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs30" />
<g
id="layer3">
<rect
style="fill:#02ccee;fill-opacity:1"
id="rect73"
width="24"
height="24"
x="0"
y="0" />
</g>
<path
d="M0 0h24v24H0V0z"
fill="none"
id="path22" />
<g
id="layer1"
style="display:inline">
<path
d="M 20,2 H 4 C 2.9,2 2,2.9 2,4 v 18 l 4,-4 h 14 c 1.1,0 2,-0.9 2,-2 V 4 C 22,2.9 21.1,2 20,2 Z m 0,14 H 6 L 4,18 V 4 h 16 z"
id="path24" />
<rect
id="rect42"
width="16"
height="12"
x="4"
y="4"
style="fill:#ffffff" />
<path
id="path54"
style="fill:#ffffff"
d="M 4,16 H 6 L 4,18 Z" />
</g>
<g
id="layer2">
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:11.8744px;line-height:0;font-family:sans-serif;letter-spacing:1.51102px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:8.90583"
x="4.1792307"
y="14.229125"
id="text36"><tspan
id="tspan34"
x="4.1792307"
y="14.229125"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:11.8744px;font-family:'Noto Sans';-inkscape-font-specification:'Noto Sans';stroke-width:8.90583"
rotate="0 0 0">XV</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;line-height:1.25;font-family:sans-serif;fill:#000000;fill-opacity:1;stroke:none"
x="9.0432262"
y="13.407496"
id="text40"><tspan
id="tspan38"
x="9.0432262"
y="13.407496">|</tspan></text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

121
logo.svg Normal file
View File

@ -0,0 +1,121 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
viewBox="0 0 24 24"
fill="black"
width="24px"
height="24px"
version="1.1"
id="svg26"
sodipodi:docname="logo.svg"
inkscape:export-filename="D:\Downloads\logo.png"
inkscape:export-xdpi="2048"
inkscape:export-ydpi="2048"
inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07)">
<metadata
id="metadata32">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs30" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1017"
id="namedview28"
showgrid="false"
inkscape:zoom="34.458333"
inkscape:cx="12.671857"
inkscape:cy="11.591307"
inkscape:window-x="1592"
inkscape:window-y="32"
inkscape:window-maximized="1"
inkscape:current-layer="layer3" />
<g
inkscape:groupmode="layer"
id="layer3"
inkscape:label="Background">
<rect
style="fill:#02ccee;fill-opacity:1"
id="rect73"
width="24"
height="24"
x="0"
y="0" />
</g>
<path
d="M0 0h24v24H0V0z"
fill="none"
id="path22" />
<g
inkscape:groupmode="layer"
id="layer1"
inkscape:label="Chat bubble"
style="display:inline">
<path
d="M 20,2 H 4 C 2.9,2 2,2.9 2,4 v 18 l 4,-4 h 14 c 1.1,0 2,-0.9 2,-2 V 4 C 22,2.9 21.1,2 20,2 Z m 0,14 H 6 L 4,18 V 4 h 16 z"
id="path24" />
<rect
id="rect42"
width="16"
height="12"
x="4"
y="4"
style="fill:#ffffff" />
<path
id="path54"
style="fill:#ffffff"
inkscape:transform-center-x="0.13553394"
inkscape:transform-center-y="-0.16324063"
d="M 4,16 H 6 L 4,18 Z"
sodipodi:nodetypes="cccc" />
</g>
<g
inkscape:groupmode="layer"
id="layer2"
inkscape:label="Text">
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:11.8744px;line-height:0;font-family:sans-serif;letter-spacing:1.51102px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:8.90583"
x="4.1792307"
y="14.229125"
id="text36"><tspan
sodipodi:role="line"
id="tspan34"
x="4.1792307"
y="14.229125"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:11.8744px;font-family:'Noto Sans';-inkscape-font-specification:'Noto Sans';stroke-width:8.90583"
rotate="0 0 0">XV</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;line-height:1.25;font-family:sans-serif;fill:#000000;fill-opacity:1;stroke:none"
x="9.0432262"
y="13.407496"
id="text40"><tspan
sodipodi:role="line"
id="tspan38"
x="9.0432262"
y="13.407496">|</tspan></text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.6 KiB