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

View File

@ -6,35 +6,36 @@ namespace OrangeGuidanceTomestone;
[Serializable] [Serializable]
[JsonObject(NamingStrategyType = typeof(SnakeCaseNamingStrategy))] [JsonObject(NamingStrategyType = typeof(SnakeCaseNamingStrategy))]
public class MessageRequest { public class MessageRequest {
public uint Territory { get; set; } public required uint Territory { get; set; }
public uint? World { get; set; } public required uint? World { get; set; }
public uint? Ward { get; set; } public required uint? Ward { get; set; }
public uint? Plot { get; set; } public required uint? Plot { get; set; }
public float X { get; set; } public required float X { get; set; }
public float Y { get; set; } public required float Y { get; set; }
public float Z { get; set; } public required float Z { get; set; }
public float Yaw { get; set; } public required float Yaw { get; set; }
public Guid PackId { get; set; } public required Guid PackId { get; set; }
[JsonProperty("template_1")] [JsonProperty("template_1")]
public int Template1 { get; set; } public required int Template1 { get; set; }
[JsonProperty("word_1_list")] [JsonProperty("word_1_list")]
public int? Word1List { get; set; } public required int? Word1List { get; set; }
[JsonProperty("word_1_word")] [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")] [JsonProperty("template_2")]
public int? Template2 { get; set; } public required int? Template2 { get; set; }
[JsonProperty("word_2_list")] [JsonProperty("word_2_list")]
public int? Word2List { get; set; } public required int? Word2List { get; set; }
[JsonProperty("word_2_word")] [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.Numerics;
using System.Text; using System.Text;
using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.ClientState.Conditions;
using Dalamud.Game.ClientState.Objects.SubKinds;
using Dalamud.Interface.Textures; 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 ImGuiNET;
using Lumina.Excel.GeneratedSheets;
using Newtonsoft.Json; using Newtonsoft.Json;
using OrangeGuidanceTomestone.Helpers; using OrangeGuidanceTomestone.Helpers;
using OrangeGuidanceTomestone.Util; using OrangeGuidanceTomestone.Util;
@ -21,6 +27,7 @@ internal class Write : ITab {
private int _part2 = -1; private int _part2 = -1;
private (int, int) _word2 = (-1, -1); private (int, int) _word2 = (-1, -1);
private int _glyph; private int _glyph;
private int _emoteIdx = -1;
private const string Placeholder = "****"; private const string Placeholder = "****";
private Pack? Pack => Pack.All.Get(this._pack); 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? Word2 => this.GetWord(this._word2, this.Template2);
private string? Conjunction => this.Pack?.Conjunctions?.Get(this._conj); private string? Conjunction => this.Pack?.Conjunctions?.Get(this._conj);
private List<Emote> Emotes { get; }
private string? GetWord((int, int) word, Template? template) { private string? GetWord((int, int) word, Template? template) {
if (word.Item2 == -1) { if (word.Item2 == -1) {
return Placeholder; return Placeholder;
@ -58,6 +67,10 @@ internal class Write : ITab {
internal Write(Plugin plugin) { internal Write(Plugin plugin) {
this.Plugin = plugin; this.Plugin = plugin;
this.Emotes = this.Plugin.DataManager.GetExcelSheet<Emote>()!
.Skip(1)
.ToList();
this._glyph = this.Plugin.Config.DefaultGlyph; this._glyph = this.Plugin.Config.DefaultGlyph;
Pack.UpdatePacks(); 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(); this.ClearIfNecessary();
var valid = this.ValidSetup(); var valid = this.ValidSetup();
@ -310,7 +354,7 @@ internal class Write : ITab {
var location = HousingLocation.Current(); var location = HousingLocation.Current();
var req = new MessageRequest { var req = new MessageRequest {
Territory = this.Plugin.ClientState.TerritoryType, Territory = this.Plugin.ClientState.TerritoryType,
World = this.Plugin.ClientState.LocalPlayer?.CurrentWorld.Id ?? 0, World = player.CurrentWorld.Id,
Ward = location?.Ward, Ward = location?.Ward,
Plot = location?.CombinedPlot(), Plot = location?.CombinedPlot(),
X = player.Position.X, X = player.Position.X,
@ -326,6 +370,9 @@ internal class Write : ITab {
Word2List = this._word2.Item1 == -1 ? null : this._word2.Item1, Word2List = this._word2.Item1 == -1 ? null : this._word2.Item1,
Word2Word = this._word2.Item2 == -1 ? null : this._word2.Item2, Word2Word = this._word2.Item2 == -1 ? null : this._word2.Item2,
Glyph = this._glyph, Glyph = this._glyph,
Emote = this._emoteIdx == -1
? null
: this.GetEmoteData(this.Emotes[this._emoteIdx], player),
}; };
var json = JsonConvert.SerializeObject(req); var json = JsonConvert.SerializeObject(req);
@ -348,7 +395,9 @@ internal class Write : ITab {
Text = actualText, Text = actualText,
NegativeVotes = 0, NegativeVotes = 0,
PositiveVotes = 0, PositiveVotes = 0,
UserVote = 0,
Glyph = this._glyph, Glyph = this._glyph,
Emote = req.Emote,
}; };
this.Plugin.Messages.Add(newMsg); 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() { private void ResetWriter() {
this._part1 = this._part2 = this._conj = -1; this._part1 = this._part2 = this._conj = -1;
this._word1 = (-1, -1); this._word1 = (-1, -1);

View File

@ -1,6 +1,7 @@
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Character;
using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.Game.Object;
using Lumina.Excel.GeneratedSheets;
namespace OrangeGuidanceTomestone.Util; namespace OrangeGuidanceTomestone.Util;
@ -51,7 +52,7 @@ internal class ActorManager : IDisposable {
Plugin.Log.Debug($"OnView message is {msg}"); Plugin.Log.Debug($"OnView message is {msg}");
this.Despawn(); this.Despawn();
if (message != null) { if (message?.Emote != null) {
this.Spawn(message); this.Spawn(message);
} }
} }
@ -108,6 +109,11 @@ internal class ActorManager : IDisposable {
return true; 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(); var idx = objMan->CreateBattleCharacter();
if (idx == 0xFFFFFFFF) { if (idx == 0xFFFFFFFF) {
Plugin.Log.Debug("actor could not be spawned"); Plugin.Log.Debug("actor could not be spawned");
@ -115,6 +121,7 @@ internal class ActorManager : IDisposable {
} }
manager._idx = idx; manager._idx = idx;
var emote = message.Emote;
var chara = (BattleChara*) objMan->GetObjectByIndex((ushort) idx); var chara = (BattleChara*) objMan->GetObjectByIndex((ushort) idx);
@ -123,11 +130,53 @@ internal class ActorManager : IDisposable {
chara->Position = message.Position; chara->Position = message.Position;
chara->Rotation = message.Yaw; chara->Rotation = message.Yaw;
var drawData = &chara->DrawData; 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->Alpha = 0.25f;
chara->SetMode(CharacterModes.AnimLock, 0); 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()); manager._tasks.Enqueue(new EnableAction());
return true; return true;

1
server/Cargo.lock generated
View File

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

View File

@ -15,6 +15,7 @@ parking_lot = "0.12"
rand = "0.8" rand = "0.8"
rayon = "1" rayon = "1"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1"
serde_yaml = "0.9" serde_yaml = "0.9"
sha3 = "0.10" sha3 = "0.10"
sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "sqlite", "chrono"] } 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 serde::{Deserialize, Serialize};
use sqlx::types::chrono::NaiveDateTime; use sqlx::types::{chrono::NaiveDateTime, Json};
use uuid::Uuid; use uuid::Uuid;
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
@ -29,6 +29,9 @@ pub struct Message {
pub ward: Option<u16>, pub ward: Option<u16>,
#[serde(default)] #[serde(default)]
pub plot: Option<u16>, pub plot: Option<u16>,
#[serde(default)]
pub emote: Option<EmoteData>,
} }
fn glyph_default() -> i8 { fn glyph_default() -> i8 {
@ -47,6 +50,7 @@ pub struct RetrievedMessage {
pub negative_votes: i32, pub negative_votes: i32,
pub user_vote: i64, pub user_vote: i64,
pub glyph: i64, pub glyph: i64,
pub emote: Option<Json<Option<EmoteData>>>,
#[serde(skip)] #[serde(skip)]
pub created: NaiveDateTime, pub created: NaiveDateTime,
#[serde(skip)] #[serde(skip)]
@ -71,6 +75,7 @@ pub struct RetrievedMessageTerritory {
pub negative_votes: i32, pub negative_votes: i32,
pub user_vote: i64, pub user_vote: i64,
pub glyph: i64, pub glyph: i64,
pub emote: Option<Json<Option<EmoteData>>>,
#[serde(skip)] #[serde(skip)]
pub created: NaiveDateTime, pub created: NaiveDateTime,
} }
@ -91,7 +96,47 @@ pub struct OwnMessage {
pub negative_votes: i32, pub negative_votes: i32,
pub user_vote: i64, pub user_vote: i64,
pub glyph: i64, pub glyph: i64,
pub emote: Option<Json<Option<EmoteData>>>,
#[serde(skip)] #[serde(skip)]
pub created: NaiveDateTime, pub created: NaiveDateTime,
pub is_hidden: bool, 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 rand::seq::SliceRandom;
use rayon::prelude::*; use rayon::prelude::*;
use serde::Deserialize; use serde::Deserialize;
use sqlx::types::Json;
use warp::{Filter, Rejection, Reply}; use warp::{Filter, Rejection, Reply};
use warp::filters::BoxedFilter; use warp::filters::BoxedFilter;
use crate::message::RetrievedMessage; use crate::message::{EmoteData, RetrievedMessage};
use crate::State; use crate::State;
use crate::util::HOUSING_ZONES; use crate::util::HOUSING_ZONES;
use crate::web::{AnyhowRejection, WebError}; 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(sum(v.vote between -1 and 0), 0) as negative_votes,
coalesce(v2.vote, 0) as user_vote, coalesce(v2.vote, 0) as user_vote,
m.glyph, m.glyph,
m.emote as "emote: Json<Option<EmoteData>>",
m.created, m.created,
m.user, m.user,
coalesce(cast((julianday(current_timestamp) - julianday(u.last_seen)) * 1440 as int), 0) as last_seen_minutes 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(sum(v.vote between -1 and 0), 0) as negative_votes,
coalesce(v2.vote, 0) as user_vote, coalesce(v2.vote, 0) as user_vote,
m.glyph, m.glyph,
m.emote as "emote: Json<Option<EmoteData>>",
m.created, m.created,
m.user, m.user,
coalesce(cast((julianday(current_timestamp) - julianday(u.last_seen)) * 1440 as int), 0) as last_seen_minutes 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 std::sync::Arc;
use anyhow::Context; use anyhow::Context;
use sqlx::types::Json;
use uuid::Uuid; use uuid::Uuid;
use warp::{Filter, Rejection, Reply}; use warp::{Filter, Rejection, Reply};
use warp::filters::BoxedFilter; use warp::filters::BoxedFilter;
use crate::message::RetrievedMessageTerritory; use crate::message::{EmoteData, RetrievedMessageTerritory};
use crate::State; use crate::State;
use crate::web::{AnyhowRejection, WebError}; 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(sum(v.vote between -1 and 0), 0) as negative_votes,
coalesce(v2.vote, 0) as user_vote, coalesce(v2.vote, 0) as user_vote,
m.glyph, m.glyph,
m.emote as "emote: Json<Option<EmoteData>>",
m.created m.created
from messages m from messages m
left join votes v on m.id = v.message left join votes v on m.id = v.message

View File

@ -2,10 +2,11 @@ use std::collections::HashMap;
use std::sync::Arc; use std::sync::Arc;
use anyhow::Context; use anyhow::Context;
use sqlx::types::Json;
use warp::{Filter, Rejection, Reply}; use warp::{Filter, Rejection, Reply};
use warp::filters::BoxedFilter; use warp::filters::BoxedFilter;
use crate::message::OwnMessage; use crate::message::{EmoteData, OwnMessage};
use crate::State; use crate::State;
use crate::web::AnyhowRejection; 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, coalesce(v2.vote, 0) as user_vote,
m.glyph, m.glyph,
m.created, m.created,
m.emote as "emote: Json<Option<EmoteData>>",
0 as "is_hidden: bool" 0 as "is_hidden: bool"
from messages m from messages m
left join votes v on m.id = v.message 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 message_id = Uuid::new_v4().simple().to_string();
let territory = message.territory as i64; 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!( sqlx::query!(
// language=sqlite // 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, message_id,
id, id,
territory, territory,
@ -80,6 +84,7 @@ async fn logic(state: Arc<State>, id: i64, extra: i64, message: Message) -> Resu
message.yaw, message.yaw,
text, text,
message.glyph, message.glyph,
json,
) )
.execute(&state.db) .execute(&state.db)
.await .await