I DID STUFF, GOD A PROC MACRO AND SHIT

This commit is contained in:
Rusty Striker 2024-09-22 23:46:51 +03:00
commit 22319e84a1
Signed by: RustyStriker
GPG key ID: 87E4D691632DFF15
28 changed files with 3101 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/target

2216
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

12
Cargo.toml Normal file
View file

@ -0,0 +1,12 @@
[package]
name = "open_tavern"
version = "0.1.0"
edition = "2021"
[dependencies]
axum = { version = "0.7.5", features = ["ws"] }
serde = "1.*.*"
serde_json = "1.*.*"
tokio = { version = "1.*.*", features = ["full"] }
sqlx = { version = "0.7.4", features = ["runtime-tokio", "sqlite"] }
tavern_macros = { version = "0.1.0", path = "tavern_macros" }

16
assets/sheet_lang_def.txt Normal file
View file

@ -0,0 +1,16 @@
Sheet -> Title Definitions;
Title -> '$' Name | [a-zA-Z0-9_\ ]+ | epsilon
// Title is either a variable whose value will be the title, constant text or none
// in the case of none, the first var will be the title
Definitions -> Definitions Definition | epsilon;
Definition -> Name ':' Type Requirements;
Name -> [a-zA-Z]+;
Type -> BOOL | INT | TEXT | TEXT '(' [0-9]+ ')' | EXP;
// ^^^^^ num of lines
EXP -> TERM '+' FACTOR | TERM '-' FACTOR;
TERM -> FACTOR '*' FACTOR | FACTOR '/' FACTOR;
FACTOR -> '(' EXP ')' | [0-9]+ | '$' Name(of type INT/BOOL);
// $Name of type bool will result in True = 1/False = 0
Requirements -> '|' Condition Requirements | epsilon;
Condition -> EXP RELOP EXP | '$' Name(of type BOOL);
RELOP -> (>|<|>=|<=|==|!=);

2
assets/web/index.html Normal file
View file

@ -0,0 +1,2 @@
<button onclick="onClick()", id="login_button">Click me!, also open terminal</button>
<script src='socket.js'></script>

15
assets/web/socket.js Normal file
View file

@ -0,0 +1,15 @@
const socket = new WebSocket('ws://localhost:3001/ws');
socket.addEventListener('open', e => { socket.send('hello server'); });
socket.addEventListener('message', e => {
console.log('message from server', e.data);
if (e.data == 'ok') {
document.getElementById('login_button').innerText = "logged in!";
}
});
function onClick() {
socket.send('login admin admin123');
}

31
db.md Normal file
View file

@ -0,0 +1,31 @@
# DATA base of operations
**Users**:
- Username - Unique
- Hashed password
- max managed tables (how many tables can the user create and make)
- table invites (pending)
**Tables**:
- UID (text)
- Table desc/name (text)
- Which users are in it (text)
- Who are the GMs (text)
- Owning user (text)
- Game type (name of the game) (text)
# Directories and Files
**Games**: Directories with the name of the game, with contents that define the game
// TODO figure out how to implement the game type itself
**Table**: game table
- **Game Settigns**: Game specific settings (might not be needed)
- **Entities** (dir?): Contains data about entities (such as players and NPCs)
- **Custom images** (dir): custom images for maps, tokens, entity portrait, cards and whatever else
- **Image Data**: What each image is used for, is it visible to all and location (custom or shared - server generic) and extra stuff like map constant data
- **Map data** (dir): "dynamic" data about maps, such as entities position and which entities are there

23
reqs.md Normal file
View file

@ -0,0 +1,23 @@
# Requests (and their responses)
## User initiated
- Login
- get available tabes
- get table data
- connect to table
- get map data
- table ping
- table action (this will prob be broken into different actions later):
- move token
-
- close table (admin closes the table)
- exit table (user exits the table)
## Server initiated
- push image
- table ping (transfer)
- table action (transfer)
- map update (switch map)
- close table (remove player from table)

18
src/api/game_actions.rs Normal file
View file

@ -0,0 +1,18 @@
//! General game actions
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize)]
pub struct Ping {
/// Which texture got pinged
pub texture: String,
/// Where the ping occured in the window
pub pos: (i32, i32),
}
#[derive(Serialize, Deserialize)]
pub struct ShowImage {
/// Which texture to show
pub texture: String
}

