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