feat: use ward info

This commit is contained in:
Anna 2023-01-29 04:26:58 -05:00
parent dc4fc02e71
commit db2cfcfb94
17 changed files with 534 additions and 337 deletions

View File

@ -7,6 +7,7 @@ namespace OrangeGuidanceTomestone;
[JsonObject(NamingStrategyType = typeof(SnakeCaseNamingStrategy))] [JsonObject(NamingStrategyType = typeof(SnakeCaseNamingStrategy))]
public class MessageRequest { public class MessageRequest {
public uint Territory { get; set; } public uint Territory { get; set; }
public uint? Ward { get; set; }
public float X { get; set; } public float X { get; set; }
public float Y { get; set; } public float Y { get; set; }
public float Z { get; set; } public float Z { get; set; }

View File

@ -153,6 +153,8 @@ internal class Messages : IDisposable {
return; return;
} }
var ward = this.Plugin.Common.Functions.Housing.Location?.Ward;
if (this.Plugin.Config.DisableTrials && this.Trials.Contains(territory)) { if (this.Plugin.Config.DisableTrials && this.Trials.Contains(territory)) {
return; return;
} }
@ -173,18 +175,23 @@ internal class Messages : IDisposable {
Task.Run(async () => { Task.Run(async () => {
try { try {
await this.DownloadMessages(territory); await this.DownloadMessages(territory, ward);
} catch (Exception ex) { } catch (Exception ex) {
PluginLog.LogError(ex, $"Failed to get messages for territory {territory}"); PluginLog.LogError(ex, $"Failed to get messages for territory {territory}");
} }
}); });
} }
private async Task DownloadMessages(ushort territory) { private async Task DownloadMessages(ushort territory, ushort? ward) {
var route = $"/messages/{territory}";
if (ward != null) {
route += $"?ward={ward}";
}
var resp = await ServerHelper.SendRequest( var resp = await ServerHelper.SendRequest(
this.Plugin.Config.ApiKey, this.Plugin.Config.ApiKey,
HttpMethod.Get, HttpMethod.Get,
$"/messages/{territory}" route
); );
var json = await resp.Content.ReadAsStringAsync(); var json = await resp.Content.ReadAsStringAsync();
var messages = JsonConvert.DeserializeObject<Message[]>(json)!; var messages = JsonConvert.DeserializeObject<Message[]>(json)!;

View File

@ -61,6 +61,7 @@
<PackageReference Include="DalamudPackager" Version="2.1.10"/> <PackageReference Include="DalamudPackager" Version="2.1.10"/>
<PackageReference Include="Fody" Version="6.6.4" PrivateAssets="all"/> <PackageReference Include="Fody" Version="6.6.4" PrivateAssets="all"/>
<PackageReference Include="Resourcer.Fody" Version="1.8.0" PrivateAssets="all"/> <PackageReference Include="Resourcer.Fody" Version="1.8.0" PrivateAssets="all"/>
<PackageReference Include="XivCommon" Version="7.0.0-alpha.1" PrivateAssets="all"/>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -7,6 +7,7 @@ using Dalamud.Game.Gui;
using Dalamud.IoC; using Dalamud.IoC;
using Dalamud.Plugin; using Dalamud.Plugin;
using OrangeGuidanceTomestone.MiniPenumbra; using OrangeGuidanceTomestone.MiniPenumbra;
using XivCommon;
namespace OrangeGuidanceTomestone; namespace OrangeGuidanceTomestone;
@ -38,6 +39,7 @@ public class Plugin : IDalamudPlugin {
internal GameGui GameGui { get; init; } internal GameGui GameGui { get; init; }
internal Configuration Config { get; } internal Configuration Config { get; }
internal XivCommonBase Common { get; }
internal Vfx Vfx { get; } internal Vfx Vfx { get; }
internal PluginUi Ui { get; } internal PluginUi Ui { get; }
internal Messages Messages { get; } internal Messages Messages { get; }
@ -51,6 +53,7 @@ public class Plugin : IDalamudPlugin {
this.AvfxFilePath = this.CopyAvfxFile(); this.AvfxFilePath = this.CopyAvfxFile();
this.Config = this.Interface!.GetPluginConfig() as Configuration ?? new Configuration(); this.Config = this.Interface!.GetPluginConfig() as Configuration ?? new Configuration();
this.Common = new XivCommonBase();
this.Vfx = new Vfx(); this.Vfx = new Vfx();
this.Messages = new Messages(this); this.Messages = new Messages(this);
this.Ui = new PluginUi(this); this.Ui = new PluginUi(this);
@ -70,6 +73,7 @@ public class Plugin : IDalamudPlugin {
this.Ui.Dispose(); this.Ui.Dispose();
this.Messages.Dispose(); this.Messages.Dispose();
this.Vfx.Dispose(); this.Vfx.Dispose();
this.Common.Dispose();
} }
internal void SaveConfig() { internal void SaveConfig() {

View File

@ -231,6 +231,7 @@ internal class Write : ITab {
if (ImGui.Button("Write") && valid && !inAir && this.Plugin.ClientState.LocalPlayer is { } player) { if (ImGui.Button("Write") && valid && !inAir && this.Plugin.ClientState.LocalPlayer is { } player) {
var req = new MessageRequest { var req = new MessageRequest {
Territory = this.Plugin.ClientState.TerritoryType, Territory = this.Plugin.ClientState.TerritoryType,
Ward = this.Plugin.Common.Functions.Housing.Location?.Ward,
X = player.Position.X, X = player.Position.X,
Y = player.Position.Y, Y = player.Position.Y,
Z = player.Position.Z, Z = player.Position.Z,

6
client/packages.lock.json Executable file → Normal file
View File

@ -24,6 +24,12 @@
"NETStandard.Library": "1.6.1" "NETStandard.Library": "1.6.1"
} }
}, },
"XivCommon": {
"type": "Direct",
"requested": "[7.0.0-alpha.1, )",
"resolved": "7.0.0-alpha.1",
"contentHash": "4Yy4Wg3bnI/g9BSEYAqOZALmVMJZS0wcP847VlUIThBqIS/47O6tw2BI84he4KuPSTfIsYOzrcz3vAia8+Pn3g=="
},
"Microsoft.NETCore.Platforms": { "Microsoft.NETCore.Platforms": {
"type": "Transitive", "type": "Transitive",
"resolved": "1.1.0", "resolved": "1.1.0",

703
server/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -7,7 +7,7 @@ edition = "2021"
[dependencies] [dependencies]
anyhow = "1" anyhow = "1"
base64 = "0.13" base64 = "0.21"
bytes = "1" bytes = "1"
chrono = "0.4" chrono = "0.4"
if_chain = "1" if_chain = "1"
@ -16,7 +16,7 @@ serde = { version = "1", features = ["derive"] }
serde_yaml = "0.9" serde_yaml = "0.9"
sha3 = "0.10" sha3 = "0.10"
sqlx = { version = "0.6", features = ["runtime-tokio-rustls", "sqlite", "chrono"] } sqlx = { version = "0.6", features = ["runtime-tokio-rustls", "sqlite", "chrono"] }
toml = "0.5" toml = "0.7"
tokio = { version = "1", features = ["rt-multi-thread", "macros"] } tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
tokio-stream = { version = "0.1", default-features = false, features = ["net"] } tokio-stream = { version = "0.1", default-features = false, features = ["net"] }
uuid = { version = "1", features = ["serde", "v4"] } uuid = { version = "1", features = ["serde", "v4"] }

View File

@ -0,0 +1,3 @@
alter table messages
add column ward int;
create index messages_ward_idx on messages (ward);

View File

@ -1,9 +1,7 @@
#![feature(drain_filter)] #![feature(drain_filter)]
use std::collections::HashMap; use std::collections::HashMap;
use std::fs::Permissions;
use std::net::SocketAddr; use std::net::SocketAddr;
use std::os::unix::fs::PermissionsExt;
use std::str::FromStr; use std::str::FromStr;
use std::sync::Arc; use std::sync::Arc;
@ -11,7 +9,6 @@ use anyhow::{Context, Result};
use sqlx::{Executor, Pool, Sqlite}; use sqlx::{Executor, Pool, Sqlite};
use sqlx::migrate::Migrator; use sqlx::migrate::Migrator;
use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions}; use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions};
use tokio::fs::File;
use tokio::net::{TcpListener, UnixListener}; use tokio::net::{TcpListener, UnixListener};
use tokio::runtime::Handle; use tokio::runtime::Handle;
use tokio::sync::RwLock; use tokio::sync::RwLock;
@ -54,7 +51,7 @@ impl State {
let text = match tokio::fs::read_to_string(entry.path()).await { let text = match tokio::fs::read_to_string(entry.path()).await {
Ok(t) => t, Ok(t) => t,
Err(e) => { Err(e) => {
eprintln!("error reading pack: {:#?}", e); eprintln!("error reading pack: {e:#?}");
continue; continue;
} }
}; };
@ -119,10 +116,10 @@ async fn main() -> Result<()> {
let address = state.config.address.clone(); let address = state.config.address.clone();
let server = warp::serve(web::routes(state)); let server = warp::serve(web::routes(state));
println!("listening at {}", address); println!("listening at {address}");
if address.starts_with("unix:") { if let Some(path) = address.strip_prefix("unix:") {
let listener = UnixListener::bind(&address[5..])?; let listener = UnixListener::bind(path)?;
let stream = UnixListenerStream::new(listener); let stream = UnixListenerStream::new(listener);
server.run_incoming(stream).await; server.run_incoming(stream).await;
} else { } else {
@ -141,16 +138,13 @@ fn spawn_command_reader(state: Arc<State>, handle: Handle) {
while let Ok(size) = std::io::stdin().read_line(&mut line) { while let Ok(size) = std::io::stdin().read_line(&mut line) {
let read = line[..size].trim(); let read = line[..size].trim();
match read { if read == "reload packs" {
"reload packs" => { let state = Arc::clone(&state);
let state = Arc::clone(&state); handle.spawn(async move {
handle.spawn(async move { if let Err(e) = state.update_packs().await {
if let Err(e) = state.update_packs().await { eprintln!("failed to update packs: {e:#?}");
eprintln!("failed to update packs: {:#?}", e); }
} });
});
}
_ => {}
} }
line.clear(); line.clear();

View File

@ -22,6 +22,9 @@ pub struct Message {
#[serde(default = "glyph_default")] #[serde(default = "glyph_default")]
pub glyph: i8, pub glyph: i8,
#[serde(default)]
pub ward: Option<u16>,
} }
fn glyph_default() -> i8 { fn glyph_default() -> i8 {
@ -52,6 +55,7 @@ pub struct RetrievedMessage {
pub struct RetrievedMessageTerritory { pub struct RetrievedMessageTerritory {
pub id: String, pub id: String,
pub territory: i64, pub territory: i64,
pub ward: Option<i64>,
pub x: f64, pub x: f64,
pub y: f64, pub y: f64,
pub z: f64, pub z: f64,
@ -69,6 +73,7 @@ pub struct RetrievedMessageTerritory {
pub struct OwnMessage { pub struct OwnMessage {
pub id: String, pub id: String,
pub territory: i64, pub territory: i64,
pub ward: Option<i64>,
pub x: f64, pub x: f64,
pub y: f64, pub y: f64,
pub z: f64, pub z: f64,

View File

@ -1,8 +1,54 @@
use base64::Engine;
use base64::prelude::BASE64_STANDARD;
use sha3::{Digest, Sha3_384}; use sha3::{Digest, Sha3_384};
// TerritoryIntendedUse = 13 or 14
pub const HOUSING_ZONES: &[u32] = &[
282,
283,
284,
339,
340,
341,
342,
343,
344,
345,
346,
347,
384,
385,
386,
423,
424,
425,
573,
574,
575,
608,
609,
610,
641,
649,
650,
651,
652,
653,
654,
655,
979,
980,
981,
982,
983,
984,
985,
999,
];
pub fn hash(input: &str) -> String { pub fn hash(input: &str) -> String {
let mut hasher = Sha3_384::default(); let mut hasher = Sha3_384::default();
hasher.update(input.as_bytes()); hasher.update(input.as_bytes());
let result = hasher.finalize(); let result = hasher.finalize();
base64::encode(result) BASE64_STANDARD.encode(result)
} }

View File

@ -73,6 +73,8 @@ pub enum WebError {
TooManyMessages, TooManyMessages,
NoSuchMessage, NoSuchMessage,
InvalidExtraCode, InvalidExtraCode,
MissingWard,
UnnecessaryWard,
} }
impl Reject for WebError {} impl Reject for WebError {}
@ -92,22 +94,24 @@ async fn handle_rejection(err: Rejection) -> Result<impl Reply, Infallible> {
WebError::TooManyMessages => (StatusCode::BAD_REQUEST, "too_many_messages", "you have run out of messages - delete one and try again".into()), WebError::TooManyMessages => (StatusCode::BAD_REQUEST, "too_many_messages", "you have run out of messages - delete one and try again".into()),
WebError::NoSuchMessage => (StatusCode::NOT_FOUND, "no_such_message", "no message with that id was found".into()), WebError::NoSuchMessage => (StatusCode::NOT_FOUND, "no_such_message", "no message with that id was found".into()),
WebError::InvalidExtraCode => (StatusCode::BAD_REQUEST, "invalid_extra_code", "that extra code was not found".into()), WebError::InvalidExtraCode => (StatusCode::BAD_REQUEST, "invalid_extra_code", "that extra code was not found".into()),
WebError::MissingWard => (StatusCode::BAD_REQUEST, "missing_ward", "a ward was not provided - try updating the plugin".into()),
WebError::UnnecessaryWard => (StatusCode::BAD_REQUEST, "unnecessar_ward", "a ward was provided but not necessary - try updating the plugin".into()),
} }
} else if err.is_not_found() { } else if err.is_not_found() {
(StatusCode::NOT_FOUND, "not_found", "route was unknown to the server".into()) (StatusCode::NOT_FOUND, "not_found", "route was unknown to the server".into())
} else if let Some(e) = err.find::<BodyDeserializeError>() { } else if let Some(e) = err.find::<BodyDeserializeError>() {
(StatusCode::BAD_REQUEST, "invalid_body", format!("invalid body: {}", e)) (StatusCode::BAD_REQUEST, "invalid_body", format!("invalid body: {e}"))
} else if let Some(_) = err.find::<MethodNotAllowed>() { } else if err.find::<MethodNotAllowed>().is_some() {
(StatusCode::METHOD_NOT_ALLOWED, "method_not_allowed", "that http method is not allowed on that route".into()) (StatusCode::METHOD_NOT_ALLOWED, "method_not_allowed", "that http method is not allowed on that route".into())
} else if let Some(AnyhowRejection(e)) = err.find::<AnyhowRejection>() { } else if let Some(AnyhowRejection(e)) = err.find::<AnyhowRejection>() {
eprintln!("{:#?}", e); eprintln!("{e:#?}");
( (
StatusCode::INTERNAL_SERVER_ERROR, StatusCode::INTERNAL_SERVER_ERROR,
"internal_error", "internal_error",
"an internal logic error occured".into(), "an internal logic error occured".into(),
) )
} else { } else {
eprintln!("{:#?}", err); eprintln!("{err:#?}");
( (
StatusCode::INTERNAL_SERVER_ERROR, StatusCode::INTERNAL_SERVER_ERROR,
"internal_error", "internal_error",

View File

@ -4,12 +4,14 @@ use anyhow::Context;
use chrono::{Duration, Utc}; use chrono::{Duration, Utc};
use rand::Rng; use rand::Rng;
use rand::seq::SliceRandom; use rand::seq::SliceRandom;
use serde::Deserialize;
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::RetrievedMessage;
use crate::State; use crate::State;
use crate::web::AnyhowRejection; use crate::util::HOUSING_ZONES;
use crate::web::{AnyhowRejection, WebError};
pub fn get_location(state: Arc<State>) -> BoxedFilter<(impl Reply, )> { pub fn get_location(state: Arc<State>) -> BoxedFilter<(impl Reply, )> {
warp::get() warp::get()
@ -17,11 +19,27 @@ pub fn get_location(state: Arc<State>) -> BoxedFilter<(impl Reply, )> {
.and(warp::path::param()) .and(warp::path::param())
.and(warp::path::end()) .and(warp::path::end())
.and(super::get_id(Arc::clone(&state))) .and(super::get_id(Arc::clone(&state)))
.and_then(move |location: u32, (id, _)| logic(Arc::clone(&state), id, location)) .and(warp::query::<GetLocationQuery>())
.and_then(move |location: u32, (id, _), query| logic(Arc::clone(&state), id, location, query))
.boxed() .boxed()
} }
async fn logic(state: Arc<State>, id: i64, location: u32) -> Result<impl Reply, Rejection> { #[derive(Deserialize)]
pub struct GetLocationQuery {
#[serde(default)]
ward: Option<u32>,
}
async fn logic(state: Arc<State>, id: i64, location: u32, query: GetLocationQuery) -> Result<impl Reply, Rejection> {
let housing = HOUSING_ZONES.contains(&location);
if housing && query.ward.is_none() {
return Err(warp::reject::custom(WebError::MissingWard));
}
if !housing && query.ward.is_some() {
return Err(warp::reject::custom(WebError::UnnecessaryWard));
}
let location = location as i64; let location = location as i64;
let mut messages = sqlx::query_as!( let mut messages = sqlx::query_as!(
RetrievedMessage, RetrievedMessage,
@ -44,10 +62,11 @@ async fn logic(state: Arc<State>, id: i64, location: u32) -> Result<impl Reply,
left join votes v on m.id = v.message left join votes v on m.id = v.message
left join votes v2 on m.id = v2.message and v2.user = ? left join votes v2 on m.id = v2.message and v2.user = ?
inner join users u on m.user = u.id inner join users u on m.user = u.id
where m.territory = ? where m.territory = ? and m.ward = ?
group by m.id"#, group by m.id"#,
id, id,
location, location,
query.ward,
) )
.fetch_all(&state.db) .fetch_all(&state.db)
.await .await

View File

@ -27,6 +27,7 @@ async fn logic(state: Arc<State>, id: i64, message_id: Uuid) -> Result<impl Repl
r#" r#"
select m.id, select m.id,
m.territory, m.territory,
m.ward,
m.x, m.x,
m.y, m.y,
m.z, m.z,

View File

@ -31,6 +31,7 @@ async fn logic(state: Arc<State>, id: i64, extra: i64, mut query: HashMap<String
r#" r#"
select m.id, select m.id,
m.territory, m.territory,
m.ward,
m.x, m.x,
m.y, m.y,
m.z, m.z,

View File

@ -7,6 +7,7 @@ use warp::filters::BoxedFilter;
use crate::message::Message; use crate::message::Message;
use crate::State; use crate::State;
use crate::util::HOUSING_ZONES;
use crate::web::{AnyhowRejection, WebError}; use crate::web::{AnyhowRejection, WebError};
pub fn write(state: Arc<State>) -> BoxedFilter<(impl Reply, )> { pub fn write(state: Arc<State>) -> BoxedFilter<(impl Reply, )> {
@ -21,6 +22,15 @@ pub fn write(state: Arc<State>) -> BoxedFilter<(impl Reply, )> {
} }
async fn logic(state: Arc<State>, id: i64, extra: i64, message: Message) -> Result<impl Reply, Rejection> { async fn logic(state: Arc<State>, id: i64, extra: i64, message: Message) -> Result<impl Reply, Rejection> {
let housing = HOUSING_ZONES.contains(&message.territory);
if housing && message.ward.is_none() {
return Err(warp::reject::custom(WebError::MissingWard));
}
if !housing && message.ward.is_some() {
return Err(warp::reject::custom(WebError::UnnecessaryWard));
}
let text = { let text = {
let packs = state.packs.read().await; let packs = state.packs.read().await;
let pack = packs.get(&message.pack_id) let pack = packs.get(&message.pack_id)
@ -57,10 +67,11 @@ async fn logic(state: Arc<State>, id: i64, extra: i64, message: Message) -> Resu
sqlx::query!( sqlx::query!(
// language=sqlite // language=sqlite
"insert into messages (id, user, territory, x, y, z, yaw, message, glyph) values (?, ?, ?, ?, ?, ?, ?, ?, ?)", "insert into messages (id, user, territory, ward, x, y, z, yaw, message, glyph) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
message_id, message_id,
id, id,
territory, territory,
message.ward,
message.x, message.x,
message.y, message.y,
message.z, message.z,