Initial release
parent
542dd6de65
commit
875c93afed
@ -0,0 +1,10 @@
|
||||
/target
|
||||
|
||||
|
||||
#Added by cargo
|
||||
#
|
||||
#already existing elements were commented out
|
||||
|
||||
#/target
|
||||
Cargo.lock
|
||||
docker-test/data
|
@ -0,0 +1,18 @@
|
||||
[package]
|
||||
name = "nwahttp"
|
||||
version = "0.1.0"
|
||||
authors = ["eater <=@eater.me>"]
|
||||
edition = "2018"
|
||||
|
||||
[lib]
|
||||
crate-type = ["staticlib", "cdylib"]
|
||||
|
||||
[dependencies]
|
||||
lazy_static = "1.4.0"
|
||||
warp = "0.2.1"
|
||||
tokio = { version = "0.2", features = ["macros"] }
|
||||
serde = { version = "1.0.104", features = ["derive"] }
|
||||
serde_json = "1.0.48"
|
||||
futures-util = "0.3.4"
|
||||
prometheus = "0.7.0"
|
||||
hyper = "0.13.2"
|
@ -0,0 +1,15 @@
|
||||
# nwahttp
|
||||
|
||||
Putting things in places where they shouldn't be
|
||||
|
||||
## Features
|
||||
|
||||
- Prometheus metrics endpoint
|
||||
- REST API with player info
|
||||
- WebSocket with realtime player info
|
||||
|
||||
## Usage
|
||||
|
||||
build with `cargo build --release` for target and place `nwahttp.so` in the `$TES3MP_HOME/scripts` folder, and add `nwahttp.so` to the scripts argument in the config,
|
||||
|
||||
then connect via `http://[ip of tes3mp server]:8787`
|
@ -0,0 +1,93 @@
|
||||
FROM alpine:3.10 as builder
|
||||
|
||||
ENV TES3MP_VERSION 0.7.0
|
||||
ENV TES3MP_VERSION_STRING 0.44.0\\n292536439eeda58becdb7e441fe2e61ebb74529e
|
||||
|
||||
ARG BUILD_THREADS="8"
|
||||
|
||||
RUN apk add --no-cache \
|
||||
libgcc \
|
||||
libstdc++ \
|
||||
boost-system \
|
||||
boost-filesystem \
|
||||
boost-dev \
|
||||
luajit-dev \
|
||||
make \
|
||||
cmake \
|
||||
build-base \
|
||||
openssl-dev \
|
||||
ncurses \
|
||||
mesa-dev \
|
||||
bash \
|
||||
git \
|
||||
wget
|
||||
|
||||
RUN git clone --depth 1 -b "${TES3MP_VERSION}" https://github.com/TES3MP/openmw-tes3mp.git /tmp/TES3MP \
|
||||
&& git clone --depth 1 -b "${TES3MP_VERSION}" https://github.com/TES3MP/CoreScripts.git /tmp/CoreScripts \
|
||||
&& git clone https://github.com/TES3MP/CrabNet.git /tmp/CrabNet \
|
||||
&& git clone --depth 1 https://github.com/OpenMW/osg.git /tmp/osg
|
||||
|
||||
RUN cd /tmp/CrabNet \
|
||||
&& git reset --hard origin/master \
|
||||
&& git checkout 4eeeaad2f6c11aeb82070df35169694b4fb7b04b \
|
||||
&& mkdir build \
|
||||
&& cd build \
|
||||
&& cmake -DCMAKE_BUILD_TYPE=Release ..\
|
||||
&& cmake --build . --target RakNetLibStatic --config Release -- -j ${BUILD_THREADS}
|
||||
|
||||
RUN cd /tmp/osg \
|
||||
&& cmake .
|
||||
|
||||
COPY ScriptFunc.patch /
|
||||
|
||||
RUN cd /tmp/TES3MP \
|
||||
&& git apply /ScriptFunc.patch \
|
||||
&& mkdir build \
|
||||
&& cd build \
|
||||
&& RAKNET_ROOT=/tmp/CrabNet/build \
|
||||
cmake .. \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DBUILD_OPENMW_MP=ON \
|
||||
-DBUILD_OPENMW=OFF \
|
||||
-DBUILD_OPENCS=OFF \
|
||||
-DBUILD_BROWSER=OFF \
|
||||
-DBUILD_BSATOOL=OFF \
|
||||
-DBUILD_ESMTOOL=OFF \
|
||||
-DBUILD_ESSIMPORTER=OFF \
|
||||
-DBUILD_LAUNCHER=OFF \
|
||||
-DBUILD_MWINIIMPORTER=OFF \
|
||||
-DBUILD_MYGUI_PLUGIN=OFF \
|
||||
-DBUILD_OPENMW=OFF \
|
||||
-DBUILD_WIZARD=OFF \
|
||||
-DOPENSCENEGRAPH_INCLUDE_DIRS=/tmp/osg/include \
|
||||
&& make -j ${BUILD_THREADS}
|
||||
|
||||
RUN mv /tmp/TES3MP/build /server \
|
||||
&& mv /tmp/CoreScripts /server/CoreScripts \
|
||||
&& sed -i "s|home = .*|home = /server/data|g" /server/tes3mp-server-default.cfg \
|
||||
&& echo -e ${TES3MP_VERSION_STRING} > /server/resources/version \
|
||||
&& cp /tmp/TES3MP/tes3mp-credits.md /server/ \
|
||||
&& mkdir /server/data
|
||||
|
||||
FROM alpine:3.10
|
||||
|
||||
LABEL maintainer="Grim Kriegor <grimkriegor@krutt.org>"
|
||||
LABEL description="Docker image for the TES3MP server"
|
||||
|
||||
RUN apk add --no-cache \
|
||||
libgcc \
|
||||
libstdc++ \
|
||||
boost-system \
|
||||
boost-filesystem \
|
||||
boost-program_options \
|
||||
luajit \
|
||||
bash
|
||||
|
||||
COPY --from=builder /server /server
|
||||
ADD bootstrap.sh /bootstrap.sh
|
||||
|
||||
EXPOSE 25565/udp
|
||||
VOLUME /data
|
||||
|
||||
WORKDIR /server
|
||||
ENTRYPOINT [ "/bin/bash", "/bootstrap.sh", "--", "./tes3mp-server" ]
|
@ -0,0 +1,14 @@
|
||||
diff --git a/apps/openmw-mp/Script/ScriptFunction.cpp b/apps/openmw-mp/Script/ScriptFunction.cpp
|
||||
index 9a6d64206..d313a6be5 100644
|
||||
--- a/apps/openmw-mp/Script/ScriptFunction.cpp
|
||||
+++ b/apps/openmw-mp/Script/ScriptFunction.cpp
|
||||
@@ -40,6 +40,8 @@ boost::any ScriptFunction::Call(const vector<boost::any> &args)
|
||||
|
||||
if (def.length() != args.size())
|
||||
throw runtime_error("Script call: Number of arguments does not match definition");
|
||||
+ if (script_type == SCRIPT_CPP)
|
||||
+ fCpp();
|
||||
#if defined (ENABLE_LUA)
|
||||
else if (script_type == SCRIPT_LUA)
|
||||
{
|
||||
|
@ -0,0 +1,10 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Check if data folder is empty, bootstrap it if so
|
||||
if [ ! -d "/server/data/data" ]; then
|
||||
echo -e "Data folder empty, populating with CoreScripts"
|
||||
cp -a /server/CoreScripts/. /server/data/
|
||||
fi
|
||||
|
||||
# Execute the rest of the arguments as a command
|
||||
exec $@
|
@ -0,0 +1,19 @@
|
||||
[General]
|
||||
# The default localAddress of 0.0.0.0 makes the server reachable at all of its local addresses
|
||||
localAddress = 0.0.0.0
|
||||
port = 25565
|
||||
maximumPlayers = 64
|
||||
hostname = local test server
|
||||
# 0 - Verbose (spam), 1 - Info, 2 - Warnings, 3 - Errors, 4 - Only fatal errors
|
||||
logLevel = 1
|
||||
password =
|
||||
|
||||
[Plugins]
|
||||
home = /server/data
|
||||
plugins = serverCore.lua,nwahttp.so
|
||||
|
||||
[MasterServer]
|
||||
enabled = false
|
||||
address = master.tes3mp.com
|
||||
port = 25561
|
||||
rate = 10000
|
@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
cd "$(dirname "$(realpath "$0")")"
|
||||
docker build -t good-timer .
|
||||
|
||||
docker run -e RUST_BACKTRACE=full -p "25565:25565/udp" -p "8787:8787" -v "$PWD/config:/server/tes3mp-server-default.cfg" -v "$(realpath "$PWD/../target/debug/libnwahttp.so"):/server/data/scripts/nwahttp.so" -v "$PWD/data:/server/data" -ti good-timer
|
@ -0,0 +1,144 @@
|
||||
use crate::plugin::{create_timer, get_mod_dir, log_message, start_timer, Events, LOG_INFO};
|
||||
use crate::server::main_http_thread;
|
||||
use crate::server_info::ServerInfoHandle;
|
||||
|
||||
use std::os::raw::{c_int, c_ulonglong, c_ushort};
|
||||
use std::rc::Rc;
|
||||
use std::sync::{Arc, RwLock};
|
||||
use tokio::runtime::Runtime;
|
||||
use warp::Future;
|
||||
|
||||
mod plugin;
|
||||
mod server;
|
||||
mod server_info;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct ServerHandle(Arc<RwLock<Server>>, Rc<RwLock<Runtime>>);
|
||||
|
||||
extern "C" fn tick() -> c_ulonglong {
|
||||
let server_handle: &mut ServerHandle = unsafe { EVENTS_INSTANCE.as_mut() }.unwrap();
|
||||
server_handle.clone().with(|server| {
|
||||
let timer = server.timer;
|
||||
server.tick += 1;
|
||||
server_handle.block_on(async {
|
||||
server.info.update_players(server.tick % 20 == 0).await;
|
||||
});
|
||||
|
||||
server.tick %= 1000;
|
||||
|
||||
start_timer(timer)
|
||||
});
|
||||
|
||||
0
|
||||
}
|
||||
|
||||
impl ServerHandle {
|
||||
fn with<O>(&self, mut block: impl FnMut(&mut Server) -> O) -> O {
|
||||
let mut guard = self.0.write().unwrap();
|
||||
block(&mut guard)
|
||||
}
|
||||
|
||||
fn block_on<F: Future>(&mut self, future: F) -> F::Output {
|
||||
self.1.write().unwrap().block_on(future)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct Server {
|
||||
info: ServerInfoHandle,
|
||||
timer: c_int,
|
||||
tick: u64,
|
||||
}
|
||||
|
||||
impl Server {
|
||||
fn into_handle(self, runtime: Rc<RwLock<Runtime>>) -> ServerHandle {
|
||||
ServerHandle(Arc::new(RwLock::new(self)), runtime)
|
||||
}
|
||||
}
|
||||
|
||||
impl Events for ServerHandle {
|
||||
fn new() -> Self {
|
||||
Server {
|
||||
info: ServerInfoHandle::new(),
|
||||
timer: -1,
|
||||
tick: 0,
|
||||
}
|
||||
.into_handle(Rc::new(RwLock::new(
|
||||
Runtime::new().expect("Failed to create Tokio runtime"),
|
||||
)))
|
||||
}
|
||||
|
||||
fn on_any(&mut self, event_name: &str) {
|
||||
log_message(
|
||||
plugin::LOG_VERBOSE,
|
||||
format!("Got event: {}", event_name).as_str(),
|
||||
)
|
||||
}
|
||||
|
||||
fn on_gui_action(&mut self, player_id: u16, message_box_id: i32, data: Option<&str>) {
|
||||
self.clone().with(|server| {
|
||||
self.block_on(async {
|
||||
server
|
||||
.info
|
||||
.gui_action(player_id, message_box_id, data)
|
||||
.await;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn on_player_connect(&mut self, player_id: c_ushort) {
|
||||
self.clone().with(|server| -> () {
|
||||
self.block_on(async {
|
||||
server.info.add_player(player_id).await;
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
fn on_player_disconnect(&mut self, player_id: c_ushort) {
|
||||
self.clone().with(|server| {
|
||||
self.block_on(async {
|
||||
server.info.remove_player(player_id).await;
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn on_server_init(&mut self) {
|
||||
log_message(
|
||||
LOG_INFO,
|
||||
format!(
|
||||
concat!(
|
||||
"Loaded ",
|
||||
env!("CARGO_PKG_NAME"),
|
||||
" ",
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
" (pwd: {:?}, mod_dir: {})"
|
||||
),
|
||||
std::env::current_dir(),
|
||||
get_mod_dir()
|
||||
)
|
||||
.as_str(),
|
||||
);
|
||||
}
|
||||
|
||||
fn on_server_post_init(&mut self) {
|
||||
let mut info = {
|
||||
self.with(|server| {
|
||||
server.timer = create_timer(tick, 50);
|
||||
log_message(
|
||||
LOG_INFO,
|
||||
format!("nwahttp tick timer registered with id {}", server.timer).as_str(),
|
||||
);
|
||||
start_timer(server.timer);
|
||||
server.info.clone()
|
||||
})
|
||||
};
|
||||
|
||||
std::thread::spawn(move || {
|
||||
let info_clone = info.clone();
|
||||
info.block_on(async { main_http_thread(info_clone).await });
|
||||
});
|
||||
log_message(LOG_INFO, "Started HTTP thread");
|
||||
}
|
||||
}
|
||||
|
||||
use_events!(ServerHandle);
|
@ -0,0 +1 @@
|
||||
../extern/tes3mp-rs/tes3mp-plugin/src/plugin
|
@ -0,0 +1,94 @@
|
||||
use crate::plugin::*;
|
||||
use crate::server_info::{Player, ServerInfoHandle};
|
||||
use hyper::{header::CONTENT_TYPE, Body, Response};
|
||||
use lazy_static::lazy_static;
|
||||
use prometheus::{Encoder, TextEncoder};
|
||||
use std::{net::SocketAddr, str::FromStr};
|
||||
use warp;
|
||||
use warp::ws::Ws;
|
||||
use warp::{filters::path::end, Filter};
|
||||
|
||||
lazy_static! {
|
||||
static ref SERVER_VERSION: String = get_server_version();
|
||||
}
|
||||
|
||||
fn get_info() -> String {
|
||||
format!(
|
||||
concat!(
|
||||
"server: tes3mp {}\n",
|
||||
concat!(
|
||||
"plugin: ",
|
||||
env!("CARGO_PKG_NAME"),
|
||||
" ",
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
"\n"
|
||||
),
|
||||
"about:\n",
|
||||
" - https://github.com/TES3MP/openmw-tes3mp\n",
|
||||
" - https://github.com/teamnwah/nwahttp\n",
|
||||
),
|
||||
*SERVER_VERSION
|
||||
)
|
||||
}
|
||||
|
||||
async fn list_players(info: ServerInfoHandle) -> Vec<Player> {
|
||||
info.get_players().await
|
||||
}
|
||||
|
||||
pub async fn main_http_thread(info: ServerInfoHandle) {
|
||||
let fs = warp::fs::dir(get_mod_dir() + "/../www");
|
||||
|
||||
let index = warp::path("info").and(warp::path::end()).map(|| get_info());
|
||||
|
||||
let player_info = info.clone();
|
||||
let players = warp::path("api")
|
||||
.and(warp::path("players"))
|
||||
.and(end())
|
||||
.and_then(move || {
|
||||
let player_info = player_info.clone();
|
||||
async move {
|
||||
Ok(warp::reply::json(&list_players(player_info.clone()).await))
|
||||
as Result<_, warp::Rejection>
|
||||
}
|
||||
});
|
||||
|
||||
let server_info = info.clone();
|
||||
let player_websocket = warp::path("ws")
|
||||
.and(warp::path("players"))
|
||||
.and(warp::path::end())
|
||||
.and(warp::ws())
|
||||
.map(move |ws: Ws| {
|
||||
let server_info = server_info.clone();
|
||||
ws.on_upgrade(move |webs| {
|
||||
let server_info = server_info.clone();
|
||||
async move {
|
||||
let server_info = server_info.clone();
|
||||
server_info.add_websocket(webs).await;
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
let metrics_endpoint = warp::path("metrics").and(warp::path::end()).map(move || {
|
||||
let encoder = TextEncoder::new();
|
||||
let metric_families = prometheus::gather();
|
||||
let mut buffer = vec![];
|
||||
encoder.encode(&metric_families, &mut buffer).unwrap();
|
||||
Response::builder()
|
||||
.status(200)
|
||||
.header(CONTENT_TYPE, encoder.format_type())
|
||||
.body(Body::from(buffer))
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
let endpoint = warp::get().and(
|
||||
index
|
||||
.or(players)
|
||||
.or(player_websocket)
|
||||
.or(metrics_endpoint)
|
||||
.or(fs),
|
||||
);
|
||||
|
||||
warp::serve(endpoint)
|
||||
.run(SocketAddr::from_str("[::]:8787").expect("Invalid listen argument"))
|
||||
.await
|
||||
}
|
@ -0,0 +1,74 @@
|
||||
use lazy_static::lazy_static;
|
||||
use prometheus::*;
|
||||
|
||||
lazy_static! {
|
||||
pub static ref SKILL_LEVEL: IntGaugeVec = register_int_gauge_vec!(
|
||||
"openmw_player_skill_level",
|
||||
"The skill levels of players",
|
||||
&["player", "skill", "skill_id"]
|
||||
)
|
||||
.unwrap();
|
||||
pub static ref SKILL_PROGRESS: GaugeVec = register_gauge_vec!(
|
||||
"openmw_player_skill_progress",
|
||||
"The skill progress of players",
|
||||
&["player", "skill", "skill_id"]
|
||||
)
|
||||
.unwrap();
|
||||
pub static ref ATTRIBUTE_LEVEL: IntGaugeVec = register_int_gauge_vec!(
|
||||
"openmw_player_attr_level",
|
||||
"The attribute levels of players",
|
||||
&["player", "attribute", "attribute_id"]
|
||||
)
|
||||
.unwrap();
|
||||
pub static ref LEVEL: IntGaugeVec =
|
||||
register_int_gauge_vec!("openmw_player_level", "The level of players", &["player"])
|
||||
.unwrap();
|
||||
pub static ref LEVEL_PROGRESS: IntGaugeVec = register_int_gauge_vec!(
|
||||
"openmw_player_level_progress",
|
||||
"The level progress of players",
|
||||
&["player"]
|
||||
)
|
||||
.unwrap();
|
||||
pub static ref MAGICKA_BASE: GaugeVec = register_gauge_vec!(
|
||||
"openmw_player_magicka_base",
|
||||
"The base magicka of players",
|
||||
&["player"]
|
||||
)
|
||||
.unwrap();
|
||||
pub static ref MAGICKA: GaugeVec = register_gauge_vec!(
|
||||
"openmw_player_magicka",
|
||||
"The current magicka of players",
|
||||
&["player"]
|
||||
)
|
||||
.unwrap();
|
||||
pub static ref HEALTH_BASE: GaugeVec = register_gauge_vec!(
|
||||
"openmw_player_health_base",
|
||||
"The base health of players",
|
||||
&["player"]
|
||||
)
|
||||
.unwrap();
|
||||
pub static ref HEALTH: GaugeVec = register_gauge_vec!(
|
||||
"openmw_player_health",
|
||||
"The current health of players",
|
||||
&["player"]
|
||||
)
|
||||
.unwrap();
|
||||
pub static ref FATIGUE_BASE: GaugeVec = register_gauge_vec!(
|
||||
"openmw_player_fatigue_base",
|
||||
"The base fatigue of players",
|
||||
&["player"]
|
||||
)
|
||||
.unwrap();
|
||||
pub static ref FATIGUE: GaugeVec = register_gauge_vec!(
|
||||
"openmw_player_fatigue",
|
||||
"The current fatigue of players",
|
||||
&["player"]
|
||||
)
|
||||
.unwrap();
|
||||
pub static ref DISTANCE_TRAVELED: HistogramVec = register_histogram_vec!(
|
||||
"openmw_player_distance_traveled",
|
||||
"The amount of distance a player has travelled",
|
||||
&["player"]
|
||||
)
|
||||
.unwrap();
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
use crate::server_info::player_details::Player;
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Serialize, Clone, Debug)]
|
||||
#[serde(tag = "type")]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum WebsocketEvent {
|
||||
FullPlayer(FullPlayerEvent),
|
||||
PlayerPosition(PlayerPositionEvent),
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct FullPlayerEvent {
|
||||
pub players: Vec<Player>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PlayerPositionEvent {
|
||||
pub positions: Vec<PlayerPosition>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PlayerPosition {
|
||||
pub name: String,
|
||||
pub position: (f64, f64),
|
||||
pub rotation: f64,
|
||||
pub cell: String,
|
||||
pub is_outside: bool,
|
||||
}
|
@ -0,0 +1,152 @@
|
||||
use crate::server_info::events::{FullPlayerEvent, PlayerPositionEvent, WebsocketEvent};
|
||||
use crate::server_info::player_details::Player;
|
||||
use futures_util::SinkExt;
|
||||
use std::collections::HashMap;
|
||||
use std::future::Future;
|
||||
use std::os::raw::c_ushort;
|
||||
use std::sync::Arc;
|
||||
use tokio::runtime::{Handle, Runtime};
|
||||
use tokio::sync::{Mutex, RwLock};
|
||||
use warp::ws::{Message, WebSocket};
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub struct ServerInfo {
|
||||
pub players: HashMap<c_ushort, Player>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub struct ServerLogic {
|
||||
pub web_sockets: HashMap<u64, WebSocket>,
|
||||
}
|
||||
|
||||
type SyncMutex<T> = std::sync::Mutex<T>;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ServerInfoHandle {
|
||||
pub info: Arc<RwLock<ServerInfo>>,
|
||||
pub logic: Arc<Mutex<ServerLogic>>,
|
||||
pub runtime: Arc<SyncMutex<Runtime>>,
|
||||
pub handle: Arc<Handle>,
|
||||
}
|
||||
|
||||
impl ServerInfoHandle {
|
||||
pub fn new() -> Self {
|
||||
let runtime = Runtime::new().unwrap();
|
||||
let handle = runtime.handle().clone();
|
||||
|
||||
ServerInfoHandle {
|
||||
info: Arc::new(RwLock::new(ServerInfo::default())),
|
||||
logic: Arc::new(Mutex::new(ServerLogic::default())),
|
||||
runtime: Arc::new(SyncMutex::new(runtime)),
|
||||
handle: Arc::new(handle),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn publish_event(&self, event: WebsocketEvent) {
|
||||
let logic = self.logic.clone();
|
||||
|
||||
self.handle.spawn(async move {
|
||||
let mut logic = logic.lock().await;
|
||||
let json = serde_json::to_string(&event).unwrap();
|
||||
|
||||
let mut to_remove = vec![];
|
||||
for (id, web_socket) in &mut logic.web_sockets {
|
||||
if web_socket.send(Message::text(&json)).await.is_err() {
|
||||
to_remove.push(*id);
|
||||
}
|
||||
}
|
||||
|
||||
for id in to_remove {
|
||||
logic.web_sockets.remove(&id).map(|x| x.close());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub async fn gui_action(&self, player_id: u16, _message_box_id: i32, _data: Option<&str>) {
|
||||
let mut info = self.info.write().await;
|
||||
if let Some(player) = info.players.get_mut(&player_id) {
|
||||
if !player.logged_in {
|
||||
player.logged_in = true;
|
||||
player.on_login();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn update_players(&self, low_freq: bool) {
|
||||
let mut info = self.info.write().await;
|
||||
for (_, player) in &mut info.players {
|
||||
if !player.logged_in {
|
||||
continue;
|
||||
}
|
||||
|
||||
player.update();
|
||||
|
||||
if low_freq {
|
||||
player.low_frequency_update();
|
||||
}
|
||||
}
|
||||
|
||||
if info.players.len() == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let players = info.players.clone();
|
||||
self.publish_event(if low_freq {
|
||||
WebsocketEvent::FullPlayer(FullPlayerEvent {
|
||||
players: players
|
||||
.values()
|
||||
.filter(|p| p.logged_in)
|
||||
.map(|p| p.clone())
|
||||
.collect::<Vec<Player>>(),
|
||||
})
|
||||
} else {
|
||||
WebsocketEvent::PlayerPosition(PlayerPositionEvent {
|
||||
positions: players
|
||||
.values()
|
||||
.filter(|p| p.logged_in)
|
||||
.map(|p| p.get_player_position())
|
||||
.collect(),
|
||||
})
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
pub async fn add_player(&self, player_id: c_ushort) {
|
||||
let mut info = self.info.write().await;
|
||||
info.players.insert(player_id, Player::new(player_id));
|
||||
}
|
||||
|
||||
pub async fn remove_player(&self, player: c_ushort) {
|
||||
let mut info = self.info.write().await;
|
||||
info.players.remove(&player);
|
||||
|
||||
if info.players.len() == 0 {
|
||||
self.publish_event(WebsocketEvent::FullPlayer(FullPlayerEvent {
|
||||
players: vec![],
|
||||
}))
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_players(&self) -> Vec<Player> {
|
||||
let info = self.info.read().await;
|
||||
|
||||
info.players.iter().map(|p| (*p.1).clone()).collect()
|
||||
}
|
||||
|
||||
pub async fn add_websocket(&self, ws: WebSocket) {
|
||||
let mut logic = self.logic.lock().await;
|
||||
let new_id = logic
|
||||
.web_sockets
|
||||
.keys()
|
||||
.max()
|
||||
.map(|x| x + 1)
|
||||
.unwrap_or_default();
|
||||
println!("Added websocket ({})", new_id);
|
||||
logic.web_sockets.insert(new_id, ws);
|
||||
}
|
||||
|
||||
pub fn block_on<F: Future>(&mut self, task: F) -> F::Output {
|
||||
self.runtime.lock().unwrap().block_on(task)
|
||||
}
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
mod counters;
|
||||
mod events;
|
||||
mod logic;
|
||||
mod player_details;
|
||||
|
||||
pub use events::*;
|
||||
pub use logic::*;
|
||||
pub use player_details::*;
|
@ -0,0 +1,431 @@
|
||||
use crate::plugin::*;
|
||||
use crate::server_info::counters::*;
|
||||
use crate::server_info::events::PlayerPosition;
|
||||
use crate::server_info::Specialization::{Combat, Magic, Stealth};
|
||||
use serde::Serialize;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::os::raw::{c_double, c_int, c_ushort};
|
||||
|
||||
#[derive(Serialize, Debug, Copy, Clone, Default, PartialOrd, PartialEq)]
|
||||
pub struct Vec3 {
|
||||
pub x: c_double,
|
||||
pub y: c_double,
|
||||
pub z: c_double,
|
||||
}
|
||||
|
||||
impl Vec3 {
|
||||
fn distance(&self, rhs: Vec3) -> f64 {
|
||||
let x = self.x - rhs.x;
|
||||
let y = self.y - rhs.y;
|
||||
let z = self.z - rhs.z;
|
||||
|
||||
((x * x) + (y * y) + (z * z)).sqrt()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||
#[repr(u16)]
|
||||
#[allow(dead_code)]
|
||||
pub enum Attribute {
|
||||
Strength = 0,
|
||||
Intelligence = 1,
|
||||
Willpower = 2,
|
||||
Agility = 3,
|
||||
Speed = 4,
|
||||
Endurance = 5,
|
||||
Personality = 6,
|
||||
Luck = 7,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone, Debug, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AttributeValue {
|
||||
pub id: c_ushort,
|
||||
pub name: String,
|
||||
pub damage: c_double,
|
||||
pub modifier: c_int,
|
||||
pub base: c_int,
|
||||
}
|
||||
|
||||
impl AttributeValue {
|
||||
fn get(player_id: c_ushort, attribute_id: c_ushort) -> AttributeValue {
|
||||
AttributeValue {
|
||||
id: attribute_id,
|
||||
name: get_attribute_name(attribute_id),
|
||||
damage: get_attribute_damage(player_id, attribute_id),
|
||||
modifier: get_attribute_modifier(player_id, attribute_id),
|
||||
base: get_attribute_base(player_id, attribute_id),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||
#[repr(u16)]
|
||||
#[allow(dead_code)]
|
||||
pub enum Skill {
|
||||
Block = 0,
|
||||
Armorer = 1,
|
||||
MediumArmor = 2,
|
||||
HeavyArmor = 3,
|
||||
Blunt = 4,
|
||||
Longblade = 5,
|
||||
Axe = 6,
|
||||
Spear = 7,
|
||||
Athletics = 8,
|
||||
Enchant = 9,
|
||||
Destruction = 10,
|
||||
Alteration = 11,
|
||||
Illusion = 12,
|
||||
Conjuration = 13,
|
||||
Mysticism = 14,
|
||||
Restoration = 15,
|
||||
Alchemy = 16,
|
||||
Unarmored = 17,
|
||||
Security = 18,
|
||||
Sneak = 19,
|
||||
Acrobatics = 20,
|
||||
LightArmor = 21,
|
||||
Shortblade = 22,
|
||||
Marksman = 23,
|
||||
Mercantile = 24,
|
||||
Speechcraft = 25,
|
||||
HandToHand = 26,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, Clone, Copy, Ord, PartialOrd, Eq, PartialEq)]
|
||||
pub enum SkillType {
|
||||
Major,
|
||||
Minor,
|
||||
Misc,
|
||||
}
|
||||
|
||||
impl Default for SkillType {
|
||||
fn default() -> Self {
|
||||
SkillType::Misc
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone, Copy, Debug, Ord, PartialOrd, Eq, PartialEq)]
|
||||
#[repr(i32)]
|
||||
pub enum Specialization {
|
||||
Combat = 0,
|
||||
Magic = 1,
|
||||
Stealth = 2,
|
||||
None = 3,
|
||||
}
|
||||
|
||||
impl Specialization {
|
||||
fn get_for_skill(skill_id: u16) -> Specialization {
|
||||
if skill_id < 9 {
|
||||
Combat
|
||||
} else if skill_id < 18 {
|
||||
Magic
|
||||
} else if skill_id < 27 {
|
||||
Stealth
|
||||
} else {
|
||||
Specialization::None
|
||||
}
|
||||
}
|
||||
|
||||
fn get(id: c_int) -> Specialization {
|
||||
match id {
|
||||
0 => Specialization::Combat,
|
||||
1 => Specialization::Magic,
|
||||
2 => Specialization::Stealth,
|
||||
_ => Specialization::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Specialization {
|
||||
fn default() -> Self {
|
||||
Specialization::None
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone, Debug, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SkillValue {
|
||||
pub id: c_ushort,
|
||||
pub name: String,
|
||||
pub progress: c_double,
|
||||
pub base: c_int,
|
||||
pub increase: c_int,
|
||||
pub modifier: c_int,
|
||||
pub damage: c_double,
|
||||
pub progress_requirement: c_double,
|
||||
pub progress_percent: c_double,
|
||||
pub skill_type: SkillType,
|
||||
}
|
||||
|
||||
impl SkillValue {
|
||||
fn get(player_id: c_ushort, skill_id: c_ushort) -> Self {
|
||||
SkillValue {
|
||||
id: skill_id,
|
||||
name: get_skill_name(skill_id),
|
||||
progress: get_skill_progress(player_id, skill_id),
|
||||
base: get_skill_base(player_id, skill_id),
|
||||
increase: get_skill_increase(player_id, skill_id.into()),
|
||||
modifier: get_skill_modifier(player_id, skill_id),
|
||||
damage: get_skill_damage(player_id, skill_id),
|
||||
progress_requirement: 0f64,
|
||||
progress_percent: 0f64,
|
||||
skill_type: SkillType::Minor,
|
||||
}
|
||||
}
|
||||
|
||||
fn calculate_progress(&mut self, is_specialization: bool, skill_type: SkillType) {
|
||||
let mut requirement = (1 + self.base) as f64;
|
||||
self.skill_type = skill_type;
|
||||
|
||||
requirement *= match skill_type {
|
||||
SkillType::Major => 0.75,
|
||||
SkillType::Minor => 1.0,
|
||||
SkillType::Misc => 1.25,
|
||||
};
|
||||
|
||||
if is_specialization {
|
||||
requirement *= 0.8;
|
||||
}
|
||||
|
||||
self.progress_requirement = requirement;
|
||||
self.progress_percent = self.progress / requirement;
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<(c_double, c_double, c_double)> for Vec3 {
|
||||
fn into(self) -> (c_double, c_double, c_double) {
|
||||
(self.x, self.y, self.z)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(c_double, c_double, c_double)> for Vec3 {
|
||||
fn from(x: (c_double, c_double, c_double)) -> Self {
|
||||
Vec3::new(x.0, x.1, x.2)
|
||||
}
|
||||
}
|
||||
|
||||
impl Vec3 {
|
||||
pub fn new(x: c_double, y: c_double, z: c_double) -> Self {
|
||||
Self { x, y, z }
|
||||
}
|
||||
|
||||
pub fn get_position(player_id: c_ushort) -> Self {
|
||||
Self::new(
|
||||
get_pos_x(player_id),
|
||||
get_pos_y(player_id),
|
||||
get_pos_z(player_id),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn get_rotation(player_id: c_ushort) -> Self {
|
||||
Self::new(get_rot_x(player_id), 0.into(), get_rot_z(player_id))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone, Debug, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Player {
|
||||
pub id: c_ushort,
|
||||
pub name: String,
|
||||
pub head: String,
|
||||
pub hair: String,
|
||||
pub logged_in: bool,
|
||||
pub distance_travelled: f64,
|
||||
pub race: String,
|
||||
pub class: PlayerClass,
|
||||
pub cell: String,
|
||||
pub is_outside: bool,
|
||||
pub position: Vec3,
|
||||
pub rotation: Vec3,
|
||||
pub health: c_double,
|
||||
pub health_base: c_double,
|
||||
pub fatigue: c_double,
|
||||
pub fatigue_base: c_double,
|
||||
pub magicka: c_double,
|
||||
pub magicka_base: c_double,
|
||||
pub level: c_int,
|
||||
pub level_progress: c_int,
|
||||
pub attributes: Vec<AttributeValue>,
|
||||
pub skills: Vec<SkillValue>,
|
||||
pub major_skills: HashSet<c_ushort>,
|
||||
pub minor_skills: HashSet<c_ushort>,
|
||||
pub specialisation: Specialization,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone, Debug)]
|
||||
#[serde(tag = "type")]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum PlayerClass {
|
||||
Custom { name: String, description: String },
|
||||
Default { name: String },
|
||||
None,
|
||||
}
|
||||
|
||||
impl Default for PlayerClass {
|
||||
fn default() -> Self {
|
||||
PlayerClass::None
|
||||
}
|
||||
}
|
||||
|
||||
impl Player {
|
||||
pub fn new(id: c_ushort) -> Self {
|
||||
let mut player = Player::default();
|
||||
player.id = id;
|
||||
player.update();
|
||||
player.low_frequency_update();
|
||||
|
||||
player
|
||||
}
|
||||
|
||||
pub fn get_player_position(&self) -> PlayerPosition {
|
||||
PlayerPosition {
|
||||
name: self.name.clone(),
|
||||
position: (self.position.x, self.position.y),
|
||||
rotation: self.rotation.z,
|
||||
cell: self.cell.clone(),
|
||||
is_outside: self.is_outside,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_skill_type(&self, skill_id: c_ushort) -> SkillType {
|
||||
if self.major_skills.contains(&skill_id) {
|
||||
SkillType::Major
|
||||
} else if self.minor_skills.contains(&skill_id) {
|
||||
SkillType::Minor
|
||||
} else {
|
||||
SkillType::Misc
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(&mut self) {
|
||||
self.rotation = Vec3::get_rotation(self.id);
|
||||
self.is_outside = is_in_exterior(self.id);
|
||||
|
||||
let cell = get_cell(self.id);
|
||||
let position = Vec3::get_position(self.id);
|
||||
|
||||
if cell == self.cell {
|
||||
self.distance_travelled += self.position.distance(position)
|
||||
}
|
||||
|
||||
self.position = position;
|
||||
|
||||
self.cell = cell;
|
||||
self.health_base = get_health_base(self.id);
|
||||
self.health = get_health_current(self.id);
|
||||
self.fatigue_base = get_fatigue_base(self.id);
|
||||
self.fatigue = get_fatigue_current(self.id);
|
||||
self.magicka_base = get_magicka_base(self.id);
|
||||
self.magicka = get_magicka_current(self.id);
|
||||
self.level = get_level(self.id);
|
||||
self.level_progress = get_level_progress(self.id)
|
||||
}
|
||||
|
||||
pub fn update_once(&mut self) {
|
||||
self.name = get_name(self.id);
|
||||
self.race = get_race(self.id);
|
||||
self.head = get_head(self.id);
|
||||
self.hair = get_hair(self.id);
|
||||
|
||||
self.major_skills = HashSet::new();
|
||||
self.major_skills
|
||||
.insert(get_class_major_attribute(self.id, 0) as c_ushort);
|
||||
self.major_skills
|
||||
.insert(get_class_major_attribute(self.id, 1) as c_ushort);
|
||||
|
||||
self.minor_skills = HashSet::new();
|
||||
self.minor_skills
|
||||
.insert(get_class_minor_skill(self.id, 0) as c_ushort);
|
||||
self.minor_skills
|
||||
.insert(get_class_minor_skill(self.id, 1) as c_ushort);
|
||||
self.minor_skills
|
||||
.insert(get_class_minor_skill(self.id, 2) as c_ushort);
|
||||
self.minor_skills
|
||||
.insert(get_class_minor_skill(self.id, 3) as c_ushort);
|
||||
self.minor_skills
|
||||
.insert(get_class_minor_skill(self.id, 4) as c_ushort);
|
||||
|
||||
self.specialisation = Specialization::get(get_class_specialization(self.id));
|
||||
|
||||
let default_class = get_default_class(self.id);
|
||||
|
||||
if default_class.len() == 0 {
|
||||
self.class = PlayerClass::Custom {
|
||||
name: get_class_name(self.id),
|
||||
description: get_class_desc(self.id),
|
||||
}
|
||||
} else {
|
||||
self.class = PlayerClass::Default {
|
||||
name: default_class,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn low_frequency_update(&mut self) {
|
||||
self.attributes = (0..get_attribute_count() as c_ushort)
|
||||
.map(|id| AttributeValue::get(self.id, id))
|
||||
.collect();
|
||||
self.skills = (0..get_skill_count() as c_ushort)
|
||||
.map(|id| {
|
||||
let mut skill = SkillValue::get(self.id, id);
|
||||
skill.calculate_progress(
|
||||
self.specialisation == Specialization::get_for_skill(id),
|
||||
self.get_skill_type(id),
|
||||
);
|
||||
|
||||
skill
|
||||
})
|
||||
.collect();
|
||||
|
||||
if self.logged_in {
|
||||
self.report_stats();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn report_stats(&mut self) {
|
||||
for skill in &self.skills {
|
||||
let mut map = HashMap::new();
|
||||
map.insert("player", self.name.as_str());
|
||||
map.insert("skill", &skill.name.as_str());
|
||||
let id = skill.id.to_string();
|
||||
map.insert("skill_id", &id);
|
||||
SKILL_LEVEL.with(&map).set(skill.base as i64);
|
||||
SKILL_PROGRESS.with(&map).set(skill.progress_percent);
|
||||
}
|
||||
|
||||
for attribute in &self.attributes {
|
||||
let mut map = HashMap::new();
|
||||
map.insert("player", self.name.as_str());
|
||||
map.insert("attribute", &attribute.name.as_str());
|
||||
let id = attribute.id.to_string();
|
||||
map.insert("attribute_id", &id);
|
||||
ATTRIBUTE_LEVEL.with(&map).set(attribute.base as i64);
|
||||
}
|
||||
let mut map = HashMap::new();
|
||||
map.insert("player", self.name.as_str());
|
||||
|
||||
LEVEL.with(&map).set(self.level as i64);
|
||||
LEVEL_PROGRESS.with(&map).set(self.level_progress as i64);
|
||||
|
||||
MAGICKA_BASE.with(&map).set(self.magicka_base);
|
||||
MAGICKA.with(&map).set(self.magicka);
|
||||
|
||||
HEALTH_BASE.with(&map).set(self.health_base);
|
||||
HEALTH.with(&map).set(self.health);
|
||||
|
||||
FATIGUE_BASE.with(&map).set(self.fatigue_base);
|
||||
FATIGUE.with(&map).set(self.fatigue);
|
||||
|
||||
DISTANCE_TRAVELED
|
||||
.with(&map)
|
||||
.observe(self.distance_travelled);
|
||||
self.distance_travelled = 0.0;
|
||||
}
|
||||
|
||||
pub fn on_login(&mut self) {
|
||||
send_message(self.id, "#ff0000This server runs #0000ffnwahttp#ff0000 and this is it's obnoxious login message for #00ff00you#ff0000!!\n", false, false);
|
||||
self.update_once();
|
||||
self.low_frequency_update();
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue