Initial release

master
eater 4 years ago
parent 542dd6de65
commit 875c93afed
Signed by: eater
GPG Key ID: AD2560A0F84F0759

10
.gitignore vendored

@ -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…
Cancel
Save