0
src/api/get_table.rs Normal file
View file

12
src/api/login.rs Normal file
View file

@ -0,0 +1,12 @@
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
pub struct LoginRequest {
pub username: String,
pub hashed_password: String,
}
#[derive(Serialize, Deserialize)]
pub struct LoginData {
// TODO: Figure out what the user needs on successful login to reduce traffic
}

16
src/api/map_actions.rs Normal file
View file

@ -0,0 +1,16 @@
//! Map specific game actions
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize)]
pub struct MoveToken {
/// Moved token
pub token: String,
/// Token original position - in case wall detection is required
pub from: (i32, i32),
/// Token final position
pub to: (i32, i32),
}
pub struct InsertToken {
pub token: String
}

12
src/api/mod.rs Normal file
View file

@ -0,0 +1,12 @@
pub mod login;
pub mod game_actions;
pub mod map_actions;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
pub struct ResultBase<T: Serialize> {
pub success: bool,
pub fail_reason: Option<String>,
pub data: Option<T>
}

View file

@ -0,0 +1,61 @@
pub trait CharacterSheet : Default {
/// Character sheet inputs (stuff that are not calculated from different items), such as Name, Age, Strength
fn inputs(&self) -> Vec<(String, EntryType)>;
/// All fields in the character sheet
fn fields(&self) -> Vec<(String, EntryType)>;
/// Character sheet to display, ordered. `None`s can be used to convey a seperator (of any sort)
fn display(&self) -> Vec<Option<(String, EntryType)>>;
/// Gets a character sheet entry value
fn get(&self, entry: &str) -> Option<EntryType>;
/// Sets a cahrater sheet entry value
fn set(&mut self, entry: &str, value: EntryType);
}
#[derive(Debug)]
pub enum EntryType {
Number(i32),
Text(String),
Bool(bool),
}
impl Default for EntryType {
fn default() -> Self {
EntryType::Number(0)
}
}
impl EntryType {
pub fn as_num(&self) -> i32 {
if let EntryType::Number(n) = *self {
n
}
else {
0
}
}
pub fn as_text(&self) -> String {
if let EntryType::Text(t) = self {
t.clone()
}
else {
String::new()
}
}
pub fn as_bool(&self) -> bool {
if let EntryType::Bool(b) = *self {
b
}
else {
false
}
}
}
impl std::fmt::Display for EntryType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match(self) {
EntryType::Number(n) => write!(f, "{}", n),
EntryType::Text(t) => write!(f, "{}", t),
EntryType::Bool(b) => write!(f, "{}", b),
}
}
}

33
src/game/interaction.rs Normal file
View file

@ -0,0 +1,33 @@
pub struct Interaction {
/// Max number of entities that can be targeted in one go, 0 for non.
pub max_targets: u32,
}
pub struct ChatMessage {
/// Optional text portion
text: String,
/// Optional action buttons, for a chat message this will be empty
actions: Vec<String>,
/// Source/Caster/Whoever initiated the action/sent the message
actor: String,
/// Targets of the action, for a chat message this will be empty
targets: Vec<String>
}
pub struct RollDialogOption {
/// Field name
name: String,
/// dice size (aka d6, d12, d20)
dice: u16,
/// amount of dice (aka 1d6, 2d12, 10d20)
dice_amount: u16,
/// Constant amout to add (+7, +3, -1)
constant: i16,
/// Extra data, like damage type
extra: String,
/// should be enabled by default
enabled: bool
}

5
src/game/mod.rs Normal file
View file

@ -0,0 +1,5 @@
//! Game Parser and Data types
pub mod character_sheet;
pub mod interaction;

5
src/lib.rs Normal file
View file

@ -0,0 +1,5 @@
pub mod user;
pub mod table;
pub mod api;
pub mod game;
pub mod pathfinder2r_impl;

84
src/main.rs Normal file
View file

