feat: send and receive emote data

This commit is contained in:
Anna 2024-07-22 03:44:30 -04:00
parent fd0ca7f445
commit a08c086323
Signed by: anna
GPG Key ID: D0943384CD9F87D1
12 changed files with 273 additions and 71 deletions

View File

@ -6,31 +6,30 @@ namespace OrangeGuidanceTomestone;
[Serializable]
[JsonObject(NamingStrategyType = typeof(SnakeCaseNamingStrategy))]
internal class Message {
public Guid Id { get; init; }
public float X { get; init; }
public float Y { get; init; }
public float Z { get; init; }
public float Yaw { get; init; }
public class Message {
public required Guid Id { get; init; }
public required float X { get; init; }
public required float Y { get; init; }
public required float Z { get; init; }
public required float Yaw { get; init; }
[JsonProperty("message")]
public string Text { get; init; }
public required string Text { get; init; }
public int PositiveVotes { get; set; }
public int NegativeVotes { get; set; }
public int UserVote { get; set; }
public required int PositiveVotes { get; set; }
public required int NegativeVotes { get; set; }
public required int UserVote { get; set; }
public uint? Emote { get; set; }
public byte[]? Customise { get; set; }
public required EmoteData? Emote { get; set; }
public int Glyph { get; set; }
public required int Glyph { get; set; }
internal Vector3 Position => new(this.X, this.Y, this.Z);
}
[Serializable]
[JsonObject(NamingStrategyType = typeof(SnakeCaseNamingStrategy))]
internal class MessageWithTerritory {
public class MessageWithTerritory {
public Guid Id { get; init; }
public uint Territory { get; init; }
public uint? Ward { get; init; }
@ -48,8 +47,7 @@ internal class MessageWithTerritory {
public int NegativeVotes { get; init; }
public int UserVote { get; set; }
public uint? Emote { get; set; }
public byte[]? Customise { get; set; }
public EmoteData? Emote { get; set; }
public int Glyph { get; set; }
public bool IsHidden { get; set; }
@ -69,7 +67,6 @@ internal class MessageWithTerritory {
NegativeVotes = message.NegativeVotes,
UserVote = message.UserVote,
Emote = message.Emote,
Customise = message.Customise,
Glyph = message.Glyph,
IsHidden = false,
};
@ -78,56 +75,57 @@ internal class MessageWithTerritory {
[Serializable]
[JsonObject(NamingStrategyType = typeof(SnakeCaseNamingStrategy))]
internal class EmoteData {
public uint Id { get; set; }
public byte[] Customise { get; set; }
public EquipmentData[] Equipment { get; set; }
public WeaponData[] Weapon { get; set; }
public bool HatHidden { get; set; }
public bool VisorToggled { get; set; }
public bool WeaponHidden { get; set; }
public class EmoteData {
public required uint Id { get; set; }
public required byte[] Customise { get; set; }
public required EquipmentData[] Equipment { get; set; }
public required WeaponData[] Weapon { get; set; }
public required uint Glasses { get; set; }
public required bool HatHidden { get; set; }
public required bool VisorToggled { get; set; }
public required bool WeaponHidden { get; set; }
}
[Serializable]
[JsonObject(NamingStrategyType = typeof(SnakeCaseNamingStrategy))]
internal class EquipmentData {
public ushort Id { get; set; }
public byte Variant { get; set; }
public byte Stain0 { get; set; }
public byte Stain1 { get; set; }
public ulong Value { get; set; }
public class EquipmentData {
public required ushort Id { get; set; }
public required byte Variant { get; set; }
public required byte Stain0 { get; set; }
public required byte Stain1 { get; set; }
public required ulong Value { get; set; }
}
[Serializable]
[JsonObject(NamingStrategyType = typeof(SnakeCaseNamingStrategy))]
internal class WeaponData {
public WeaponModelId ModelId { get; set; }
public byte State { get; set; }
public ushort Flags1 { get; set; }
public byte Flags2 { get; set; }
public class WeaponData {
public required WeaponModelId ModelId { get; set; }
public required byte State { get; set; }
public required ushort Flags1 { get; set; }
public required byte Flags2 { get; set; }
}
[Serializable]
[JsonObject(NamingStrategyType = typeof(SnakeCaseNamingStrategy))]
internal class WeaponModelId {
public ushort Id { get; set; }
public ushort Type { get; set; }
public ushort Variant { get; set; }
public byte Stain0 { get; set; }
public byte Stain1 { get; set; }
public ulong Value { get; set; }
public class WeaponModelId {
public required ushort Id { get; set; }
public required ushort Kind { get; set; }
public required ushort Variant { get; set; }
public required byte Stain0 { get; set; }
public required byte Stain1 { get; set; }
public required ulong Value { get; set; }
}
[Serializable]
[JsonObject(NamingStrategyType = typeof(SnakeCaseNamingStrategy))]
internal class ErrorMessage {
public class ErrorMessage {
public string Code { get; set; }
public string Message { get; set; }
}
[Serializable]
[JsonObject(NamingStrategyType = typeof(SnakeCaseNamingStrategy))]
internal class MyMessages {
public class MyMessages {
public uint Extra { get; set; }
public MessageWithTerritory[] Messages { get; set; }
}

View File

@ -6,35 +6,36 @@ namespace OrangeGuidanceTomestone;
[Serializable]
[JsonObject(NamingStrategyType = typeof(SnakeCaseNamingStrategy))]
public class MessageRequest {
public uint Territory { get; set; }
public uint? World { get; set; }
public uint? Ward { get; set; }
public uint? Plot { get; set; }
public float X { get; set; }
public float Y { get; set; }
public float Z { get; set; }
public float Yaw { get; set; }
public Guid PackId { get; set; }
public required uint Territory { get; set; }
public required uint? World { get; set; }
public required uint? Ward { get; set; }
public required uint? Plot { get; set; }
public required float X { get; set; }
public required float Y { get; set; }
public required float Z { get; set; }
public required float Yaw { get; set; }
public required Guid PackId { get; set; }
[JsonProperty("template_1")]
public int Template1 { get; set; }
public required int Template1 { get; set; }
[JsonProperty("word_1_list")]
public int? Word1List { get; set; }
public required int? Word1List { get; set; }
[JsonProperty("word_1_word")]
public int? Word1Word { get; set; }
public required int? Word1Word { get; set; }
public int? Conjunction { get; set; }
public required int? Conjunction { get; set; }
[JsonProperty("template_2")]
public int? Template2 { get; set; }
public required int? Template2 { get; set; }
[JsonProperty("word_2_list")]
public int? Word2List { get; set; }
public required int? Word2List { get; set; }
[JsonProperty("word_2_word")]
public int? Word2Word { get; set; }
public required int? Word2Word { get; set; }
public int Glyph { get; set; }
public required int Glyph { get; set; }
public required EmoteData? Emote { get; set; }
}

View File

@ -1,8 +1,14 @@
using System.Numerics;
using System.Text;
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Game.ClientState.Objects.SubKinds;
using Dalamud.Interface.Textures;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.Game.Character;
using FFXIVClientStructs.FFXIV.Client.Game.Object;
using FFXIVClientStructs.FFXIV.Client.Game.UI;
using ImGuiNET;
using Lumina.Excel.GeneratedSheets;
using Newtonsoft.Json;
using OrangeGuidanceTomestone.Helpers;
using OrangeGuidanceTomestone.Util;
@ -21,6 +27,7 @@ internal class Write : ITab {
private int _part2 = -1;
private (int, int) _word2 = (-1, -1);
private int _glyph;
private int _emoteIdx = -1;
private const string Placeholder = "****";
private Pack? Pack => Pack.All.Get(this._pack);
@ -30,6 +37,8 @@ internal class Write : ITab {
private string? Word2 => this.GetWord(this._word2, this.Template2);
private string? Conjunction => this.Pack?.Conjunctions?.Get(this._conj);
private List<Emote> Emotes { get; }
private string? GetWord((int, int) word, Template? template) {
if (word.Item2 == -1) {
return Placeholder;
@ -58,6 +67,10 @@ internal class Write : ITab {
internal Write(Plugin plugin) {
this.Plugin = plugin;
this.Emotes = this.Plugin.DataManager.GetExcelSheet<Emote>()!
.Skip(1)
.ToList();
this._glyph = this.Plugin.Config.DefaultGlyph;
Pack.UpdatePacks();
}
@ -296,6 +309,37 @@ internal class Write : ITab {
}
}
var emoteLabel = this._emoteIdx == -1
? "None"
: this.Emotes[this._emoteIdx].Name.ToDalamudString().TextValue;
if (ImGui.BeginCombo("Emote", emoteLabel)) {
using var endCombo = new OnDispose(ImGui.EndCombo);
if (ImGui.Selectable("None##no-emote", this._emoteIdx == -1)) {
this._emoteIdx = -1;
}
ImGui.Separator();
for (var i = 0; i < this.Emotes.Count; i++) {
var emote = this.Emotes[i];
var name = emote.Name.ToDalamudString().TextValue;
var unlocked = IsEmoteUnlocked(emote);
if (!unlocked) {
ImGui.BeginDisabled();
}
if (ImGui.Selectable($"{name}##emote-{emote.RowId}", this._emoteIdx == i)) {
this._emoteIdx = i;
}
if (!unlocked) {
ImGui.EndDisabled();
}
}
}
this.ClearIfNecessary();
var valid = this.ValidSetup();
@ -310,7 +354,7 @@ internal class Write : ITab {
var location = HousingLocation.Current();
var req = new MessageRequest {
Territory = this.Plugin.ClientState.TerritoryType,
World = this.Plugin.ClientState.LocalPlayer?.CurrentWorld.Id ?? 0,
World = player.CurrentWorld.Id,
Ward = location?.Ward,
Plot = location?.CombinedPlot(),
X = player.Position.X,
@ -326,6 +370,9 @@ internal class Write : ITab {
Word2List = this._word2.Item1 == -1 ? null : this._word2.Item1,
Word2Word = this._word2.Item2 == -1 ? null : this._word2.Item2,
Glyph = this._glyph,
Emote = this._emoteIdx == -1
? null
: this.GetEmoteData(this.Emotes[this._emoteIdx], player),
};
var json = JsonConvert.SerializeObject(req);
@ -348,7 +395,9 @@ internal class Write : ITab {
Text = actualText,
NegativeVotes = 0,
PositiveVotes = 0,
UserVote = 0,
Glyph = this._glyph,
Emote = req.Emote,
};
this.Plugin.Messages.Add(newMsg);
@ -366,6 +415,50 @@ internal class Write : ITab {
}
}
private unsafe EmoteData GetEmoteData(Emote emote, IPlayerCharacter player) {
var objMan = ClientObjectManager.Instance();
var chara = (BattleChara*) objMan->GetObjectByIndex(player.ObjectIndex);
return new EmoteData {
Id = emote.RowId,
Customise = player.Customize,
Equipment = chara->DrawData.EquipmentModelIds
.ToArray()
.Select(equip => new EquipmentData {
Id = equip.Id,
Variant = equip.Variant,
Stain0 = equip.Stain0,
Stain1 = equip.Stain1,
Value = equip.Value,
})
.ToArray(),
Weapon = chara->DrawData.WeaponData
.ToArray()
.Select(weapon => new WeaponData {
ModelId = new WeaponModelId {
Id = weapon.ModelId.Id,
Kind = weapon.ModelId.Type,
Variant = weapon.ModelId.Variant,
Stain0 = weapon.ModelId.Stain0,
Stain1 = weapon.ModelId.Stain1,
Value = weapon.ModelId.Value,
},
Flags1 = weapon.Flags1,
Flags2 = weapon.Flags2,
State = weapon.State,
})
.ToArray(),
Glasses = chara->DrawData.GlassesIds[0],
HatHidden = chara->DrawData.IsHatHidden,
VisorToggled = chara->DrawData.IsVisorToggled,
WeaponHidden = chara->DrawData.IsWeaponHidden,
};
}
private static unsafe bool IsEmoteUnlocked(Emote emote) {
return UIState.Instance()->IsEmoteUnlocked((ushort) emote.RowId);
}
private void ResetWriter() {
this._part1 = this._part2 = this._conj = -1;
this._word1 = (-1, -1);

View File

@ -1,6 +1,7 @@
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game.Character;
using FFXIVClientStructs.FFXIV.Client.Game.Object;
using Lumina.Excel.GeneratedSheets;
namespace OrangeGuidanceTomestone.Util;
@ -51,7 +52,7 @@ internal class ActorManager : IDisposable {
Plugin.Log.Debug($"OnView message is {msg}");
this.Despawn();
if (message != null) {
if (message?.Emote != null) {
this.Spawn(message);
}
}
@ -108,6 +109,11 @@ internal class ActorManager : IDisposable {
return true;
}
if (message.Emote == null) {
Plugin.Log.Warning("refusing to spawn an actor for a message without an emote");
return true;
}
var idx = objMan->CreateBattleCharacter();
if (idx == 0xFFFFFFFF) {
Plugin.Log.Debug("actor could not be spawned");
@ -115,6 +121,7 @@ internal class ActorManager : IDisposable {
}
manager._idx = idx;
var emote = message.Emote;
var chara = (BattleChara*) objMan->GetObjectByIndex((ushort) idx);
@ -123,11 +130,53 @@ internal class ActorManager : IDisposable {
chara->Position = message.Position;
chara->Rotation = message.Yaw;
var drawData = &chara->DrawData;
drawData->CustomizeData = new CustomizeData();
var maxLen = Math.Min(sizeof(CustomizeData), emote.Customise.Length);
var rawCustomise = (byte*) &drawData->CustomizeData;
for (var i = 0; i < maxLen; i++) {
rawCustomise[i] = emote.Customise[i];
}
for (var i = 0; i < Math.Min(drawData->EquipmentModelIds.Length, emote.Equipment.Length); i++) {
var equip = emote.Equipment[i];
drawData->Equipment((DrawDataContainer.EquipmentSlot) i) = new EquipmentModelId {
Id = equip.Id,
Variant = equip.Variant,
Stain0 = equip.Stain0,
Stain1 = equip.Stain1,
Value = equip.Value,
};
}
for (var i = 0; i < Math.Min(drawData->WeaponData.Length, emote.Weapon.Length); i++) {
var weapon = emote.Weapon[i];
drawData->Weapon((DrawDataContainer.WeaponSlot) i) = new DrawObjectData {
ModelId = new FFXIVClientStructs.FFXIV.Client.Game.Character.WeaponModelId {
Id = weapon.ModelId.Id,
Type = weapon.ModelId.Kind,
Variant = weapon.ModelId.Variant,
Stain0 = weapon.ModelId.Stain0,
Stain1 = weapon.ModelId.Stain1,
Value = weapon.ModelId.Value,
},
DrawObject = chara->DrawObject,
Flags1 = weapon.Flags1,
Flags2 = weapon.Flags2,
State = weapon.State,
};
}
drawData->IsHatHidden = emote.HatHidden;
drawData->IsVisorToggled = emote.VisorToggled;
drawData->IsWeaponHidden = emote.WeaponHidden;
drawData->SetGlasses(0, (ushort) emote.Glasses);
chara->Alpha = 0.25f;
chara->SetMode(CharacterModes.AnimLock, 0);
chara->Timeline.BaseOverride = 4818;
if (manager.Plugin.DataManager.GetExcelSheet<Emote>()?.GetRow(emote.Id) is { } row) {
chara->Timeline.BaseOverride = (ushort) row.ActionTimeline[0].Row;
}
manager._tasks.Enqueue(new EnableAction());
return true;

1
server/Cargo.lock generated
View File

@ -1322,6 +1322,7 @@ dependencies = [
"rand",
"rayon",
"serde",
"serde_json",
"serde_yaml",
"sha3",
"sqlx",

View File

@ -15,6 +15,7 @@ parking_lot = "0.12"
rand = "0.8"
rayon = "1"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
serde_yaml = "0.9"
sha3 = "0.10"
sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "sqlite", "chrono"] }

View File

@ -0,0 +1,2 @@
alter table messages
add column emote text default null;

View File

@ -1,5 +1,5 @@
use serde::{Deserialize, Serialize};
use sqlx::types::chrono::NaiveDateTime;
use sqlx::types::{chrono::NaiveDateTime, Json};
use uuid::Uuid;
#[derive(Debug, Deserialize)]
@ -29,6 +29,9 @@ pub struct Message {
pub ward: Option<u16>,
#[serde(default)]
pub plot: Option<u16>,
#[serde(default)]
pub emote: Option<EmoteData>,
}
fn glyph_default() -> i8 {
@ -47,6 +50,7 @@ pub struct RetrievedMessage {
pub negative_votes: i32,
pub user_vote: i64,
pub glyph: i64,
pub emote: Option<Json<Option<EmoteData>>>,
#[serde(skip)]
pub created: NaiveDateTime,
#[serde(skip)]
@ -71,6 +75,7 @@ pub struct RetrievedMessageTerritory {
pub negative_votes: i32,
pub user_vote: i64,
pub glyph: i64,
pub emote: Option<Json<Option<EmoteData>>>,
#[serde(skip)]
pub created: NaiveDateTime,
}
@ -91,7 +96,47 @@ pub struct OwnMessage {
pub negative_votes: i32,
pub user_vote: i64,
pub glyph: i64,
pub emote: Option<Json<Option<EmoteData>>>,
#[serde(skip)]
pub created: NaiveDateTime,
pub is_hidden: bool,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct EmoteData {
pub id: u32,
pub customise: Vec<u8>,
pub equipment_data: Vec<EquipmentData>,
pub weapon_data: Vec<WeaponData>,
pub glasses: u32,
pub hat_hidden: bool,
pub visor_toggled: bool,
pub weapon_hidden: bool,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct EquipmentData {
pub id: u16,
pub variant: u8,
pub stain_0: u8,
pub stain_1: u8,
pub value: u64,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct WeaponData {
pub model_id: WeaponModelId,
pub state: u8,
pub flags_1: u16,
pub flags_2: u8,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct WeaponModelId {
pub id: u16,
pub kind: u16,
pub variant: u16,
pub stain_0: u8,
pub stain_1: u8,
pub value: u64,
}

View File

@ -7,10 +7,11 @@ use rand::Rng;
use rand::seq::SliceRandom;
use rayon::prelude::*;
use serde::Deserialize;
use sqlx::types::Json;
use warp::{Filter, Rejection, Reply};
use warp::filters::BoxedFilter;
use crate::message::RetrievedMessage;
use crate::message::{EmoteData, RetrievedMessage};
use crate::State;
use crate::util::HOUSING_ZONES;
use crate::web::{AnyhowRejection, WebError};
@ -68,6 +69,7 @@ async fn logic(state: Arc<State>, id: i64, location: u32, query: GetLocationQuer
coalesce(sum(v.vote between -1 and 0), 0) as negative_votes,
coalesce(v2.vote, 0) as user_vote,
m.glyph,
m.emote as "emote: Json<Option<EmoteData>>",
m.created,
m.user,
coalesce(cast((julianday(current_timestamp) - julianday(u.last_seen)) * 1440 as int), 0) as last_seen_minutes
@ -103,6 +105,7 @@ async fn logic(state: Arc<State>, id: i64, location: u32, query: GetLocationQuer
coalesce(sum(v.vote between -1 and 0), 0) as negative_votes,
coalesce(v2.vote, 0) as user_vote,
m.glyph,
m.emote as "emote: Json<Option<EmoteData>>",
m.created,
m.user,
coalesce(cast((julianday(current_timestamp) - julianday(u.last_seen)) * 1440 as int), 0) as last_seen_minutes

View File

@ -1,11 +1,12 @@
use std::sync::Arc;
use anyhow::Context;
use sqlx::types::Json;
use uuid::Uuid;
use warp::{Filter, Rejection, Reply};
use warp::filters::BoxedFilter;
use crate::message::RetrievedMessageTerritory;
use crate::message::{EmoteData, RetrievedMessageTerritory};
use crate::State;
use crate::web::{AnyhowRejection, WebError};
@ -39,6 +40,7 @@ async fn logic(state: Arc<State>, id: i64, message_id: Uuid) -> Result<impl Repl
coalesce(sum(v.vote between -1 and 0), 0) as negative_votes,
coalesce(v2.vote, 0) as user_vote,
m.glyph,
m.emote as "emote: Json<Option<EmoteData>>",
m.created
from messages m
left join votes v on m.id = v.message

View File

@ -2,10 +2,11 @@ use std::collections::HashMap;
use std::sync::Arc;
use anyhow::Context;
use sqlx::types::Json;
use warp::{Filter, Rejection, Reply};
use warp::filters::BoxedFilter;
use crate::message::OwnMessage;
use crate::message::{EmoteData, OwnMessage};
use crate::State;
use crate::web::AnyhowRejection;
@ -44,6 +45,7 @@ async fn logic(state: Arc<State>, id: i64, extra: i64, mut query: HashMap<String
coalesce(v2.vote, 0) as user_vote,
m.glyph,
m.created,
m.emote as "emote: Json<Option<EmoteData>>",
0 as "is_hidden: bool"
from messages m
left join votes v on m.id = v.message

View File

@ -65,9 +65,13 @@ async fn logic(state: Arc<State>, id: i64, extra: i64, message: Message) -> Resu
let message_id = Uuid::new_v4().simple().to_string();
let territory = message.territory as i64;
let json = serde_json::to_string(&message.emote)
.context("could not serialise emote")
.map_err(AnyhowRejection)
.map_err(warp::reject::custom)?;
sqlx::query!(
// language=sqlite
"insert into messages (id, user, territory, world, ward, plot, x, y, z, yaw, message, glyph) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
"insert into messages (id, user, territory, world, ward, plot, x, y, z, yaw, message, glyph, emote) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
message_id,
id,
territory,
@ -80,6 +84,7 @@ async fn logic(state: Arc<State>, id: i64, extra: i64, message: Message) -> Resu
message.yaw,
text,
message.glyph,
json,
)
.execute(&state.db)
.await