commit 5b09f68db96e990def066fdfddc33b33d51acef2 Author: Anna Clemens Date: Wed Dec 29 14:31:45 2021 -0500 chore: initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1db30bf --- /dev/null +++ b/.gitignore @@ -0,0 +1,365 @@ +## 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 + +# Packaging +pack/ + +# 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 diff --git a/ChatTwo.sln b/ChatTwo.sln new file mode 100755 index 0000000..3316d4b --- /dev/null +++ b/ChatTwo.sln @@ -0,0 +1,16 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChatTwo", "ChatTwo\ChatTwo.csproj", "{739F75E6-B65F-41EF-9D90-F7BC519E4875}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {739F75E6-B65F-41EF-9D90-F7BC519E4875}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {739F75E6-B65F-41EF-9D90-F7BC519E4875}.Debug|Any CPU.Build.0 = Debug|Any CPU + {739F75E6-B65F-41EF-9D90-F7BC519E4875}.Release|Any CPU.ActiveCfg = Release|Any CPU + {739F75E6-B65F-41EF-9D90-F7BC519E4875}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/ChatTwo/ChatTwo.csproj b/ChatTwo/ChatTwo.csproj new file mode 100755 index 0000000..d0365a0 --- /dev/null +++ b/ChatTwo/ChatTwo.csproj @@ -0,0 +1,59 @@ + + + + 1.0.0 + net5.0-windows + enable + enable + true + false + true + preview + + + + $(AppData)\XIVLauncher\addon\Hooks\dev + + + + $(HOME)/dalamud + + + + + $(Dalamud)\Dalamud.dll + false + + + $(Dalamud)\FFXIVClientStructs.dll + false + + + $(Dalamud)\ImGui.NET.dll + false + + + $(Dalamud)\ImGuiScene.dll + false + + + $(Dalamud)\Lumina.dll + false + + + $(Dalamud)\Lumina.Excel.dll + false + + + + + + + + + + + + + + diff --git a/ChatTwo/Chunk.cs b/ChatTwo/Chunk.cs new file mode 100755 index 0000000..6504be7 --- /dev/null +++ b/ChatTwo/Chunk.cs @@ -0,0 +1,35 @@ +using ChatTwo.Code; +using Dalamud.Game.Text.SeStringHandling; + +namespace ChatTwo; + +internal abstract class Chunk { +} + +internal class TextChunk : Chunk { + internal ChatType? FallbackColour { get; set; } + internal uint? Foreground { get; set; } + internal uint? Glow { get; set; } + internal bool Italic { get; set; } + internal string Content { get; set; } + + internal TextChunk(string content) { + this.Content = content; + } + + internal TextChunk(ChatType? fallbackColour, uint? foreground, uint? glow, bool italic, string content) { + this.FallbackColour = fallbackColour; + this.Foreground = foreground; + this.Glow = glow; + this.Italic = italic; + this.Content = content; + } +} + +internal class IconChunk : Chunk { + internal BitmapFontIcon Icon; + + public IconChunk(BitmapFontIcon icon) { + this.Icon = icon; + } +} diff --git a/ChatTwo/Code/ChatCode.cs b/ChatTwo/Code/ChatCode.cs new file mode 100755 index 0000000..11ddcb3 --- /dev/null +++ b/ChatTwo/Code/ChatCode.cs @@ -0,0 +1,91 @@ +namespace ChatTwo.Code; + +internal class ChatCode { + private const ushort Clear7 = ~(~0 << 7); + + internal ushort Raw { get; } + + internal ChatType Type => (ChatType) (this.Raw & Clear7); + internal ChatSource Source => this.SourceFrom(11); + internal ChatSource Target => this.SourceFrom(7); + private ChatSource SourceFrom(ushort shift) => (ChatSource) (1 << ((this.Raw >> shift) & 0xF)); + + internal ChatCode(ushort raw) { + this.Raw = raw; + } + + internal ChatType Parent() => this.Type switch { + ChatType.Say => ChatType.Say, + ChatType.GmSay => ChatType.Say, + ChatType.Shout => ChatType.Shout, + ChatType.GmShout => ChatType.Shout, + ChatType.TellOutgoing => ChatType.TellOutgoing, + ChatType.TellIncoming => ChatType.TellOutgoing, + ChatType.GmTell => ChatType.TellOutgoing, + ChatType.Party => ChatType.Party, + ChatType.CrossParty => ChatType.Party, + ChatType.GmParty => ChatType.Party, + ChatType.Linkshell1 => ChatType.Linkshell1, + ChatType.GmLinkshell1 => ChatType.Linkshell1, + ChatType.Linkshell2 => ChatType.Linkshell2, + ChatType.GmLinkshell2 => ChatType.Linkshell2, + ChatType.Linkshell3 => ChatType.Linkshell3, + ChatType.GmLinkshell3 => ChatType.Linkshell3, + ChatType.Linkshell4 => ChatType.Linkshell4, + ChatType.GmLinkshell4 => ChatType.Linkshell4, + ChatType.Linkshell5 => ChatType.Linkshell5, + ChatType.GmLinkshell5 => ChatType.Linkshell5, + ChatType.Linkshell6 => ChatType.Linkshell6, + ChatType.GmLinkshell6 => ChatType.Linkshell6, + ChatType.Linkshell7 => ChatType.Linkshell7, + ChatType.GmLinkshell7 => ChatType.Linkshell7, + ChatType.Linkshell8 => ChatType.Linkshell8, + ChatType.GmLinkshell8 => ChatType.Linkshell8, + ChatType.FreeCompany => ChatType.FreeCompany, + ChatType.GmFreeCompany => ChatType.FreeCompany, + ChatType.NoviceNetwork => ChatType.NoviceNetwork, + ChatType.GmNoviceNetwork => ChatType.NoviceNetwork, + ChatType.CustomEmote => ChatType.CustomEmote, + ChatType.StandardEmote => ChatType.StandardEmote, + ChatType.Yell => ChatType.Yell, + ChatType.GmYell => ChatType.Yell, + ChatType.GainBuff => ChatType.GainBuff, + ChatType.LoseBuff => ChatType.GainBuff, + ChatType.GainDebuff => ChatType.GainDebuff, + ChatType.LoseDebuff => ChatType.GainDebuff, + ChatType.System => ChatType.System, + ChatType.Alarm => ChatType.System, + ChatType.RetainerSale => ChatType.System, + ChatType.PeriodicRecruitmentNotification => ChatType.System, + ChatType.Sign => ChatType.System, + ChatType.Orchestrion => ChatType.System, + ChatType.MessageBook => ChatType.System, + ChatType.NpcDialogue => ChatType.NpcDialogue, + ChatType.NpcAnnouncement => ChatType.NpcDialogue, + ChatType.LootRoll => ChatType.LootRoll, + ChatType.RandomNumber => ChatType.LootRoll, + ChatType.FreeCompanyAnnouncement => ChatType.FreeCompanyAnnouncement, + ChatType.FreeCompanyLoginLogout => ChatType.FreeCompanyAnnouncement, + ChatType.PvpTeamAnnouncement => ChatType.PvpTeamAnnouncement, + ChatType.PvpTeamLoginLogout => ChatType.PvpTeamAnnouncement, + _ => this.Type, + }; + + internal 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; + } + } +} diff --git a/ChatTwo/Code/ChatSource.cs b/ChatTwo/Code/ChatSource.cs new file mode 100755 index 0000000..1e2966b --- /dev/null +++ b/ChatTwo/Code/ChatSource.cs @@ -0,0 +1,17 @@ +namespace ChatTwo.Code; + +[System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1028:Enum Storage should be Int32")] +[Flags] +internal 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, +} diff --git a/ChatTwo/Code/ChatSourceExt.cs b/ChatTwo/Code/ChatSourceExt.cs new file mode 100755 index 0000000..c07fdbf --- /dev/null +++ b/ChatTwo/Code/ChatSourceExt.cs @@ -0,0 +1,16 @@ +namespace ChatTwo.Code; + +internal static class ChatSourceExt { + internal const ChatSource All = + ChatSource.Self + | ChatSource.PartyMember + | ChatSource.AllianceMember + | ChatSource.Other + | ChatSource.EngagedEnemy + | ChatSource.UnengagedEnemy + | ChatSource.FriendlyNpc + | ChatSource.SelfPet + | ChatSource.PartyPet + | ChatSource.AlliancePet + | ChatSource.OtherPet; +} diff --git a/ChatTwo/Code/ChatType.cs b/ChatTwo/Code/ChatType.cs new file mode 100755 index 0000000..657394c --- /dev/null +++ b/ChatTwo/Code/ChatType.cs @@ -0,0 +1,87 @@ +namespace ChatTwo.Code; + +[System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1028:Enum Storage should be Int32")] +internal 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, +} diff --git a/ChatTwo/Code/ChatTypeExt.cs b/ChatTwo/Code/ChatTypeExt.cs new file mode 100755 index 0000000..5fefb5b --- /dev/null +++ b/ChatTwo/Code/ChatTypeExt.cs @@ -0,0 +1,209 @@ +using ChatTwo.Util; + +namespace ChatTwo.Code; + +internal static class ChatTypeExt { + internal static string? Name(this ChatType type) { + return type switch { + ChatType.Debug => "Debug", + ChatType.Urgent => "Urgent", + ChatType.Notice => "Notice", + ChatType.Say => "Say", + ChatType.Shout => "Shout", + ChatType.TellOutgoing => "Tell (Outgoing)", + ChatType.TellIncoming => "Tell (Incoming)", + ChatType.Party => "Party", + ChatType.Alliance => "Alliance", + ChatType.Linkshell1 => "Linkshell [1]", + ChatType.Linkshell2 => "Linkshell [2]", + ChatType.Linkshell3 => "Linkshell [3]", + ChatType.Linkshell4 => "Linkshell [4]", + ChatType.Linkshell5 => "Linkshell [5]", + ChatType.Linkshell6 => "Linkshell [6]", + ChatType.Linkshell7 => "Linkshell [7]", + ChatType.Linkshell8 => "Linkshell [8]", + ChatType.FreeCompany => "Free Company", + ChatType.NoviceNetwork => "Novice Network", + ChatType.CustomEmote => "Custom Emotes", + ChatType.StandardEmote => "Standard Emotes", + ChatType.Yell => "Yell", + ChatType.CrossParty => "Cross-world Party", + ChatType.PvpTeam => "PvP Team", + ChatType.CrossLinkshell1 => "Cross-world Linkshell [1]", + ChatType.Damage => "Damage dealt", + ChatType.Miss => "Failed attacks", + ChatType.Action => "Actions used", + ChatType.Item => "Items used", + ChatType.Healing => "Healing", + ChatType.GainBuff => "Beneficial effects granted", + ChatType.GainDebuff => "Detrimental effects inflicted", + ChatType.LoseBuff => "Beneficial effects lost", + ChatType.LoseDebuff => "Detrimental effects cured", + ChatType.Alarm => "Alarm Notifications", + ChatType.Echo => "Echo", + ChatType.System => "System Messages", + ChatType.BattleSystem => "Battle System Messages", + ChatType.GatheringSystem => "Gathering System Messages", + ChatType.Error => "Error Messages", + ChatType.NpcDialogue => "NPC Dialogue", + ChatType.LootNotice => "Loot Notices", + ChatType.Progress => "Progression Messages", + ChatType.LootRoll => "Loot Messages", + ChatType.Crafting => "Synthesis Messages", + ChatType.Gathering => "Gathering Messages", + ChatType.NpcAnnouncement => "NPC Dialogue (Announcements)", + ChatType.FreeCompanyAnnouncement => "Free Company Announcements", + ChatType.FreeCompanyLoginLogout => "Free Company Member Login Notifications", + ChatType.RetainerSale => "Retainer Sale Notifications", + ChatType.PeriodicRecruitmentNotification => "Periodic Recruitment Notifications", + ChatType.Sign => "Sign Messages for PC Targets", + ChatType.RandomNumber => "Random Number Messages", + ChatType.NoviceNetworkSystem => "Novice Network Notifications", + ChatType.Orchestrion => "Current Orchestrion Track Messages", + ChatType.PvpTeamAnnouncement => "PvP Team Announcements", + ChatType.PvpTeamLoginLogout => "PvP Team Member Login Notifications", + ChatType.MessageBook => "Message Book Alert", + ChatType.GmTell => "Tell (GM)", + ChatType.GmSay => "Say (GM)", + ChatType.GmShout => "Shout (GM)", + ChatType.GmYell => "Yell (GM)", + ChatType.GmParty => "Party (GM)", + ChatType.GmFreeCompany => "Free Company (GM)", + ChatType.GmLinkshell1 => "Linkshell [1] (GM)", + ChatType.GmLinkshell2 => "Linkshell [2] (GM)", + ChatType.GmLinkshell3 => "Linkshell [3] (GM)", + ChatType.GmLinkshell4 => "Linkshell [4] (GM)", + ChatType.GmLinkshell5 => "Linkshell [5] (GM)", + ChatType.GmLinkshell6 => "Linkshell [6] (GM)", + ChatType.GmLinkshell7 => "Linkshell [7] (GM)", + ChatType.GmLinkshell8 => "Linkshell [8] (GM)", + ChatType.GmNoviceNetwork => "Novice Network (GM)", + ChatType.CrossLinkshell2 => "Cross-world Linkshell [2]", + ChatType.CrossLinkshell3 => "Cross-world Linkshell [3]", + ChatType.CrossLinkshell4 => "Cross-world Linkshell [4]", + ChatType.CrossLinkshell5 => "Cross-world Linkshell [5]", + ChatType.CrossLinkshell6 => "Cross-world Linkshell [6]", + ChatType.CrossLinkshell7 => "Cross-world Linkshell [7]", + ChatType.CrossLinkshell8 => "Cross-world Linkshell [8]", + _ => type.ToString(), + }; + } + + internal static uint? DefaultColour(this ChatType type) { + switch (type) { + case ChatType.Debug: + return ColourUtil.ComponentsToRgba(204, 204, 204); + case ChatType.Urgent: + return ColourUtil.ComponentsToRgba(255, 127, 127); + case ChatType.Notice: + return ColourUtil.ComponentsToRgba(179, 140, 255); + + case ChatType.Say: + case ChatType.GmSay: + return ColourUtil.ComponentsToRgba(247, 247, 247); + case ChatType.Shout: + case ChatType.GmShout: + return ColourUtil.ComponentsToRgba(255, 166, 102); + case ChatType.TellIncoming: + case ChatType.TellOutgoing: + case ChatType.GmTell: + return ColourUtil.ComponentsToRgba(255, 184, 222); + case ChatType.Party: + case ChatType.CrossParty: + case ChatType.GmParty: + return ColourUtil.ComponentsToRgba(102, 229, 255); + case ChatType.Alliance: + return ColourUtil.ComponentsToRgba(255, 127, 0); + case ChatType.NoviceNetwork: + case ChatType.NoviceNetworkSystem: + case ChatType.GmNoviceNetwork: + return ColourUtil.ComponentsToRgba(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: + case ChatType.GmLinkshell1: + case ChatType.GmLinkshell2: + case ChatType.GmLinkshell3: + case ChatType.GmLinkshell4: + case ChatType.GmLinkshell5: + case ChatType.GmLinkshell6: + case ChatType.GmLinkshell7: + case ChatType.GmLinkshell8: + return ColourUtil.ComponentsToRgba(212, 255, 125); + case ChatType.StandardEmote: + return ColourUtil.ComponentsToRgba(186, 255, 240); + case ChatType.CustomEmote: + return ColourUtil.ComponentsToRgba(186, 255, 240); + case ChatType.Yell: + case ChatType.GmYell: + return ColourUtil.ComponentsToRgba(255, 255, 0); + case ChatType.Echo: + return ColourUtil.ComponentsToRgba(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 ColourUtil.ComponentsToRgba(204, 204, 204); + case ChatType.NpcAnnouncement: + case ChatType.NpcDialogue: + return ColourUtil.ComponentsToRgba(171, 214, 71); + case ChatType.Error: + return ColourUtil.ComponentsToRgba(255, 74, 74); + case ChatType.FreeCompany: + case ChatType.FreeCompanyAnnouncement: + case ChatType.FreeCompanyLoginLogout: + case ChatType.GmFreeCompany: + return ColourUtil.ComponentsToRgba(171, 219, 229); + case ChatType.PvpTeam: + return ColourUtil.ComponentsToRgba(171, 219, 229); + case ChatType.PvpTeamAnnouncement: + case ChatType.PvpTeamLoginLogout: + return ColourUtil.ComponentsToRgba(171, 219, 229); + case ChatType.Action: + case ChatType.Item: + case ChatType.LootNotice: + return ColourUtil.ComponentsToRgba(255, 255, 176); + case ChatType.Progress: + return ColourUtil.ComponentsToRgba(255, 222, 115); + case ChatType.LootRoll: + case ChatType.RandomNumber: + return ColourUtil.ComponentsToRgba(199, 191, 158); + case ChatType.Crafting: + case ChatType.Gathering: + return ColourUtil.ComponentsToRgba(222, 191, 247); + case ChatType.Damage: + return ColourUtil.ComponentsToRgba(255, 125, 125); + case ChatType.Miss: + return ColourUtil.ComponentsToRgba(204, 204, 204); + case ChatType.Healing: + return ColourUtil.ComponentsToRgba(212, 255, 125); + case ChatType.GainBuff: + case ChatType.LoseBuff: + return ColourUtil.ComponentsToRgba(148, 191, 255); + case ChatType.GainDebuff: + case ChatType.LoseDebuff: + return ColourUtil.ComponentsToRgba(255, 138, 196); + case ChatType.BattleSystem: + return ColourUtil.ComponentsToRgba(204, 204, 204); + default: + return null; + } + } +} diff --git a/ChatTwo/Code/InputChannel.cs b/ChatTwo/Code/InputChannel.cs new file mode 100755 index 0000000..63c36e7 --- /dev/null +++ b/ChatTwo/Code/InputChannel.cs @@ -0,0 +1,32 @@ +namespace ChatTwo.Code; + +internal enum InputChannel : uint { + 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, +} diff --git a/ChatTwo/Code/InputChannelExt.cs b/ChatTwo/Code/InputChannelExt.cs new file mode 100755 index 0000000..899539e --- /dev/null +++ b/ChatTwo/Code/InputChannelExt.cs @@ -0,0 +1,34 @@ +namespace ChatTwo.Code; + +internal static class InputChannelExt { + internal static ChatType ToChatType(this InputChannel input) { + return input switch { + InputChannel.Tell => ChatType.TellOutgoing, + InputChannel.Say => ChatType.Say, + InputChannel.Party => ChatType.Party, + InputChannel.Alliance => ChatType.Alliance, + InputChannel.Yell => ChatType.Yell, + InputChannel.Shout => ChatType.Shout, + InputChannel.FreeCompany => ChatType.FreeCompany, + InputChannel.PvpTeam => ChatType.PvpTeam, + InputChannel.NoviceNetwork => ChatType.NoviceNetwork, + InputChannel.CrossLinkshell1 => ChatType.CrossLinkshell1, + InputChannel.CrossLinkshell2 => ChatType.CrossLinkshell2, + InputChannel.CrossLinkshell3 => ChatType.CrossLinkshell3, + InputChannel.CrossLinkshell4 => ChatType.CrossLinkshell4, + InputChannel.CrossLinkshell5 => ChatType.CrossLinkshell5, + InputChannel.CrossLinkshell6 => ChatType.CrossLinkshell6, + InputChannel.CrossLinkshell7 => ChatType.CrossLinkshell7, + InputChannel.CrossLinkshell8 => ChatType.CrossLinkshell8, + InputChannel.Linkshell1 => ChatType.Linkshell1, + InputChannel.Linkshell2 => ChatType.Linkshell2, + InputChannel.Linkshell3 => ChatType.Linkshell3, + InputChannel.Linkshell4 => ChatType.Linkshell4, + InputChannel.Linkshell5 => ChatType.Linkshell5, + InputChannel.Linkshell6 => ChatType.Linkshell6, + InputChannel.Linkshell7 => ChatType.Linkshell7, + InputChannel.Linkshell8 => ChatType.Linkshell8, + _ => throw new ArgumentOutOfRangeException(nameof(input), input, null), + }; + } +} diff --git a/ChatTwo/Configuration.cs b/ChatTwo/Configuration.cs new file mode 100755 index 0000000..4e273f2 --- /dev/null +++ b/ChatTwo/Configuration.cs @@ -0,0 +1,59 @@ +using ChatTwo.Code; +using Dalamud.Configuration; + +namespace ChatTwo; + +[Serializable] +internal class Configuration : IPluginConfiguration { + public int Version { get; set; } = 1; + + public bool HideChat = true; + public float FontSize = 17f; + public Dictionary ChatColours = new(); + public List Tabs = new(); +} + +[Serializable] +internal class Tab { + public string Name = "New tab"; + public Dictionary ChatCodes = new(); + public bool DisplayUnread = true; + public bool DisplayTimestamp = true; + + [NonSerialized] + public uint Unread; + + [NonSerialized] + public Mutex MessagesMutex = new(); + + [NonSerialized] + public List Messages = new(); + + ~Tab() { + this.MessagesMutex.Dispose(); + } + + internal bool Matches(Message message) { + return this.ChatCodes.TryGetValue(message.Code.Type, out var sources) && (message.Code.Source is 0 or (ChatSource) 1 || sources.HasFlag(message.Code.Source)); + } + + internal void AddMessage(Message message) { + this.MessagesMutex.WaitOne(); + this.Messages.Add(message); + if (this.Messages.Count > 1000) { + this.Messages.RemoveAt(0); + } + this.MessagesMutex.ReleaseMutex(); + + this.Unread += 1; + } + + internal Tab Clone() { + return new Tab { + Name = this.Name, + ChatCodes = this.ChatCodes.ToDictionary(entry => entry.Key, entry => entry.Value), + DisplayUnread = this.DisplayUnread, + DisplayTimestamp = this.DisplayTimestamp, + }; + } +} diff --git a/ChatTwo/GameFunctions.cs b/ChatTwo/GameFunctions.cs new file mode 100755 index 0000000..27ec42a --- /dev/null +++ b/ChatTwo/GameFunctions.cs @@ -0,0 +1,171 @@ +using ChatTwo.Code; +using Dalamud.Game.Text.SeStringHandling; +using Dalamud.Hooking; +using Dalamud.Logging; +using Dalamud.Memory; +using FFXIVClientStructs.FFXIV.Client.System.Framework; +using FFXIVClientStructs.FFXIV.Client.UI.Agent; +using FFXIVClientStructs.FFXIV.Component.GUI; + +namespace ChatTwo; + +internal unsafe class GameFunctions : IDisposable { + private static class Signatures { + internal const string ChatLogRefresh = "40 53 56 57 48 81 EC ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 84 24 ?? ?? ?? ?? 49 8B F0 8B FA"; + internal const string ChangeChannelName = "E8 ?? ?? ?? ?? BA ?? ?? ?? ?? 48 8D 4D B0 48 8B F8 E8 ?? ?? ?? ?? 41 8B D6"; + } + + private delegate byte ChatLogRefreshDelegate(IntPtr log, ushort eventId, AtkValue* value); + + private delegate IntPtr ChangeChannelNameDelegate(IntPtr agent); + + internal delegate void ChatActivatedEventDelegate(string? input); + + private Plugin Plugin { get; } + private Hook? ChatLogRefreshHook { get; } + private Hook? ChangeChannelNameHook { get; } + + internal event ChatActivatedEventDelegate? ChatActivated; + + internal (InputChannel channel, string name) ChatChannel { get; private set; } + + internal GameFunctions(Plugin plugin) { + this.Plugin = plugin; + + if (this.Plugin.SigScanner.TryScanText(Signatures.ChatLogRefresh, out var chatLogPtr)) { + this.ChatLogRefreshHook = new Hook(chatLogPtr, this.ChatLogRefreshDetour); + this.ChatLogRefreshHook.Enable(); + } + + if (this.Plugin.SigScanner.TryScanText(Signatures.ChangeChannelName, out var channelNamePtr)) { + this.ChangeChannelNameHook = new Hook(channelNamePtr, this.ChangeChannelNameDetour); + this.ChangeChannelNameHook.Enable(); + } + + this.Plugin.ClientState.Login += this.Login; + this.Login(null, null); + } + + public void Dispose() { + this.Plugin.ClientState.Login -= this.Login; + this.ChangeChannelNameHook?.Dispose(); + this.ChatLogRefreshHook?.Dispose(); + this.ChatActivated = null; + } + + private void Login(object? sender, EventArgs? e) { + if (this.ChangeChannelNameHook == null) { + return; + } + + var agent = Framework.Instance()->GetUiModule()->GetAgentModule()->GetAgentByInternalId(AgentId.ChatLog); + if (agent == null) { + return; + } + + this.ChangeChannelNameDetour((IntPtr) agent); + } + + internal static void SetAddonInteractable(string name, bool interactable) { + var unitManager = AtkStage.GetSingleton()->RaptureAtkUnitManager; + + var addon = (IntPtr) unitManager->GetAddonByName(name); + if (addon == IntPtr.Zero) { + return; + } + + var flags = (uint*) (addon + 0x180); + if (interactable) { + *flags &= ~(1u << 22); + } else { + *flags |= 1 << 22; + } + } + + internal void SetChatInteractable(bool interactable) { + for (var i = 0; i < 4; i++) { + SetAddonInteractable($"ChatLogPanel_{i}", interactable); + } + + SetAddonInteractable("ChatLog", interactable); + } + + internal static bool IsAddonInteractable(string name) { + var unitManager = AtkStage.GetSingleton()->RaptureAtkUnitManager; + + var addon = (IntPtr) unitManager->GetAddonByName(name); + if (addon == IntPtr.Zero) { + return false; + } + + var flags = (uint*) (addon + 0x180); + return (*flags & (1 << 22)) == 0; + } + + private byte ChatLogRefreshDetour(IntPtr log, ushort eventId, AtkValue* value) { + if (eventId == 0x31 && value != null && value->UInt is 0x05 or 0x0C) { + string? eventInput = null; + + var str = value + 2; + if (str != null && str->String != null) { + var input = MemoryHelper.ReadStringNullTerminated((IntPtr) str->String); + if (input.Length > 0) { + eventInput = input; + } + } + + try { + this.ChatActivated?.Invoke(eventInput); + } catch (Exception ex) { + PluginLog.LogError(ex, "Error in ChatActivated event"); + } + + return 0; + } + + return this.ChatLogRefreshHook!.Original(log, eventId, value); + } + + private IntPtr ChangeChannelNameDetour(IntPtr agent) { + // Last ShB patch + // +0x40 = chat channel (byte or uint?) + // channel is 17 (maybe 18?) for tells + // +0x48 = pointer to channel name string + var ret = this.ChangeChannelNameHook!.Original(agent); + if (agent == IntPtr.Zero) { + return ret; + } + + // E8 ?? ?? ?? ?? 8D 48 F7 + // RaptureShellModule + 0xFD0 + var shellModule = (IntPtr) Framework.Instance()->GetUiModule()->GetRaptureShellModule(); + if (shellModule == IntPtr.Zero) { + return ret; + } + + var channel = *(uint*) (shellModule + 0xFD0); + + // var channel = *(uint*) (agent + 0x40); + if (channel is 17 or 18) { + channel = 0; + } + + SeString? name = null; + var namePtrPtr = (byte**) (agent + 0x48); + if (namePtrPtr != null) { + var namePtr = *namePtrPtr; + name = MemoryHelper.ReadSeStringNullTerminated((IntPtr) namePtr); + if (name.Payloads.Count == 0) { + name = null; + } + } + + if (name == null) { + return ret; + } + + this.ChatChannel = ((InputChannel) channel, name.TextValue.TrimStart('\uE01E').Trim()); + + return ret; + } +} diff --git a/ChatTwo/Message.cs b/ChatTwo/Message.cs new file mode 100755 index 0000000..bf074ab --- /dev/null +++ b/ChatTwo/Message.cs @@ -0,0 +1,17 @@ +using ChatTwo.Code; + +namespace ChatTwo; + +internal class Message { + internal DateTime Date { get; } + internal ChatCode Code { get; } + internal List Sender { get; } + internal List Content { get; } + + internal Message(ChatCode code, List sender, List content) { + this.Date = DateTime.UtcNow; + this.Code = code; + this.Sender = sender; + this.Content = content; + } +} diff --git a/ChatTwo/Plugin.cs b/ChatTwo/Plugin.cs new file mode 100755 index 0000000..8192629 --- /dev/null +++ b/ChatTwo/Plugin.cs @@ -0,0 +1,88 @@ +using Dalamud.Data; +using Dalamud.Game; +using Dalamud.Game.ClientState; +using Dalamud.Game.Command; +using Dalamud.Game.Gui; +using Dalamud.IoC; +using Dalamud.Plugin; +using XivCommon; + +namespace ChatTwo; + +// ReSharper disable once ClassNeverInstantiated.Global +public sealed class Plugin : IDalamudPlugin { + public string Name => "Chat 2"; + + [PluginService] + internal DalamudPluginInterface Interface { get; init; } + + [PluginService] + internal ChatGui ChatGui { get; init; } + + [PluginService] + internal ClientState ClientState { get; init; } + + [PluginService] + internal CommandManager CommandManager { get; init; } + + [PluginService] + internal DataManager DataManager { get; init; } + + [PluginService] + internal Framework Framework { get; init; } + + [PluginService] + internal SigScanner SigScanner { get; init; } + + internal Configuration Config { get; } + internal XivCommonBase Common { get; } + internal GameFunctions Functions { get; } + internal Store Store { get; } + internal PluginUi Ui { get; } + + #pragma warning disable CS8618 + public Plugin() { + this.Config = this.Interface!.GetPluginConfig() as Configuration ?? new Configuration(); + this.Common = new XivCommonBase(); + this.Functions = new GameFunctions(this); + this.Store = new Store(this); + this.Ui = new PluginUi(this); + + this.Framework!.Update += this.FrameworkUpdate; + } + #pragma warning restore CS8618 + + public void Dispose() { + this.Framework.Update -= this.FrameworkUpdate; + this.Functions.SetChatInteractable(true); + + this.Ui.Dispose(); + this.Store.Dispose(); + this.Functions.Dispose(); + this.Common.Dispose(); + } + + internal void SaveConfig() { + this.Interface.SavePluginConfig(this.Config); + } + + private static readonly string[] ChatAddonNames = { + "ChatLog", + "ChatLogPanel_0", + "ChatLogPanel_1", + "ChatLogPanel_2", + "ChatLogPanel_3", + }; + + private void FrameworkUpdate(Framework framework) { + if (!this.Config.HideChat) { + return; + } + + foreach (var name in ChatAddonNames) { + if (GameFunctions.IsAddonInteractable(name)) { + GameFunctions.SetAddonInteractable(name, false); + } + } + } +} diff --git a/ChatTwo/PluginUi.cs b/ChatTwo/PluginUi.cs new file mode 100755 index 0000000..dadb2c7 --- /dev/null +++ b/ChatTwo/PluginUi.cs @@ -0,0 +1,187 @@ +using System.Runtime.InteropServices; +using ChatTwo.Ui; +using Dalamud.Interface; +using Dalamud.Logging; +using ImGuiNET; + +namespace ChatTwo; + +internal sealed class PluginUi : IDisposable { + internal Plugin Plugin { get; } + internal ImFontPtr? RegularFont { get; private set; } + internal ImFontPtr? ItalicFont { get; private set; } + + private List Components { get; } + private ImFontConfigPtr _fontCfg; + private ImFontConfigPtr _fontCfgMerge; + private (GCHandle, int) _regularFont; + private (GCHandle, int) _italicFont; + private (GCHandle, int) _jpFont; + private (GCHandle, int) _gameSymFont; + + private ImVector _ranges; + + private GCHandle _jpRange = GCHandle.Alloc( + GlyphRangesJapanese.GlyphRanges, + GCHandleType.Pinned + ); + + private GCHandle _symRange = GCHandle.Alloc( + new ushort[] { + 0xE020, + 0xE0DB, + 0, + }, + GCHandleType.Pinned + ); + + internal unsafe PluginUi(Plugin plugin) { + this.Plugin = plugin; + this.Components = new List { + new Settings(this), + new ChatLog(this), + }; + + this._fontCfg = new ImFontConfigPtr(ImGuiNative.ImFontConfig_ImFontConfig()) { + FontDataOwnedByAtlas = false, + }; + + this._fontCfgMerge = new ImFontConfigPtr(ImGuiNative.ImFontConfig_ImFontConfig()) { + FontDataOwnedByAtlas = false, + MergeMode = true, + }; + + var builder = new ImFontGlyphRangesBuilderPtr(ImGuiNative.ImFontGlyphRangesBuilder_ImFontGlyphRangesBuilder()); + builder.AddRanges(ImGui.GetIO().Fonts.GetGlyphRangesDefault()); + builder.AddText("←→↑↓《》■※☀★★☆♥♡ヅツッシ☀☁☂℃℉°♀♂♠♣♦♣♧®©™€$£♯♭♪✓√◎◆◇♦■□〇●△▽▼▲‹›≤≥<«“”─"); + builder.BuildRanges(out this._ranges); + + var regular = this.GetResource("ChatTwo.fonts.NotoSans-Regular.ttf"); + this._regularFont = ( + GCHandle.Alloc(regular, GCHandleType.Pinned), + regular.Length + ); + + var italic = this.GetResource("ChatTwo.fonts.NotoSans-Italic.ttf"); + this._italicFont = ( + GCHandle.Alloc(italic, GCHandleType.Pinned), + italic.Length + ); + + var jp = this.GetResource("ChatTwo.fonts.NotoSansJP-Regular.otf"); + this._jpFont = ( + GCHandle.Alloc(jp, GCHandleType.Pinned), + jp.Length + ); + + var gameSym = File.ReadAllBytes(Path.Combine(this.Plugin.Interface.DalamudAssetDirectory.FullName, "UIRes", "gamesym.ttf")); + this._gameSymFont = ( + GCHandle.Alloc(gameSym, GCHandleType.Pinned), + gameSym.Length + ); + + this.Plugin.Interface.UiBuilder.BuildFonts += this.BuildFonts; + this.Plugin.Interface.UiBuilder.Draw += this.Draw; + + this.Plugin.Interface.UiBuilder.RebuildFonts(); + } + + public void Dispose() { + this.Plugin.Interface.UiBuilder.Draw -= this.Draw; + this.Plugin.Interface.UiBuilder.BuildFonts -= this.BuildFonts; + + foreach (var component in this.Components) { + component.Dispose(); + } + + this._regularFont.Item1.Free(); + this._italicFont.Item1.Free(); + this._gameSymFont.Item1.Free(); + this._symRange.Free(); + this._jpRange.Free(); + this._fontCfg.Destroy(); + this._fontCfgMerge.Destroy(); + } + + private void Draw() { + var font = this.RegularFont.HasValue; + + if (font) { + ImGui.PushFont(this.RegularFont!.Value); + } + + foreach (var component in this.Components) { + try { + component.Draw(); + } catch (Exception ex) { + PluginLog.LogError(ex, "Error drawing component"); + } + } + + if (font) { + ImGui.PopFont(); + } + } + + private byte[] GetResource(string name) { + var stream = this.GetType().Assembly.GetManifestResourceStream(name)!; + var memory = new MemoryStream(); + stream.CopyTo(memory); + return memory.ToArray(); + } + + private void BuildFonts() { + this.RegularFont = null; + this.ItalicFont = null; + + // load regular noto sans and merge in jp + game icons + this.RegularFont = ImGui.GetIO().Fonts.AddFontFromMemoryTTF( + this._regularFont.Item1.AddrOfPinnedObject(), + this._regularFont.Item2, + this.Plugin.Config.FontSize, + this._fontCfg, + this._ranges.Data + ); + + ImGui.GetIO().Fonts.AddFontFromMemoryTTF( + this._jpFont.Item1.AddrOfPinnedObject(), + this._jpFont.Item2, + this.Plugin.Config.FontSize, + this._fontCfgMerge, + this._jpRange.AddrOfPinnedObject() + ); + + ImGui.GetIO().Fonts.AddFontFromMemoryTTF( + this._gameSymFont.Item1.AddrOfPinnedObject(), + this._gameSymFont.Item2, + this.Plugin.Config.FontSize, + this._fontCfgMerge, + this._symRange.AddrOfPinnedObject() + ); + + // load italic noto sans and merge in jp + game icons + this.ItalicFont = ImGui.GetIO().Fonts.AddFontFromMemoryTTF( + this._italicFont.Item1.AddrOfPinnedObject(), + this._italicFont.Item2, + this.Plugin.Config.FontSize, + this._fontCfg, + this._ranges.Data + ); + + ImGui.GetIO().Fonts.AddFontFromMemoryTTF( + this._jpFont.Item1.AddrOfPinnedObject(), + this._jpFont.Item2, + this.Plugin.Config.FontSize, + this._fontCfgMerge, + this._jpRange.AddrOfPinnedObject() + ); + + ImGui.GetIO().Fonts.AddFontFromMemoryTTF( + this._gameSymFont.Item1.AddrOfPinnedObject(), + this._gameSymFont.Item2, + this.Plugin.Config.FontSize, + this._fontCfgMerge, + this._symRange.AddrOfPinnedObject() + ); + } +} diff --git a/ChatTwo/Store.cs b/ChatTwo/Store.cs new file mode 100755 index 0000000..6cda22e --- /dev/null +++ b/ChatTwo/Store.cs @@ -0,0 +1,168 @@ +using ChatTwo.Code; +using ChatTwo.Util; +using Dalamud.Game.Text; +using Dalamud.Game.Text.SeStringHandling; +using Lumina.Excel.GeneratedSheets; + +namespace ChatTwo; + +internal class Store : IDisposable { + internal sealed class MessagesLock : IDisposable { + private Mutex Mutex { get; } + internal List Messages { get; } + + internal MessagesLock(List messages, Mutex mutex) { + this.Messages = messages; + this.Mutex = mutex; + + this.Mutex.WaitOne(); + } + + public void Dispose() { + this.Mutex.ReleaseMutex(); + } + } + + private Plugin Plugin { get; } + + private Mutex MessagesMutex { get; } = new(); + private List Messages { get; } = new(); + + private Dictionary Formats { get; } = new(); + + internal Store(Plugin plugin) { + this.Plugin = plugin; + + this.Plugin.ChatGui.ChatMessageUnhandled += this.ChatMessage; + } + + public void Dispose() { + this.Plugin.ChatGui.ChatMessageUnhandled -= this.ChatMessage; + + this.MessagesMutex.Dispose(); + } + + internal MessagesLock GetMessages() { + return new MessagesLock(this.Messages, this.MessagesMutex); + } + + internal void AddMessage(Message message) { + using var messages = this.GetMessages(); + messages.Messages.Add(message); + + if (messages.Messages.Count > 1_000) { + messages.Messages.RemoveAt(0); + } + + foreach (var tab in this.Plugin.Config.Tabs) { + if (tab.Matches(message)) { + tab.AddMessage(message); + } + } + } + + internal void FilterAllTabs() { + foreach (var tab in this.Plugin.Config.Tabs) { + this.FilterTab(tab); + } + } + + internal void FilterTab(Tab tab) { + using var messages = this.GetMessages(); + foreach (var message in messages.Messages) { + if (tab.Matches(message)) { + tab.AddMessage(message); + } + } + } + + private void ChatMessage(XivChatType type, uint senderId, SeString sender, SeString message) { + var chatCode = new ChatCode((ushort) type); + + NameFormatting? formatting = null; + if (sender.Payloads.Count > 0) { + formatting = this.FormatFor(chatCode.Type); + } + + var senderChunks = new List(); + if (formatting is { IsPresent: true }) { + senderChunks.Add(new TextChunk(formatting.Before) { + FallbackColour = chatCode.Type, + }); + senderChunks.AddRange(ChunkUtil.ToChunks(sender, chatCode.Type)); + senderChunks.Add(new TextChunk(formatting.After) { + FallbackColour = chatCode.Type, + }); + } + + var messageChunks = ChunkUtil.ToChunks(message, chatCode.Type).ToList(); + + this.AddMessage(new Message(chatCode, senderChunks, messageChunks)); + } + + internal class NameFormatting { + internal string Before { get; private set; } = string.Empty; + internal string After { get; private set; } = string.Empty; + internal bool IsPresent { get; private set; } = true; + + internal static NameFormatting Empty() { + return new() { + IsPresent = false, + }; + } + + internal static NameFormatting Of(string before, string after) { + return new() { + Before = before, + After = after, + }; + } + } + + private NameFormatting? FormatFor(ChatType type) { + if (this.Formats.TryGetValue(type, out var cached)) { + return cached; + } + + var logKind = this.Plugin.DataManager.GetExcelSheet()!.GetRow((ushort) type); + + if (logKind == null) { + return null; + } + + var format = (SeString) logKind.Format; + + static bool IsStringParam(Payload payload, byte num) { + var data = payload.Encode(); + + return data.Length >= 5 && data[1] == 0x29 && data[4] == num + 1; + } + + var firstStringParam = format.Payloads.FindIndex(payload => IsStringParam(payload, 1)); + var secondStringParam = format.Payloads.FindIndex(payload => IsStringParam(payload, 2)); + + if (firstStringParam == -1 || secondStringParam == -1) { + return NameFormatting.Empty(); + } + + var before = format.Payloads + .GetRange(0, firstStringParam) + .Where(payload => payload is ITextProvider) + .Cast() + .Select(text => text.Text); + var after = format.Payloads + .GetRange(firstStringParam + 1, secondStringParam - firstStringParam) + .Where(payload => payload is ITextProvider) + .Cast() + .Select(text => text.Text); + + var nameFormatting = NameFormatting.Of( + string.Join("", before), + string.Join("", after) + ); + + this.Formats[type] = nameFormatting; + + return nameFormatting; + } +} diff --git a/ChatTwo/Ui/ChatLog.cs b/ChatTwo/Ui/ChatLog.cs new file mode 100755 index 0000000..384c510 --- /dev/null +++ b/ChatTwo/Ui/ChatLog.cs @@ -0,0 +1,290 @@ +using System.Numerics; +using ChatTwo.Code; +using ChatTwo.Util; +using ImGuiNET; +using ImGuiScene; + +namespace ChatTwo.Ui; + +internal sealed class ChatLog : IUiComponent { + private PluginUi Ui { get; } + + private bool _activate; + private string _chat = string.Empty; + private readonly TextureWrap? _fontIcon; + private readonly List _inputBacklog = new(); + private int _inputBacklogIdx = -1; + private int _lastTab; + + internal ChatLog(PluginUi ui) { + this.Ui = ui; + + this._fontIcon = this.Ui.Plugin.DataManager.GetImGuiTexture("common/font/fonticon_ps5.tex"); + + this.Ui.Plugin.Functions.ChatActivated += this.ChatActivated; + } + + public void Dispose() { + this.Ui.Plugin.Functions.ChatActivated -= this.ChatActivated; + this._fontIcon?.Dispose(); + } + + private void ChatActivated(string? input) { + this._activate = true; + if (input != null && !this._chat.Contains(input)) { + this._chat += input; + } + } + + private void AddBacklog(string message) { + for (var i = 0; i < this._inputBacklog.Count; i++) { + if (this._inputBacklog[i] != message) { + continue; + } + + this._inputBacklog.RemoveAt(i); + break; + } + + this._inputBacklog.Add(message); + } + + public unsafe void Draw() { + if (!ImGui.Begin($"{this.Ui.Plugin.Name}##chat", ImGuiWindowFlags.NoTitleBar)) { + ImGui.End(); + return; + } + + var lineHeight = ImGui.CalcTextSize("A").Y; + + if (ImGui.BeginTabBar("##chat2-tabs")) { + for (var tabI = 0; tabI < this.Ui.Plugin.Config.Tabs.Count; tabI++) { + var tab = this.Ui.Plugin.Config.Tabs[tabI]; + + var unread = tabI == this._lastTab || !tab.DisplayUnread || tab.Unread == 0 ? "" : $" ({tab.Unread})"; + if (ImGui.BeginTabItem($"{tab.Name}{unread}###log-tab-{tabI}")) { + var switchedTab = this._lastTab != tabI; + this._lastTab = tabI; + tab.Unread = 0; + + // var drawnHeight = 0f; + // var numDrawn = 0; + // var lastPos = ImGui.GetCursorPosY(); + var height = ImGui.GetContentRegionAvail().Y + - lineHeight * 2 + - ImGui.GetStyle().ItemSpacing.Y * 4; + if (ImGui.BeginChild("##chat2-messages", new Vector2(-1, height))) { + ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, Vector2.Zero); + + // var clipper = new ImGuiListClipperPtr(ImGuiNative.ImGuiListClipper_ImGuiListClipper()); + // int numMessages; + try { + tab.MessagesMutex.WaitOne(); + + for (var i = 0; i < tab.Messages.Count; i++) { + // numDrawn += 1; + var message = tab.Messages[i]; + + if (tab.DisplayTimestamp) { + var timestamp = message.Date.ToLocalTime().ToString("t"); + this.DrawChunk(new TextChunk($"[{timestamp}]") { + Foreground = 0xFFFFFFFF, + }); + ImGui.SameLine(); + } + + if (message.Sender.Count > 0) { + this.DrawChunks(message.Sender); + ImGui.SameLine(); + } + + this.DrawChunks(message.Content); + + // drawnHeight += ImGui.GetCursorPosY() - lastPos; + // lastPos = ImGui.GetCursorPosY(); + } + + // numMessages = tab.Messages.Count; + // may render too many items, but this is easier + // clipper.Begin(numMessages, lineHeight + ImGui.GetStyle().ItemSpacing.Y); + // while (clipper.Step()) { + // } + } finally { + tab.MessagesMutex.ReleaseMutex(); + ImGui.PopStyleVar(); + } + + // PluginLog.Log($"numDrawn: {numDrawn}"); + + if (switchedTab || ImGui.GetScrollY() >= ImGui.GetScrollMaxY()) { + // PluginLog.Log($"drawnHeight: {drawnHeight}"); + // var itemPosY = clipper.StartPosY + drawnHeight; + // PluginLog.Log($"itemPosY: {itemPosY}"); + // ImGui.SetScrollFromPosY(itemPosY - ImGui.GetWindowPos().Y); + ImGui.SetScrollHereY(1f); + } + } + + ImGui.EndChild(); + + ImGui.EndTabItem(); + } + } + + ImGui.EndTabBar(); + } + + if (this._activate) { + ImGui.SetKeyboardFocusHere(); + } + + ImGui.TextUnformatted(this.Ui.Plugin.Functions.ChatChannel.name); + + var inputType = this.Ui.Plugin.Functions.ChatChannel.channel.ToChatType(); + var inputColour = this.Ui.Plugin.Config.ChatColours.TryGetValue(inputType, out var inputCol) + ? inputCol + : inputType.DefaultColour(); + + if (inputColour != null) { + ImGui.PushStyleColor(ImGuiCol.Text, ColourUtil.RgbaToAbgr(inputColour.Value)); + } + + ImGui.SetNextItemWidth(-1); + const ImGuiInputTextFlags inputFlags = ImGuiInputTextFlags.EnterReturnsTrue + | ImGuiInputTextFlags.CallbackAlways + | ImGuiInputTextFlags.CallbackHistory; + if (ImGui.InputText("##chat2-input", ref this._chat, 500, inputFlags, this.Callback)) { + if (!string.IsNullOrWhiteSpace(this._chat)) { + var trimmed = this._chat.Trim(); + this.AddBacklog(trimmed); + this._inputBacklogIdx = -1; + + this.Ui.Plugin.Common.Functions.Chat.SendMessage(trimmed); + } + + this._chat = string.Empty; + } + + if (inputColour != null) { + ImGui.PopStyleColor(); + } + + ImGui.End(); + } + + private unsafe int Callback(ImGuiInputTextCallbackData* data) { + var ptr = new ImGuiInputTextCallbackDataPtr(data); + + if (this._activate) { + this._activate = false; + data->CursorPos = this._chat.Length; + data->SelectionStart = data->SelectionEnd = data->CursorPos; + } + + if (data->EventFlag != ImGuiInputTextFlags.CallbackHistory) { + return 0; + } + + var prevPos = this._inputBacklogIdx; + + switch (data->EventKey) { + case ImGuiKey.UpArrow: + switch (this._inputBacklogIdx) { + case -1: + var offset = 0; + + if (!string.IsNullOrWhiteSpace(this._chat)) { + this.AddBacklog(this._chat); + offset = 1; + } + + this._inputBacklogIdx = this._inputBacklog.Count - 1 - offset; + break; + case > 0: + this._inputBacklogIdx--; + break; + } + + break; + case ImGuiKey.DownArrow: { + if (this._inputBacklogIdx != -1) { + if (++this._inputBacklogIdx >= this._inputBacklog.Count) { + this._inputBacklogIdx = -1; + } + } + + break; + } + } + + if (prevPos == this._inputBacklogIdx) { + return 0; + } + + var historyStr = this._inputBacklogIdx >= 0 ? this._inputBacklog[this._inputBacklogIdx] : string.Empty; + + ptr.DeleteChars(0, ptr.BufTextLen); + ptr.InsertChars(0, historyStr); + + return 0; + } + + private void DrawChunks(IReadOnlyList chunks) { + for (var i = 0; i < chunks.Count; i++) { + this.DrawChunk(chunks[i]); + + if (i < chunks.Count - 1) { + ImGui.SameLine(); + } + } + } + + private void DrawChunk(Chunk chunk) { + if (chunk is IconChunk icon && this._fontIcon != null) { + var bounds = IconUtil.GetBounds((byte) icon.Icon); + if (bounds != null) { + var texSize = new Vector2(this._fontIcon.Width, this._fontIcon.Height); + + var sizeRatio = this.Ui.Plugin.Config.FontSize / bounds.Value.W; + var size = new Vector2(bounds.Value.Z, bounds.Value.W) * sizeRatio; + + var uv0 = new Vector2(bounds.Value.X, bounds.Value.Y - 2) / texSize; + var uv1 = new Vector2(bounds.Value.X + bounds.Value.Z, bounds.Value.Y - 2 + bounds.Value.W) / texSize; + ImGui.Image(this._fontIcon.ImGuiHandle, size, uv0, uv1); + } + + return; + } + + if (chunk is not TextChunk text) { + return; + } + + var colour = text.Foreground; + if (colour == null && text.FallbackColour != null) { + var type = text.FallbackColour.Value; + colour = this.Ui.Plugin.Config.ChatColours.TryGetValue(type, out var col) + ? col + : type.DefaultColour(); + } + + if (colour != null) { + colour = ColourUtil.RgbaToAbgr(colour.Value); + ImGui.PushStyleColor(ImGuiCol.Text, colour.Value); + } + + if (text.Italic && this.Ui.ItalicFont.HasValue) { + ImGui.PushFont(this.Ui.ItalicFont.Value); + } + + ImGuiUtil.WrapText(text.Content); + + if (text.Italic && this.Ui.ItalicFont.HasValue) { + ImGui.PopFont(); + } + + if (colour != null) { + ImGui.PopStyleColor(); + } + } +} diff --git a/ChatTwo/Ui/Settings.cs b/ChatTwo/Ui/Settings.cs new file mode 100755 index 0000000..e45d5ed --- /dev/null +++ b/ChatTwo/Ui/Settings.cs @@ -0,0 +1,185 @@ +using System.Numerics; +using ChatTwo.Code; +using ChatTwo.Util; +using Dalamud.Game.Command; +using ImGuiNET; + +namespace ChatTwo.Ui; + +internal sealed class Settings : IUiComponent { + private PluginUi Ui { get; } + + private bool _visible; + + private bool _hideChat; + private float _fontSize; + private Dictionary _chatColours = new(); + private List _tabs = new(); + + internal Settings(PluginUi ui) { + this.Ui = ui; + this.Ui.Plugin.CommandManager.AddHandler("/chat2", new CommandInfo(this.Command) { + HelpMessage = "Toggle the Chat 2 settings", + }); + } + + public void Dispose() { + this.Ui.Plugin.CommandManager.RemoveHandler("/chat2"); + } + + private void Command(string command, string args) { + this._visible ^= true; + } + + private void Initialise() { + var config = this.Ui.Plugin.Config; + this._hideChat = config.HideChat; + this._fontSize = config.FontSize; + this._chatColours = config.ChatColours.ToDictionary(entry => entry.Key, entry => entry.Value); + this._tabs = config.Tabs.Select(tab => tab.Clone()).ToList(); + } + + public void Draw() { + if (!this._visible) { + return; + } + + if (!ImGui.Begin($"{this.Ui.Plugin.Name} settings", ref this._visible)) { + ImGui.End(); + return; + } + + if (ImGui.IsWindowAppearing()) { + this.Initialise(); + } + + var height = ImGui.GetContentRegionAvail().Y + - ImGui.GetStyle().FramePadding.Y * 2 + - ImGui.GetStyle().ItemSpacing.Y + - ImGui.GetStyle().ItemInnerSpacing.Y * 2 + - ImGui.CalcTextSize("A").Y; + if (ImGui.BeginChild("##chat2-settings", new Vector2(-1, height))) { + ImGui.Checkbox("Hide chat", ref this._hideChat); + ImGui.DragFloat("Font size", ref this._fontSize, .5f, 12f, 36f); + + if (ImGui.TreeNodeEx("Chat colours")) { + foreach (var type in Enum.GetValues()) { + if (ImGui.Button($"Default##{type}")) { + this._chatColours.Remove(type); + } + + ImGui.SameLine(); + + var vec = this._chatColours.TryGetValue(type, out var colour) + ? ColourUtil.RgbaToVector3(colour) + : ColourUtil.RgbaToVector3(type.DefaultColour() ?? 0); + if (ImGui.ColorEdit3(type.Name(), ref vec, ImGuiColorEditFlags.NoInputs)) { + this._chatColours[type] = ColourUtil.Vector3ToRgba(vec); + } + } + + ImGui.TreePop(); + } + + if (ImGui.TreeNodeEx("Tabs")) { + if (ImGui.Button("Add")) { + this._tabs.Add(new Tab()); + } + + for (var i = 0; i < this._tabs.Count; i++) { + var tab = this._tabs[i]; + + if (ImGui.TreeNodeEx($"{tab.Name}###tab-{i}")) { + ImGui.PushID($"tab-{i}"); + + ImGui.InputText("Name", ref tab.Name, 512, ImGuiInputTextFlags.EnterReturnsTrue); + ImGui.Checkbox("Show unread count", ref tab.DisplayUnread); + ImGui.Checkbox("Show timestamps", ref tab.DisplayTimestamp); + + if (ImGui.TreeNodeEx("Channels")) { + foreach (var type in Enum.GetValues()) { + var enabled = tab.ChatCodes.ContainsKey(type); + if (ImGui.Checkbox($"##{type.Name()}-{i}", ref enabled)) { + if (enabled) { + tab.ChatCodes[type] = ChatSourceExt.All; + } else { + tab.ChatCodes.Remove(type); + } + } + + ImGui.SameLine(); + + if (ImGui.TreeNodeEx($"{type.Name()}##{i}")) { + tab.ChatCodes.TryGetValue(type, out var sourcesEnum); + var sources = (uint) sourcesEnum; + + foreach (var source in Enum.GetValues()) { + if (ImGui.CheckboxFlags(source.ToString(), ref sources, (uint) source)) { + tab.ChatCodes[type] = (ChatSource) sources; + } + } + + ImGui.TreePop(); + } + } + + + ImGui.TreePop(); + } + + ImGui.TreePop(); + + ImGui.PopID(); + } + } + } + + ImGui.EndChild(); + } + + ImGui.Separator(); + + var save = ImGui.Button("Save"); + + ImGui.SameLine(); + + if (ImGui.Button("Save and close")) { + save = true; + this._visible = false; + } + + ImGui.SameLine(); + + if (ImGui.Button("Discard")) { + this._visible = false; + } + + ImGui.End(); + + if (save) { + var config = this.Ui.Plugin.Config; + + var hideChatChanged = this._hideChat != this.Ui.Plugin.Config.HideChat; + var fontSizeChanged = Math.Abs(this._fontSize - this.Ui.Plugin.Config.FontSize) > float.Epsilon; + + config.HideChat = this._hideChat; + config.FontSize = this._fontSize; + config.ChatColours = this._chatColours; + config.Tabs = this._tabs; + + this.Ui.Plugin.SaveConfig(); + + this.Ui.Plugin.Store.FilterAllTabs(); + + if (fontSizeChanged) { + this.Ui.Plugin.Interface.UiBuilder.RebuildFonts(); + } + + if (!this._hideChat && hideChatChanged) { + this.Ui.Plugin.Functions.SetChatInteractable(true); + } + + this.Initialise(); + } + } +} diff --git a/ChatTwo/Ui/UiComponent.cs b/ChatTwo/Ui/UiComponent.cs new file mode 100755 index 0000000..38d3067 --- /dev/null +++ b/ChatTwo/Ui/UiComponent.cs @@ -0,0 +1,5 @@ +namespace ChatTwo.Ui; + +internal interface IUiComponent : IDisposable { + void Draw(); +} diff --git a/ChatTwo/Util/ChunkUtil.cs b/ChatTwo/Util/ChunkUtil.cs new file mode 100755 index 0000000..59c69ae --- /dev/null +++ b/ChatTwo/Util/ChunkUtil.cs @@ -0,0 +1,76 @@ +using ChatTwo.Code; +using Dalamud.Game.Text.SeStringHandling; +using Dalamud.Game.Text.SeStringHandling.Payloads; + +namespace ChatTwo.Util; + +internal static class ChunkUtil { + internal static IEnumerable ToChunks(SeString msg, ChatType? defaultColour) { + var chunks = new List(); + + var italic = false; + var foreground = new Stack(); + var glow = new Stack(); + + void Append(string text) { + chunks.Add(new TextChunk(text) { + FallbackColour = defaultColour, + Foreground = foreground.Count > 0 ? foreground.Peek() : null, + Glow = glow.Count > 0 ? glow.Peek() : null, + Italic = italic, + }); + } + + 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(BitmapFontIcon.AutoTranslateBegin)); + var autoText = ((AutoTranslatePayload) payload).Text; + Append(autoText.Substring(2, autoText.Length - 4)); + chunks.Add(new IconChunk(BitmapFontIcon.AutoTranslateEnd)); + break; + case PayloadType.Icon: + chunks.Add(new IconChunk(((IconPayload) payload).Icon)); + break; + case PayloadType.Unknown: + var rawPayload = (RawPayload) payload; + if (rawPayload.Data[1] == 0x13) { + foreground.Pop(); + glow.Pop(); + } + + break; + default: + if (payload is ITextProvider textProvider) { + Append(textProvider.Text); + } + + break; + } + } + + return chunks; + } +} diff --git a/ChatTwo/Util/ColourUtil.cs b/ChatTwo/Util/ColourUtil.cs new file mode 100755 index 0000000..d4b8894 --- /dev/null +++ b/ChatTwo/Util/ColourUtil.cs @@ -0,0 +1,36 @@ +using System.Numerics; + +namespace ChatTwo.Util; + +internal static class ColourUtil { + private static (byte r, byte g, byte b, byte a) RgbaToComponents(uint rgba) { + var r = (byte) ((rgba & 0xFF000000) >> 24); + var g = (byte) ((rgba & 0xFF0000) >> 16); + var b = (byte) ((rgba & 0xFF00) >> 8); + var a = (byte) (rgba & 0xFF); + return (r, g, b, a); + } + + internal static uint RgbaToAbgr(uint rgba) { + var (r, g, b, a) = RgbaToComponents(rgba); + return (uint) ((a << 24) | (b << 16) | (g << 8) | r); + } + + internal static Vector3 RgbaToVector3(uint rgba) { + var (r, g, b, _) = RgbaToComponents(rgba); + return new Vector3((float) r / 255, (float) g / 255, (float) b / 255); + } + + internal static uint Vector3ToRgba(Vector3 col) { + return ComponentsToRgba( + (byte) Math.Round(col.X * 255), + (byte) Math.Round(col.Y * 255), + (byte) Math.Round(col.Z * 255) + ); + } + + internal static uint ComponentsToRgba(byte red, byte green, byte blue, byte alpha = 0xFF) => alpha + | (uint) (red << 24) + | (uint) (green << 16) + | (uint) (blue << 8); +} diff --git a/ChatTwo/Util/IconUtil.cs b/ChatTwo/Util/IconUtil.cs new file mode 100755 index 0000000..d62366e --- /dev/null +++ b/ChatTwo/Util/IconUtil.cs @@ -0,0 +1,95 @@ +using System.Numerics; + +namespace ChatTwo.Util; + +internal static class IconUtil { + internal static Vector4? GetBounds(byte id) => id switch { + 1 => new Vector4(0, 342, 40, 40), + 2 => new Vector4(40, 342, 40, 40), + 3 => new Vector4(80, 342, 40, 40), + 4 => new Vector4(120, 342, 40, 40), + 5 => new Vector4(160, 342, 40, 40), + 6 => new Vector4(0, 382, 40, 40), + 7 => new Vector4(40, 382, 40, 40), + 8 => new Vector4(80, 382, 40, 40), + 9 => new Vector4(120, 382, 40, 40), + 10 => new Vector4(160, 382, 40, 40), + 11 => new Vector4(0, 422, 40, 40), + 12 => new Vector4(40, 422, 40, 40), + 13 => new Vector4(80, 422, 40, 40), + 14 => new Vector4(120, 422, 40, 40), + 15 => new Vector4(160, 422, 40, 40), + 16 => new Vector4(120, 542, 40, 40), + 17 => new Vector4(160, 542, 40, 40), + 18 => new Vector4(0, 462, 108, 40), + 19 => new Vector4(108, 462, 108, 40), + 20 => new Vector4(120, 502, 40, 40), + 21 => new Vector4(0, 502, 56, 40), + 22 => new Vector4(56, 502, 64, 40), + 23 => new Vector4(160, 502, 40, 40), + 24 => new Vector4(0, 542, 56, 40), + 25 => new Vector4(56, 542, 64, 40), + 51 => new Vector4(248, 342, 40, 40), + 52 => new Vector4(288, 342, 40, 40), + 53 => new Vector4(328, 342, 40, 40), + 54 => new Vector4(200, 342, 24, 40), + 55 => new Vector4(224, 342, 24, 40), + 56 => new Vector4(200, 382, 40, 40), + 57 => new Vector4(240, 382, 40, 40), + 58 => new Vector4(280, 382, 40, 40), + 59 => new Vector4(200, 422, 40, 40), + 60 => new Vector4(240, 422, 40, 40), + 61 => new Vector4(280, 422, 40, 40), + 62 => new Vector4(320, 382, 40, 40), + 63 => new Vector4(320, 422, 40, 40), + 64 => new Vector4(368, 342, 40, 40), + 65 => new Vector4(408, 342, 40, 40), + 66 => new Vector4(448, 342, 40, 40), + 67 => new Vector4(360, 382, 40, 40), + 68 => new Vector4(400, 382, 40, 40), + 70 => new Vector4(360, 422, 40, 40), + 71 => new Vector4(400, 422, 40, 40), + 72 => new Vector4(440, 422, 40, 40), + 73 => new Vector4(440, 382, 40, 40), + 74 => new Vector4(216, 462, 40, 40), + 75 => new Vector4(256, 462, 40, 40), + 76 => new Vector4(296, 462, 40, 40), + 77 => new Vector4(336, 462, 40, 40), + 78 => new Vector4(376, 462, 40, 40), + 79 => new Vector4(416, 462, 40, 40), + 80 => new Vector4(456, 462, 40, 40), + 81 => new Vector4(200, 502, 40, 40), + 82 => new Vector4(240, 502, 40, 40), + 83 => new Vector4(280, 502, 40, 40), + 84 => new Vector4(320, 502, 40, 40), + 85 => new Vector4(360, 502, 40, 40), + 86 => new Vector4(400, 502, 40, 40), + 87 => new Vector4(440, 502, 40, 40), + 88 => new Vector4(200, 542, 40, 40), + 89 => new Vector4(240, 542, 40, 40), + 90 => new Vector4(280, 542, 40, 40), + 91 => new Vector4(320, 542, 40, 40), + 92 => new Vector4(360, 542, 40, 40), + 93 => new Vector4(400, 542, 40, 40), + 94 => new Vector4(440, 542, 40, 40), + 95 => new Vector4(0, 582, 40, 40), + 96 => new Vector4(40, 582, 40, 40), + 97 => new Vector4(80, 582, 40, 40), + 98 => new Vector4(120, 582, 40, 40), + 99 => new Vector4(160, 582, 40, 40), + 100 => new Vector4(200, 582, 40, 40), + 101 => new Vector4(240, 582, 40, 40), + 102 => new Vector4(280, 582, 40, 40), + 103 => new Vector4(320, 582, 40, 40), + 104 => new Vector4(360, 582, 40, 40), + 105 => new Vector4(400, 582, 40, 40), + 106 => new Vector4(440, 582, 40, 40), + 107 => new Vector4(0, 622, 40, 40), + 108 => new Vector4(40, 622, 40, 40), + 109 => new Vector4(80, 622, 40, 40), + 110 => new Vector4(120, 622, 40, 40), + 111 => new Vector4(160, 622, 40, 40), + 112 => new Vector4(200, 622, 40, 40), + _ => null, + }; +} diff --git a/ChatTwo/Util/ImGuiUtil.cs b/ChatTwo/Util/ImGuiUtil.cs new file mode 100755 index 0000000..0b5ece3 --- /dev/null +++ b/ChatTwo/Util/ImGuiUtil.cs @@ -0,0 +1,45 @@ +using System.Text; +using ImGuiNET; + +namespace ChatTwo.Util; + +internal static class ImGuiUtil { + internal static unsafe void WrapText(string csText) { + foreach (var part in csText.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None)) { + var bytes = Encoding.UTF8.GetBytes(part); + fixed (byte* rawText = bytes) { + var text = rawText; + var textEnd = text + bytes.Length; + + // idk how this is possible, but it is, I guess + if (text == null) { + return; + } + + const float scale = 1.0f; + var widthLeft = ImGui.GetContentRegionAvail().X; + var endPrevLine = ImGuiNative.ImFont_CalcWordWrapPositionA(ImGui.GetFont().NativePtr, scale, text, textEnd, widthLeft); + if (endPrevLine == null) { + return; + } + + ImGuiNative.igTextUnformatted(text, endPrevLine); + + widthLeft = ImGui.GetContentRegionAvail().X; + while (endPrevLine < textEnd) { + text = endPrevLine; + if (*text == ' ') { + ++text; + } // skip a space at start of line + + endPrevLine = ImGuiNative.ImFont_CalcWordWrapPositionA(ImGui.GetFont().NativePtr, scale, text, textEnd, widthLeft); + if (endPrevLine == null) { + break; + } + + ImGuiNative.igTextUnformatted(text, endPrevLine); + } + } + } + } +} diff --git a/ChatTwo/fonts/NotoSans-Italic.ttf b/ChatTwo/fonts/NotoSans-Italic.ttf new file mode 100755 index 0000000..27ff1ed Binary files /dev/null and b/ChatTwo/fonts/NotoSans-Italic.ttf differ diff --git a/ChatTwo/fonts/NotoSans-Regular.ttf b/ChatTwo/fonts/NotoSans-Regular.ttf new file mode 100755 index 0000000..10589e2 Binary files /dev/null and b/ChatTwo/fonts/NotoSans-Regular.ttf differ diff --git a/ChatTwo/fonts/NotoSansJP-Regular.otf b/ChatTwo/fonts/NotoSansJP-Regular.otf new file mode 100755 index 0000000..5791298 Binary files /dev/null and b/ChatTwo/fonts/NotoSansJP-Regular.otf differ diff --git a/LICENCE b/LICENCE new file mode 100644 index 0000000..4153cd3 --- /dev/null +++ b/LICENCE @@ -0,0 +1,287 @@ + EUROPEAN UNION PUBLIC LICENCE v. 1.2 + EUPL © the European Union 2007, 2016 + +This European Union Public Licence (the ‘EUPL’) applies to the Work (as defined +below) which is provided under the terms of this Licence. Any use of the Work, +other than as authorised under this Licence is prohibited (to the extent such +use is covered by a right of the copyright holder of the Work). + +The Work is provided under the terms of this Licence when the Licensor (as +defined below) has placed the following notice immediately following the +copyright notice for the Work: + + Licensed under the EUPL + +or has expressed by any other means his willingness to license under the EUPL. + +1. Definitions + +In this Licence, the following terms have the following meaning: + +- ‘The Licence’: this Licence. + +- ‘The Original Work’: the work or software distributed or communicated by the + Licensor under this Licence, available as Source Code and also as Executable + Code as the case may be. + +- ‘Derivative Works’: the works or software that could be created by the + Licensee, based upon the Original Work or modifications thereof. This Licence + does not define the extent of modification or dependence on the Original Work + required in order to classify a work as a Derivative Work; this extent is + determined by copyright law applicable in the country mentioned in Article 15. + +- ‘The Work’: the Original Work or its Derivative Works. + +- ‘The Source Code’: the human-readable form of the Work which is the most + convenient for people to study and modify. + +- ‘The Executable Code’: any code which has generally been compiled and which is + meant to be interpreted by a computer as a program. + +- ‘The Licensor’: the natural or legal person that distributes or communicates + the Work under the Licence. + +- ‘Contributor(s)’: any natural or legal person who modifies the Work under the + Licence, or otherwise contributes to the creation of a Derivative Work. + +- ‘The Licensee’ or ‘You’: any natural or legal person who makes any usage of + the Work under the terms of the Licence. + +- ‘Distribution’ or ‘Communication’: any act of selling, giving, lending, + renting, distributing, communicating, transmitting, or otherwise making + available, online or offline, copies of the Work or providing access to its + essential functionalities at the disposal of any other natural or legal + person. + +2. Scope of the rights granted by the Licence + +The Licensor hereby grants You a worldwide, royalty-free, non-exclusive, +sublicensable licence to do the following, for the duration of copyright vested +in the Original Work: + +- use the Work in any circumstance and for all usage, +- reproduce the Work, +- modify the Work, and make Derivative Works based upon the Work, +- communicate to the public, including the right to make available or display + the Work or copies thereof to the public and perform publicly, as the case may + be, the Work, +- distribute the Work or copies thereof, +- lend and rent the Work or copies thereof, +- sublicense rights in the Work or copies thereof. + +Those rights can be exercised on any media, supports and formats, whether now +known or later invented, as far as the applicable law permits so. + +In the countries where moral rights apply, the Licensor waives his right to +exercise his moral right to the extent allowed by law in order to make effective +the licence of the economic rights here above listed. + +The Licensor grants to the Licensee royalty-free, non-exclusive usage rights to +any patents held by the Licensor, to the extent necessary to make use of the +rights granted on the Work under this Licence. + +3. Communication of the Source Code + +The Licensor may provide the Work either in its Source Code form, or as +Executable Code. If the Work is provided as Executable Code, the Licensor +provides in addition a machine-readable copy of the Source Code of the Work +along with each copy of the Work that the Licensor distributes or indicates, in +a notice following the copyright notice attached to the Work, a repository where +the Source Code is easily and freely accessible for as long as the Licensor +continues to distribute or communicate the Work. + +4. Limitations on copyright + +Nothing in this Licence is intended to deprive the Licensee of the benefits from +any exception or limitation to the exclusive rights of the rights owners in the +Work, of the exhaustion of those rights or of other applicable limitations +thereto. + +5. Obligations of the Licensee + +The grant of the rights mentioned above is subject to some restrictions and +obligations imposed on the Licensee. Those obligations are the following: + +Attribution right: The Licensee shall keep intact all copyright, patent or +trademarks notices and all notices that refer to the Licence and to the +disclaimer of warranties. The Licensee must include a copy of such notices and a +copy of the Licence with every copy of the Work he/she distributes or +communicates. The Licensee must cause any Derivative Work to carry prominent +notices stating that the Work has been modified and the date of modification. + +Copyleft clause: If the Licensee distributes or communicates copies of the +Original Works or Derivative Works, this Distribution or Communication will be +done under the terms of this Licence or of a later version of this Licence +unless the Original Work is expressly distributed only under this version of the +Licence — for example by communicating ‘EUPL v. 1.2 only’. The Licensee +(becoming Licensor) cannot offer or impose any additional terms or conditions on +the Work or Derivative Work that alter or restrict the terms of the Licence. + +Compatibility clause: If the Licensee Distributes or Communicates Derivative +Works or copies thereof based upon both the Work and another work licensed under +a Compatible Licence, this Distribution or Communication can be done under the +terms of this Compatible Licence. For the sake of this clause, ‘Compatible +Licence’ refers to the licences listed in the appendix attached to this Licence. +Should the Licensee's obligations under the Compatible Licence conflict with +his/her obligations under this Licence, the obligations of the Compatible +Licence shall prevail. + +Provision of Source Code: When distributing or communicating copies of the Work, +the Licensee will provide a machine-readable copy of the Source Code or indicate +a repository where this Source will be easily and freely available for as long +as the Licensee continues to distribute or communicate the Work. + +Legal Protection: This Licence does not grant permission to use the trade names, +trademarks, service marks, or names of the Licensor, except as required for +reasonable and customary use in describing the origin of the Work and +reproducing the content of the copyright notice. + +6. Chain of Authorship + +The original Licensor warrants that the copyright in the Original Work granted +hereunder is owned by him/her or licensed to him/her and that he/she has the +power and authority to grant the Licence. + +Each Contributor warrants that the copyright in the modifications he/she brings +to the Work are owned by him/her or licensed to him/her and that he/she has the +power and authority to grant the Licence. + +Each time You accept the Licence, the original Licensor and subsequent +Contributors grant You a licence to their contributions to the Work, under the +terms of this Licence. + +7. Disclaimer of Warranty + +The Work is a work in progress, which is continuously improved by numerous +Contributors. It is not a finished work and may therefore contain defects or +‘bugs’ inherent to this type of development. + +For the above reason, the Work is provided under the Licence on an ‘as is’ basis +and without warranties of any kind concerning the Work, including without +limitation merchantability, fitness for a particular purpose, absence of defects +or errors, accuracy, non-infringement of intellectual property rights other than +copyright as stated in Article 6 of this Licence. + +This disclaimer of warranty is an essential part of the Licence and a condition +for the grant of any rights to the Work. + +8. Disclaimer of Liability + +Except in the cases of wilful misconduct or damages directly caused to natural +persons, the Licensor will in no event be liable for any direct or indirect, +material or moral, damages of any kind, arising out of the Licence or of the use +of the Work, including without limitation, damages for loss of goodwill, work +stoppage, computer failure or malfunction, loss of data or any commercial +damage, even if the Licensor has been advised of the possibility of such damage. +However, the Licensor will be liable under statutory product liability laws as +far such laws apply to the Work. + +9. Additional agreements + +While distributing the Work, You may choose to conclude an additional agreement, +defining obligations or services consistent with this Licence. However, if +accepting obligations, You may act only on your own behalf and on your sole +responsibility, not on behalf of the original Licensor or any other Contributor, +and only if You agree to indemnify, defend, and hold each Contributor harmless +for any liability incurred by, or claims asserted against such Contributor by +the fact You have accepted any warranty or additional liability. + +10. Acceptance of the Licence + +The provisions of this Licence can be accepted by clicking on an icon ‘I agree’ +placed under the bottom of a window displaying the text of this Licence or by +affirming consent in any other similar way, in accordance with the rules of +applicable law. Clicking on that icon indicates your clear and irrevocable +acceptance of this Licence and all of its terms and conditions. + +Similarly, you irrevocably accept this Licence and all of its terms and +conditions by exercising any rights granted to You by Article 2 of this Licence, +such as the use of the Work, the creation by You of a Derivative Work or the +Distribution or Communication by You of the Work or copies thereof. + +11. Information to the public + +In case of any Distribution or Communication of the Work by means of electronic +communication by You (for example, by offering to download the Work from a +remote location) the distribution channel or media (for example, a website) must +at least provide to the public the information requested by the applicable law +regarding the Licensor, the Licence and the way it may be accessible, concluded, +stored and reproduced by the Licensee. + +12. Termination of the Licence + +The Licence and the rights granted hereunder will terminate automatically upon +any breach by the Licensee of the terms of the Licence. + +Such a termination will not terminate the licences of any person who has +received the Work from the Licensee under the Licence, provided such persons +remain in full compliance with the Licence. + +13. Miscellaneous + +Without prejudice of Article 9 above, the Licence represents the complete +agreement between the Parties as to the Work. + +If any provision of the Licence is invalid or unenforceable under applicable +law, this will not affect the validity or enforceability of the Licence as a +whole. Such provision will be construed or reformed so as necessary to make it +valid and enforceable. + +The European Commission may publish other linguistic versions or new versions of +this Licence or updated versions of the Appendix, so far this is required and +reasonable, without reducing the scope of the rights granted by the Licence. New +versions of the Licence will be published with a unique version number. + +All linguistic versions of this Licence, approved by the European Commission, +have identical value. Parties can take advantage of the linguistic version of +their choice. + +14. Jurisdiction + +Without prejudice to specific agreement between parties, + +- any litigation resulting from the interpretation of this License, arising + between the European Union institutions, bodies, offices or agencies, as a + Licensor, and any Licensee, will be subject to the jurisdiction of the Court + of Justice of the European Union, as laid down in article 272 of the Treaty on + the Functioning of the European Union, + +- any litigation arising between other parties and resulting from the + interpretation of this License, will be subject to the exclusive jurisdiction + of the competent court where the Licensor resides or conducts its primary + business. + +15. Applicable Law + +Without prejudice to specific agreement between parties, + +- this Licence shall be governed by the law of the European Union Member State + where the Licensor has his seat, resides or has his registered office, + +- this licence shall be governed by Belgian law if the Licensor has no seat, + residence or registered office inside a European Union Member State. + +Appendix + +‘Compatible Licences’ according to Article 5 EUPL are: + +- GNU General Public License (GPL) v. 2, v. 3 +- GNU Affero General Public License (AGPL) v. 3 +- Open Software License (OSL) v. 2.1, v. 3.0 +- Eclipse Public License (EPL) v. 1.0 +- CeCILL v. 2.0, v. 2.1 +- Mozilla Public Licence (MPL) v. 2 +- GNU Lesser General Public Licence (LGPL) v. 2.1, v. 3 +- Creative Commons Attribution-ShareAlike v. 3.0 Unported (CC BY-SA 3.0) for + works other than software +- European Union Public Licence (EUPL) v. 1.1, v. 1.2 +- Québec Free and Open-Source Licence — Reciprocity (LiLiQ-R) or Strong + Reciprocity (LiLiQ-R+). + +The European Commission may update this Appendix to later versions of the above +licences without producing a new version of the EUPL, as long as they provide +the rights granted in Article 2 of this Licence and protect the covered Source +Code from exclusive appropriation. + +All other changes or additions to this Appendix require the production of a new +EUPL version. diff --git a/global.json b/global.json new file mode 100755 index 0000000..cbde930 --- /dev/null +++ b/global.json @@ -0,0 +1,7 @@ +{ + "sdk": { + "version": "5.0.0", + "rollForward": "latestMajor", + "allowPrerelease": true + } +} \ No newline at end of file