@ -0,0 +1,84 @@
use axum::{
extract::ws, response, routing, Router
};
use open_tavern::{game::character_sheet::{CharacterSheet, EntryType}, pathfinder2r_impl};
#[tokio::main]
async fn main() {
let mut pf = pathfinder2r_impl::Pathfinder2rCharacterSheet::default();
pf.set("Dexterity", EntryType::Number(10));
pf.set("Name", EntryType::Text("AAAA".to_string()));
pf.set("Items", EntryType::Text("Short Sword, Dragonleather Helmet".to_string()));
let d = pf.display();
for e in d {
if let Some((s, v)) = e {
println!("{}: {}", s, v);
}
else {
println!();
}
}
println!();
// let app = Router::new()
// .route("/", routing::get(root))
// .route("/socket.js", routing::get(socket))
// .route("/ws", routing::get(ws_handler))
// ;
// let listener = tokio::net::TcpListener::bind("0.0.0.0:3001").await.unwrap();
// axum::serve(listener, app).await.unwrap();
}
async fn ws_handler(ws: ws::WebSocketUpgrade) -> impl axum::response::IntoResponse {
ws.on_upgrade(handle_socket)
}
async fn handle_socket(mut socket: ws::WebSocket) {
if socket.send(ws::Message::Text("This is my test message!".to_string())).await.is_ok() {
println!("pinged!");
}
let mut logged_in = false;
loop {
if let Some(msg) = socket.recv().await {
if let Ok(msg) = msg {
match msg {
ws::Message::Text(t) => {
println!("Got message: {}", t);
if !logged_in && t.starts_with("login") {
let mut split = t.splitn(2, ' ');
split.next(); // the login part
let user = split.next().unwrap();
if open_tavern::user::User::default_admin().login(user) {
socket.send(ws::Message::Text("ok".to_string())).await.unwrap();
logged_in = true;
}
else {
socket.send(ws::Message::Text("bad".to_string())).await.unwrap();
}
}
}
ws::Message::Binary(b) => println!("got bytes: {:?}", b),
ws::Message::Ping(_) => todo!(),
ws::Message::Pong(_) => todo!(),
ws::Message::Close(_) => break,
}
}
}
else {
break;
}
}
println!("Done with so-cat");
}
async fn root() -> axum::response::Html<&'static str> {
response::Html(include_str!("../assets/web/index.html"))
}
async fn socket() -> &'static str {
include_str!("../assets/web/socket.js")
}

View file

@ -0,0 +1,44 @@
use tavern_macros::CharacterSheet;
use crate::game::character_sheet::*;
#[derive(Default, CharacterSheet)]
pub struct Pathfinder2rCharacterSheet {
// Genral stuff
#[Input("Name")]
name: String,
#[Input("Items")]
items: String,
// Attributes
#[Seperator]
#[Input("Strength")]
str: i32,
#[Input("Dexterity")]
dex: i32,
#[Input("Constitution")]
con: i32,
#[Input("Intelligence")]
int: i32,
#[Input("Wisdom")]
wis: i32,
#[Input("Charisma")]
cha: i32,
// Skills
#[Seperator]
#[Field("Acrobatics")]
#[FieldExpr(self.dex + self.acrobatics_prof * 2)]
_acro: i32,
#[Input("Acrobatics Prof")]
acrobatics_prof: i32,
#[Field("Arcana")]
#[FieldExpr(self.int + self.arcana_prof * 2)]
_arca: i32,
#[Input("Arcana Prof")]
arcana_prof: i32,
#[Field("Crafting")]
#[FieldExpr(self.int + self.crafting_prof * 2)]
_crafting: i32,
#[Input("Crafting Prof")]
crafting_prof: i32,
// TODO: Add more skills
// TODO: Also add all the rest of the sheet items
}

9
src/table/entity.rs Normal file
View file

@ -0,0 +1,9 @@
use crate::user::User;
pub struct Entity {
/// Id will be an increasing u64, which might cause problems if the game has (way too) many entities
pub id: u64,
pub owner: User
}

4
src/table/mod.rs Normal file
View file

@ -0,0 +1,4 @@
mod texture;
pub mod entity;
pub use texture::*;

33
src/table/texture.rs Normal file
View file

@ -0,0 +1,33 @@
pub enum Grid {
Square { width: u32 },
Hexagon { width: u32 },
Triangle { width: u32 },
}
/// Texture directory source and file
pub enum TextureSource {
// TODO: I think os string would better fit, to do later
/// Server's shared textures directory
Shared(String),
/// Custom table directory
Custom(String),
/// Game shared textures directory
Game(String),
}
pub enum TextureType {
Image,
Token,
Map {
/// Grid type and size
grid: Grid,
/// grid offset from (0, 0)
grid_offset: (i32, i32),
}
}
pub struct Texture {
pub source: TextureSource,
pub ttype: TextureType,
}

16
src/user.rs Normal file
View file

@ -0,0 +1,16 @@
pub struct User {
username: String,
}
impl User {
pub fn default_admin() -> User {
User {
username: "admin".to_string(),
}
}
pub fn login(&self, username: &str) -> bool {
self.username == username
}
}

33
table_data.md Normal file
View file

@ -0,0 +1,33 @@
# Data types needed
- fields
+ functions
## Table - struct
- ID
- Users (who have access to the table in general)
- Admins/GMs (who is the Gm from the Users group)
- Map
- Images (non maps)
- Tokens - entities on a map
- Game definition
- User Character sheets
## Game definition
- Player sheets
- dice rolls
- abilities and such
## Map - struct
- Background
- Grid
- Visible to players
+ Force Show
## Background - struct
- Image
- Offset
## Grid - Enum
- Type: Square/Hexagonal/Triangular
- Size

101
tavern_macros/Cargo.lock generated Normal file
View file

@ -0,0 +1,101 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "darling"
version = "0.13.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c"
dependencies = [
"darling_core",
"darling_macro",
]
[[package]]
name = "darling_core"
version = "0.13.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "859d65a907b6852c9361e3185c862aae7fafd2887876799fa55f5f99dc40d610"
dependencies = [
"fnv",
"ident_case",
"proc-macro2",
"quote",
"strsim",
"syn",
]
[[package]]
name = "darling_macro"
version = "0.13.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835"
dependencies = [
"darling_core",
"quote",
"syn",
]
[[package]]
name = "fnv"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "ident_case"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]]
name = "proc-macro2"
version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
dependencies = [
"proc-macro2",
]
[[package]]
name = "strsim"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]]
name = "syn"
version = "1.0.109"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "tavern_macros"
version = "0.1.0"
dependencies = [
"darling",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "unicode-ident"
version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe"

13
tavern_macros/Cargo.toml Normal file
View file

@ -0,0 +1,13 @@
[package]
name = "tavern_macros"
version = "0.1.0"
edition = "2021"
[lib]
proc-macro = true
[dependencies]
darling = "0.13"
proc-macro2 = "1.0"
quote = "1.0"
syn = { version = "1.0", features = ["full"] }

258
tavern_macros/src/lib.rs Normal file
View file

@ -0,0 +1,258 @@
use proc_macro2::{Delimiter, Group, Span, TokenStream};
use quote::{quote, ToTokens, TokenStreamExt};
use syn::{parse_macro_input, DeriveInput, Ident};
#[proc_macro_derive(CharacterSheet, attributes(Input, Field, FieldExpr, Seperator))]
pub fn derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let DeriveInput { ident, data, .. } = parse_macro_input!(input as DeriveInput);
let mut output = quote! {
impl CharacterSheet for #ident
};
// Create inputs impl
let mut impls = impl_inputs(&data);
impls.extend(impl_fields(&data));
impls.extend(impl_display(&data));
impls.extend(impl_get(&data));
impls.extend(impl_set(&data));
output.append(Group::new(Delimiter::Brace, impls));
output.into()
}
fn get_type_ident(ty: &syn::Type) -> Option<Ident> {
match ty {
syn::Type::Path(p) => {
if p.path.is_ident("String") {
Some(Ident::new("Text", Span::call_site()))
} else if p.path.is_ident("i32") {
Some(Ident::new("Number", Span::call_site()))
} else if p.path.is_ident("bool") {
Some(Ident::new("Bool", Span::call_site()))
} else { None }
}
_ => panic!("Invalid data type"),
}
}
fn get_type_ident_set(ty: &syn::Type) -> Option<Ident> {
match ty {
syn::Type::Path(p) => {
if p.path.is_ident("String") {
Some(Ident::new("as_text", Span::call_site()))
} else if p.path.is_ident("i32") {
Some(Ident::new("as_num", Span::call_site()))
} else if p.path.is_ident("bool") {
Some(Ident::new("as_bool", Span::call_site()))
} else { None }
}
_ => panic!("Invalid data type"),
}
}
fn impl_inputs(data: &syn::Data) -> TokenStream {
let mut output = TokenStream::new();
output.extend(quote! {
fn inputs(&self) -> Vec<(String, EntryType)>
});
let mut items = TokenStream::new();
if let syn::Data::Struct(ds) = data {
if let syn::Fields::Named(fs) = &ds.fields {
for f in &fs.named {
let name = f.ident.clone().unwrap();
let attr = f.attrs.iter().find(|a| a.path.is_ident("Input"));
if let Some(attr) = attr {
let t = get_type_ident(&f.ty).expect(&format!("Invalid type for input: {}", name));
let arg: syn::LitStr = attr.parse_args().expect("No arguments supplied for Input attribute, usage: `Input(\"Name\")`");
items.extend(quote! {
(#arg.to_string(), EntryType::#t(self.#name.clone())),
})
}
}
}
}
let mut vec = quote! { Vec::from };
vec.append(Group::new(Delimiter::Parenthesis, Group::new(Delimiter::Bracket, items).to_token_stream()));
output.append(Group::new(Delimiter::Brace, vec));
output
}
fn impl_fields(data: &syn::Data) -> TokenStream {
let mut output = TokenStream::new();
output.extend(quote! {
fn fields(&self) -> Vec<(String, EntryType)>
});
let mut items = TokenStream::new();
if let syn::Data::Struct(ds) = data {
if let syn::Fields::Named(fs) = &ds.fields {
for f in &fs.named {
let name = f.ident.clone().unwrap();
let input_attr = f.attrs.iter().find(|a| a.path.is_ident("Input"));
if let Some(attr) = input_attr {
let t = get_type_ident(&f.ty).expect(&format!("Invalid type for input: {}", name));
let arg: syn::LitStr = attr.parse_args().expect("No arguments supplied for Input attribute, usage: `Input(\"Name\")`");
items.extend(quote! {
(#arg.to_string(), EntryType::#t(self.#name.clone())),
})
}
let field_attr = f.attrs.iter().find(|a| a.path.is_ident("Field"));
if let Some(attr) = field_attr {
let t = get_type_ident(&f.ty).expect(&format!("Invalid type for field: {}", name));
let arg: syn::LitStr = attr.parse_args().expect("Invalid arguments for Field attribute");
let field_expr = f.attrs.iter().find(|a| a.path.is_ident("FieldExpr"));
if let Some(exp) = field_expr {
let exp: syn::Expr = exp.parse_args().expect("Invalid expression");
items.extend(quote! {
(#arg.to_string(), EntryType::Number(#exp)),
})
}
else {
// No expression, guess we just push this and nothing special (should prob be an input but mehhh)
items.extend(quote! {
(#arg.to_string(), EntryType::#t(self.#name.clone())),
});
}
}
}
}
}
let mut vec = quote! { Vec::from };
vec.append(Group::new(Delimiter::Parenthesis, Group::new(Delimiter::Bracket, items).to_token_stream()));
output.append(Group::new(Delimiter::Brace, vec));
output
}
fn impl_get(data: &syn::Data) -> TokenStream {
let mut output = TokenStream::new();
output.extend(quote! {
fn get(&self, entry: &str) -> Option<EntryType>
});
let mut match_hands = TokenStream::new();
if let syn::Data::Struct(ds) = data {
if let syn::Fields::Named(fs) = &ds.fields {
for f in &fs.named {
let name = f.ident.clone().unwrap();
let input_attr = f.attrs.iter().find(|a| a.path.is_ident("Input"));
if let Some(attr) = input_attr {
let t = get_type_ident(&f.ty).expect(&format!("Invalid type for input: {}", name));
let arg: syn::LitStr = attr.parse_args().expect("No arguments supplied for Input attribute, usage: `Input(\"Name\")`");
match_hands.extend(quote! {
#arg => Some(EntryType::#t(self.#name.clone())),
})
}
let field_attr = f.attrs.iter().find(|a| a.path.is_ident("Field"));
if let Some(attr) = field_attr {
let t = get_type_ident(&f.ty).expect(&format!("Invalid type for field: {}", name));
let arg: syn::LitStr = attr.parse_args().expect("Invalid arguments for Field attribute");
let field_expr = f.attrs.iter().find(|a| a.path.is_ident("FieldExpr"));
if let Some(exp) = field_expr {
let exp: syn::Expr = exp.parse_args().expect("Invalid expression");
match_hands.extend(quote! {
#arg => Some(EntryType::Number(#exp)),
})
}
else {
// No expression, guess we just push this and nothing special (should prob be an input but mehhh)
match_hands.extend(quote! {
#arg => Some(EntryType::#t(self.#name.clone())),
});
}
}
}
}
}
match_hands.extend(quote! { _ => None, });
let mut vec = quote! { match(entry) };
vec.append(Group::new(Delimiter::Brace, match_hands));
output.append(Group::new(Delimiter::Brace, vec));
output
}
fn impl_set(data: &syn::Data) -> TokenStream {
let mut output = TokenStream::new();
output.extend(quote! {
fn set(&mut self, entry: &str, value: EntryType)
});
let mut match_hands = TokenStream::new();
if let syn::Data::Struct(ds) = data {
if let syn::Fields::Named(fs) = &ds.fields {
for f in &fs.named {
let name = f.ident.clone().unwrap();
let input_attr = f.attrs.iter().find(|a| a.path.is_ident("Input"));
if let Some(attr) = input_attr {
let t = get_type_ident_set(&f.ty).expect(&format!("Invalid type for input: {}", name));
let arg: syn::LitStr = attr.parse_args().expect("No arguments supplied for Input attribute, usage: `Input(\"Name\")`");
match_hands.extend(quote! {
#arg => self.#name = value.#t(),
})
}
}
}
}
match_hands.extend(quote! { _ => {} });
let mut vec = quote! { match(entry) };
vec.append(Group::new(Delimiter::Brace, match_hands));
output.append(Group::new(Delimiter::Brace, vec));
output
}
fn impl_display(data: &syn::Data) -> TokenStream {
let mut output = TokenStream::new();
output.extend(quote! {
fn display(&self) -> Vec<Option<(String, EntryType)>>
});
let mut items = TokenStream::new();
if let syn::Data::Struct(ds) = data {
if let syn::Fields::Named(fs) = &ds.fields {
for f in &fs.named {
let name = f.ident.clone().unwrap();
if f.attrs.iter().any(|a| a.path.is_ident("Seperator")) {
items.extend(quote! { None, })
}
let input_attr = f.attrs.iter().find(|a| a.path.is_ident("Input"));
if let Some(attr) = input_attr {
let t = get_type_ident(&f.ty).expect(&format!("Invalid type for input: {}", name));
let arg: syn::LitStr = attr.parse_args().expect("No arguments supplied for Input attribute, usage: `Input(\"Name\")`");
items.extend(quote! {
Some((#arg.to_string(), EntryType::#t(self.#name.clone()))),
})
}
let field_attr = f.attrs.iter().find(|a| a.path.is_ident("Field"));
if let Some(attr) = field_attr {
let t = get_type_ident(&f.ty).expect(&format!("Invalid type for field: {}", name));
let arg: syn::LitStr = attr.parse_args().expect("Invalid arguments for Field attribute");
let field_expr = f.attrs.iter().find(|a| a.path.is_ident("FieldExpr"));
if let Some(exp) = field_expr {
let exp: syn::Expr = exp.parse_args().expect("Invalid expression");
items.extend(quote! {
Some((#arg.to_string(), EntryType::Number(#exp))),
})
}
else {
// No expression, guess we just push this and nothing special (should prob be an input but mehhh)
items.extend(quote! {
Some((#arg.to_string(), EntryType::#t(self.#name.clone()))),
});
}
}
}
}
}
let mut vec = quote! { Vec::from };
vec.append(Group::new(Delimiter::Parenthesis, Group::new(Delimiter::Bracket, items).to_token_stream()));
output.append(Group::new(Delimiter::Brace, vec));
output
}

28
tests/macro_test.rs Normal file
View file

@ -0,0 +1,28 @@
use open_tavern::game::character_sheet::EntryType;
use tavern_macros::CharacterSheet;
#[test]
fn test_macro() {
trait CharacterSheet {
fn inputs(&self) -> Vec<(String, EntryType)>;
fn fields(&self) -> Vec<(String, EntryType)>;
fn get(&self, entry: &str) -> Option<EntryType>;
fn set(&mut self, entry: &str, value: EntryType);
}
#[derive(CharacterSheet)]
struct Testy {
a: String,
#[Input("AA")]
b: String,
#[Input("BB")]
c: String,
#[Input("Strength")]
str: i32,
#[Field("Athletics")]
#[FieldExpr(self.str + (self.athletics_trained as i32) * 2)]
athletics: i32,
#[Input("Trained Athletics")]
athletics_trained: bool,
}
}