1
0
Fork 0
mirror of https://codeberg.org/demostf/parser.git synced 2026-06-03 18:24:05 +02:00

rework baseline prop handling

This commit is contained in:
Robin Appelman 2022-08-06 01:20:26 +02:00
commit bae9acdd92
9 changed files with 135 additions and 105 deletions

1
Cargo.lock generated
View file

@ -1120,6 +1120,7 @@ dependencies = [
"err-derive", "err-derive",
"fnv", "fnv",
"iai", "iai",
"itertools",
"jemallocator", "jemallocator",
"main_error", "main_error",
"no-panic", "no-panic",

View file

@ -46,6 +46,7 @@ steamid-ng = "1.0.0"
schemars = { version = "0.8.10", optional = true } schemars = { version = "0.8.10", optional = true }
tracing = { version = "0.1.36", optional = true } tracing = { version = "0.1.36", optional = true }
tracing-subscriber = { version = "0.3.15", features = ["env-filter"], optional = true } tracing-subscriber = { version = "0.3.15", features = ["env-filter"], optional = true }
itertools = "0.10.3"
[features] [features]
schema = ["schemars", "bitbuffer/schemars"] schema = ["schemars", "bitbuffer/schemars"]

View file

@ -42,7 +42,7 @@ impl MessageHandler for PropAnalyzer {
matches!(message_type, MessageType::PacketEntities) matches!(message_type, MessageType::PacketEntities)
} }
fn handle_message(&mut self, message: &Message, _tick: u32) { fn handle_message(&mut self, message: &Message, _tick: u32, _parser_state: &ParserState) {
if let Message::PacketEntities(message) = message { if let Message::PacketEntities(message) = message {
for entity in &message.entities { for entity in &message.entities {
for prop in &entity.props { for prop in &entity.props {

View file

@ -12,6 +12,12 @@ pub enum MaybeUtf8String {
Invalid(Vec<u8>), Invalid(Vec<u8>),
} }
impl From<&'_ str> for MaybeUtf8String {
fn from(str: &'_ str) -> Self {
MaybeUtf8String::Valid(str.into())
}
}
impl Default for MaybeUtf8String { impl Default for MaybeUtf8String {
fn default() -> Self { fn default() -> Self {
MaybeUtf8String::Valid(String::new()) MaybeUtf8String::Valid(String::new())
@ -62,9 +68,15 @@ impl<'a, E: Endianness> BitRead<'a, E> for MaybeUtf8String {
match String::read(stream) { match String::read(stream) {
Ok(str) => Ok(MaybeUtf8String::Valid(str)), Ok(str) => Ok(MaybeUtf8String::Valid(str)),
Err(bitbuffer::BitError::Utf8Error(_, size)) => { Err(bitbuffer::BitError::Utf8Error(_, size)) => {
stream.set_pos(stream.pos() - size * 8)?; stream.set_pos(stream.pos().saturating_sub(size * 8))?;
let data = stream.read_sized(size)?; let mut data: Vec<u8> = stream.read_sized(size)?;
Ok(MaybeUtf8String::Invalid(data)) while data.last() == Some(&0) {
data.pop();
}
match String::from_utf8(data) {
Ok(str) => Ok(MaybeUtf8String::Valid(str)),
Err(e) => Ok(MaybeUtf8String::Invalid(e.into_bytes())),
}
} }
Err(e) => Err(e), Err(e) => Err(e),
} }

View file

@ -1,6 +1,7 @@
use bitbuffer::{BitRead, BitReadSized, BitWrite, BitWriteSized, BitWriteStream, LittleEndian}; use bitbuffer::{BitRead, BitReadSized, BitWrite, BitWriteSized, BitWriteStream, LittleEndian};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_repr::{Deserialize_repr, Serialize_repr}; use serde_repr::{Deserialize_repr, Serialize_repr};
use std::borrow::Cow;
use crate::demo::message::stringtable::log_base2; use crate::demo::message::stringtable::log_base2;
use crate::demo::packet::datatable::{ClassId, SendTable}; use crate::demo::packet::datatable::{ClassId, SendTable};
@ -10,6 +11,7 @@ use crate::{Parse, ParseError, ParserState, ReadResult, Result, Stream};
use parse_display::{Display, FromStr}; use parse_display::{Display, FromStr};
use std::cmp::{min, Ordering}; use std::cmp::{min, Ordering};
use itertools::Either;
use std::fmt; use std::fmt;
use std::num::NonZeroU32; use std::num::NonZeroU32;
#[cfg(feature = "trace")] #[cfg(feature = "trace")]
@ -87,12 +89,13 @@ pub enum UpdateType {
pub struct PacketEntity { pub struct PacketEntity {
pub server_class: ClassId, pub server_class: ClassId,
pub entity_index: EntityId, pub entity_index: EntityId,
pub baseline_props: Vec<SendProp>,
pub props: Vec<SendProp>, pub props: Vec<SendProp>,
pub in_pvs: bool, pub in_pvs: bool,
pub update_type: UpdateType, pub update_type: UpdateType,
pub serial_number: u32, pub serial_number: u32,
pub delay: Option<f32>, pub delay: Option<f32>,
pub delta: Option<u32>,
pub baseline_index: usize,
} }
impl fmt::Display for PacketEntity { impl fmt::Display for PacketEntity {
@ -110,8 +113,13 @@ impl PacketEntity {
self.props.iter_mut().find(|prop| prop.identifier == *index) self.props.iter_mut().find(|prop| prop.identifier == *index)
} }
pub fn get_prop_by_identifier(&self, index: &SendPropIdentifier) -> Option<&SendProp> { pub fn get_prop_by_identifier(
self.props().find(|prop| prop.identifier == *index) &self,
index: &SendPropIdentifier,
parser_state: &ParserState,
) -> Option<SendProp> {
self.props(parser_state)
.find(|prop| prop.identifier == *index)
} }
pub fn apply_update(&mut self, props: &[SendProp]) { pub fn apply_update(&mut self, props: &[SendProp]) {
@ -123,19 +131,42 @@ impl PacketEntity {
} }
} }
pub fn get_prop_by_name(&self, table_name: &str, name: &str) -> Option<&SendProp> { pub fn get_prop_by_name(
&self,
table_name: &str,
name: &str,
parser_state: &ParserState,
) -> Option<SendProp> {
let identifier = SendPropIdentifier::new(table_name, name); let identifier = SendPropIdentifier::new(table_name, name);
self.get_prop_by_identifier(&identifier) self.get_prop_by_identifier(&identifier, parser_state)
} }
pub fn props(&self) -> impl Iterator<Item = &SendProp> { fn get_baseline_props<'a>(&self, parser_state: &'a ParserState) -> Cow<'a, [SendProp]> {
self.baseline_props.iter().chain(self.props.iter()) parser_state
.get_baseline(
self.baseline_index,
self.entity_index,
self.server_class,
&parser_state.send_tables[usize::from(self.server_class)],
self.delta.is_some(),
)
.unwrap_or_default()
} }
pub fn into_props(self) -> impl Iterator<Item = SendProp> { pub fn props<'a>(
self.baseline_props &'a self,
.into_iter() parser_state: &'a ParserState,
.chain(self.props.into_iter()) ) -> impl Iterator<Item = SendProp> + 'a {
if self.update_type == UpdateType::Enter {
Either::Left(
self.get_baseline_props(parser_state)
.into_owned()
.into_iter()
.chain(self.props.iter().cloned()),
)
} else {
Either::Right(self.props.iter().cloned())
}
} }
} }
@ -230,12 +261,13 @@ fn get_entity_for_update(
Ok(PacketEntity { Ok(PacketEntity {
server_class: class_id, server_class: class_id,
entity_index, entity_index,
baseline_props: vec![],
props: Vec::with_capacity(8), props: Vec::with_capacity(8),
in_pvs: false, in_pvs: false,
update_type, update_type,
serial_number: 0, serial_number: 0,
delay: None, delay: None,
delta: None,
baseline_index: 0,
}) })
} }
@ -262,13 +294,8 @@ impl Parse<'_> for PacketEntitiesMessage {
let update_type = data.read()?; let update_type = data.read()?;
if update_type == UpdateType::Enter { if update_type == UpdateType::Enter {
let mut entity = Self::read_enter( let mut entity =
&mut data, Self::read_enter(&mut data, entity_index, state, base_line as usize, delta)?;
entity_index,
state,
base_line as usize,
delta.is_some(),
)?;
let send_table = get_send_table(state, entity.server_class)?; let send_table = get_send_table(state, entity.server_class)?;
Self::read_update(&mut data, send_table, &mut entity.props, entity_index)?; Self::read_update(&mut data, send_table, &mut entity.props, entity_index)?;
@ -287,12 +314,13 @@ impl Parse<'_> for PacketEntitiesMessage {
entities.push(PacketEntity { entities.push(PacketEntity {
server_class: 0.into(), server_class: 0.into(),
entity_index, entity_index,
baseline_props: vec![],
props: vec![], props: vec![],
in_pvs: false, in_pvs: false,
update_type, update_type,
serial_number: 0, serial_number: 0,
delay: None, delay: None,
delta,
baseline_index: 0,
}); });
} }
} }
@ -372,34 +400,23 @@ impl PacketEntitiesMessage {
entity_index: EntityId, entity_index: EntityId,
state: &ParserState, state: &ParserState,
baseline_index: usize, baseline_index: usize,
is_delta: bool, delta: Option<u32>,
) -> Result<PacketEntity> { ) -> Result<PacketEntity> {
let bits = log_base2(state.server_classes.len()) + 1; let bits = log_base2(state.server_classes.len()) + 1;
let class_index: ClassId = stream.read_sized::<u16>(bits as usize)?.into(); let class_index: ClassId = stream.read_sized::<u16>(bits as usize)?.into();
let serial = stream.read_sized(10)?; let serial = stream.read_sized(10)?;
let send_table = state
.send_tables
.get(usize::from(class_index))
.ok_or(ParseError::UnknownServerClass(class_index))?;
let baseline_props = state.get_baseline(
baseline_index,
entity_index,
class_index,
send_table,
is_delta,
)?;
Ok(PacketEntity { Ok(PacketEntity {
server_class: class_index, server_class: class_index,
entity_index, entity_index,
baseline_props,
props: vec![], props: vec![],
in_pvs: true, in_pvs: true,
update_type: UpdateType::Enter, update_type: UpdateType::Enter,
serial_number: serial, serial_number: serial,
delay: None, delay: None,
delta,
baseline_index,
}) })
} }
@ -583,12 +600,13 @@ fn test_packet_entitier_message_roundtrip() {
entities: vec![PacketEntity { entities: vec![PacketEntity {
server_class: ClassId::from(0), server_class: ClassId::from(0),
entity_index: Default::default(), entity_index: Default::default(),
baseline_props: vec![],
props: vec![], props: vec![],
in_pvs: true, in_pvs: true,
update_type: UpdateType::Enter, update_type: UpdateType::Enter,
serial_number: 0, serial_number: 0,
delay: None, delay: None,
delta: None,
baseline_index: 0,
}], }],
removed_entities: vec![], removed_entities: vec![],
max_entries: 4, max_entries: 4,
@ -604,17 +622,17 @@ fn test_packet_entitier_message_roundtrip() {
PacketEntity { PacketEntity {
server_class: ClassId::from(0), server_class: ClassId::from(0),
entity_index: EntityId::from(0u32), entity_index: EntityId::from(0u32),
baseline_props: vec![],
props: vec![], props: vec![],
in_pvs: true, in_pvs: true,
update_type: UpdateType::Enter, update_type: UpdateType::Enter,
serial_number: 0, serial_number: 0,
delay: None, delay: None,
delta: None,
baseline_index: 0,
}, },
PacketEntity { PacketEntity {
server_class: ClassId::from(1), server_class: ClassId::from(1),
entity_index: EntityId::from(4u32), entity_index: EntityId::from(4u32),
baseline_props: vec![],
props: vec![ props: vec![
SendProp { SendProp {
index: 0, index: 0,
@ -631,11 +649,14 @@ fn test_packet_entitier_message_roundtrip() {
update_type: UpdateType::Preserve, update_type: UpdateType::Preserve,
serial_number: 0, serial_number: 0,
delay: None, delay: None,
delta: None,
baseline_index: 0,
}, },
PacketEntity { PacketEntity {
server_class: ClassId::from(1), server_class: ClassId::from(1),
entity_index: EntityId::from(5u32), entity_index: EntityId::from(5u32),
baseline_props: vec![], delta: None,
baseline_index: 0,
props: vec![ props: vec![
SendProp { SendProp {
index: 0, index: 0,

View file

@ -160,10 +160,10 @@ impl MessageHandler for GameStateAnalyser {
matches!(message_type, MessageType::PacketEntities) matches!(message_type, MessageType::PacketEntities)
} }
fn handle_message(&mut self, message: &Message, _tick: u32, _parser_state: &ParserState) { fn handle_message(&mut self, message: &Message, _tick: u32, parser_state: &ParserState) {
if let Message::PacketEntities(message) = message { if let Message::PacketEntities(message) = message {
for entity in &message.entities { for entity in &message.entities {
self.handle_entity(entity); self.handle_entity(entity, parser_state);
} }
} }
} }
@ -221,22 +221,22 @@ impl GameStateAnalyser {
Self::default() Self::default()
} }
pub fn handle_entity(&mut self, entity: &PacketEntity) { pub fn handle_entity(&mut self, entity: &PacketEntity, parser_state: &ParserState) {
let class_name: &str = self let class_name: &str = self
.class_names .class_names
.get(usize::from(entity.server_class)) .get(usize::from(entity.server_class))
.map(|class_name| class_name.as_str()) .map(|class_name| class_name.as_str())
.unwrap_or(""); .unwrap_or("");
match class_name { match class_name {
"CTFPlayer" => self.handle_player_entity(entity), "CTFPlayer" => self.handle_player_entity(entity, parser_state),
"CTFPlayerResource" => self.handle_player_resource(entity), "CTFPlayerResource" => self.handle_player_resource(entity, parser_state),
"CWorld" => self.handle_world_entity(entity), "CWorld" => self.handle_world_entity(entity, parser_state),
_ => {} _ => {}
} }
} }
pub fn handle_player_resource(&mut self, entity: &PacketEntity) { pub fn handle_player_resource(&mut self, entity: &PacketEntity, parser_state: &ParserState) {
for prop in entity.props() { for prop in entity.props(parser_state) {
if let Some((table_name, prop_name)) = self.prop_names.get(&prop.identifier) { if let Some((table_name, prop_name)) = self.prop_names.get(&prop.identifier) {
if let Ok(player_id) = u32::from_str(prop_name.as_str()) { if let Ok(player_id) = u32::from_str(prop_name.as_str()) {
let entity_id = EntityId::from(player_id); let entity_id = EntityId::from(player_id);
@ -267,7 +267,7 @@ impl GameStateAnalyser {
} }
} }
pub fn handle_player_entity(&mut self, entity: &PacketEntity) { pub fn handle_player_entity(&mut self, entity: &PacketEntity, parser_state: &ParserState) {
let player = self.state.get_or_create_player(entity.entity_index); let player = self.state.get_or_create_player(entity.entity_index);
const HEALTH_PROP: SendPropIdentifier = const HEALTH_PROP: SendPropIdentifier =
@ -294,7 +294,7 @@ impl GameStateAnalyser {
const NON_LOCAL_PITCH_ANGLES: SendPropIdentifier = const NON_LOCAL_PITCH_ANGLES: SendPropIdentifier =
SendPropIdentifier::new("DT_TFNonLocalPlayerExclusive", "m_angEyeAngles[0]"); SendPropIdentifier::new("DT_TFNonLocalPlayerExclusive", "m_angEyeAngles[0]");
for prop in entity.props() { for prop in entity.props(parser_state) {
match prop.identifier { match prop.identifier {
HEALTH_PROP => { HEALTH_PROP => {
player.health = i64::try_from(&prop.value).unwrap_or_default() as u16 player.health = i64::try_from(&prop.value).unwrap_or_default() as u16
@ -324,7 +324,7 @@ impl GameStateAnalyser {
} }
} }
pub fn handle_world_entity(&mut self, entity: &PacketEntity) { pub fn handle_world_entity(&mut self, entity: &PacketEntity, parser_state: &ParserState) {
if let ( if let (
Some(SendProp { Some(SendProp {
value: SendPropValue::Vector(boundary_min), value: SendPropValue::Vector(boundary_min),
@ -335,12 +335,12 @@ impl GameStateAnalyser {
.. ..
}), }),
) = ( ) = (
entity.get_prop_by_name("DT_WORLD", "m_WorldMins"), entity.get_prop_by_name("DT_WORLD", "m_WorldMins", parser_state),
entity.get_prop_by_name("DT_WORLD", "m_WorldMaxs"), entity.get_prop_by_name("DT_WORLD", "m_WorldMaxs", parser_state),
) { ) {
self.state.world = Some(World { self.state.world = Some(World {
boundary_min: *boundary_min, boundary_min,
boundary_max: *boundary_max, boundary_max,
}) })
} }
} }

View file

@ -2,7 +2,7 @@ use crate::demo::message::{Message, MessageType};
use crate::demo::packet::datatable::{ParseSendTable, ServerClass}; use crate::demo::packet::datatable::{ParseSendTable, ServerClass};
use crate::demo::packet::stringtable::{StringTable, StringTableEntry}; use crate::demo::packet::stringtable::{StringTable, StringTableEntry};
use crate::demo::packet::Packet; use crate::demo::packet::Packet;
use crate::{ParseError, Result}; use crate::Result;
use crate::demo::header::Header; use crate::demo::header::Header;
use crate::demo::packet::message::MessagePacketMeta; use crate::demo::packet::message::MessagePacketMeta;
@ -110,7 +110,6 @@ impl<'a, T: MessageHandler> DemoHandler<'a, T> {
} }
pub fn handle_packet(&mut self, packet: Packet<'a>) -> Result<()> { pub fn handle_packet(&mut self, packet: Packet<'a>) -> Result<()> {
let mut baselines_updated = false;
match packet { match packet {
Packet::DataTables(packet) => { Packet::DataTables(packet) => {
self.handle_data_table(packet.tables, packet.server_classes)?; self.handle_data_table(packet.tables, packet.server_classes)?;
@ -123,7 +122,6 @@ impl<'a, T: MessageHandler> DemoHandler<'a, T> {
Packet::Message(packet) | Packet::Signon(packet) => { Packet::Message(packet) | Packet::Signon(packet) => {
self.analyser self.analyser
.handle_packet_meta(packet.tick, &packet.meta, &self.state_handler); .handle_packet_meta(packet.tick, &packet.meta, &self.state_handler);
//self.tick = packet.tick;
for message in packet.messages { for message in packet.messages {
match message { match message {
Message::NetTick(message) => self.tick = message.tick, Message::NetTick(message) => self.tick = message.tick,
@ -131,28 +129,9 @@ impl<'a, T: MessageHandler> DemoHandler<'a, T> {
self.handle_string_table(message.table) self.handle_string_table(message.table)
} }
Message::UpdateStringTable(message) => { Message::UpdateStringTable(message) => {
baselines_updated = true;
self.handle_table_update(message.table_id, message.entries) self.handle_table_update(message.table_id, message.entries)
} }
Message::PacketEntities(mut msg) => { Message::PacketEntities(msg) => {
if baselines_updated {
// if baselines were updated in the same packet, the newly added
// static baselines wont be used yet, patch it up afterward
for ent in msg.entities.iter_mut() {
if ent.baseline_props.is_empty() {
let send_table = self
.state_handler
.send_tables
.get(usize::from(ent.server_class))
.ok_or(ParseError::UnknownServerClass(
ent.server_class,
))?;
ent.baseline_props = self
.state_handler
.get_static_baseline(ent.server_class, send_table)?;
}
}
}
self.handle_message(Message::PacketEntities(msg)) self.handle_message(Message::PacketEntities(msg))
} }
message => self.handle_message(message), message => self.handle_message(message),

View file

@ -1,4 +1,5 @@
use fnv::FnvHashMap; use fnv::FnvHashMap;
use std::borrow::Cow;
use std::collections::HashMap; use std::collections::HashMap;
use crate::demo::gamevent::GameEventDefinition; use crate::demo::gamevent::GameEventDefinition;
@ -97,24 +98,35 @@ impl<'a> ParserState {
class_id: ClassId, class_id: ClassId,
send_table: &SendTable, send_table: &SendTable,
) -> Result<Vec<SendProp>> { ) -> Result<Vec<SendProp>> {
let mut cached = self.parsed_static_baselines.borrow_mut(); match self.static_baselines.get(&class_id) {
Ok(match cached.get(&class_id) { Some(static_baseline) => static_baseline.parse(send_table),
Some(props) => props.clone(), None => {
None => match self.static_baselines.get(&class_id) { #[cfg(feature = "trace")]
Some(static_baseline) => { warn!(
let props = static_baseline.parse(send_table)?; class_id = display(class_id),
cached.entry(class_id).or_insert(props).clone() "class without static baseline"
} );
None => { Ok(Vec::new())
#[cfg(feature = "trace")] }
warn!( }
class_id = display(class_id), // let mut cached = self.parsed_static_baselines.borrow_mut();
"class without static baseline" // Ok(match cached.entry(class_id) {
); // Entry::Occupied(entry) => entry.get().as_slice(),
Vec::with_capacity(8) // Entry::Vacant(entry) => match self.static_baselines.get(&class_id) {
} // Some(static_baseline) => {
}, // let props = static_baseline.parse(send_table)?;
}) // entry.insert(props).as_slice()
// }
// None => {
// #[cfg(feature = "trace")]
// warn!(
// class_id = display(class_id),
// "class without static baseline"
// );
// &[]
// }
// },
// })
} }
pub fn get_baseline( pub fn get_baseline(
@ -124,20 +136,22 @@ impl<'a> ParserState {
class_id: ClassId, class_id: ClassId,
send_table: &SendTable, send_table: &SendTable,
is_delta: bool, is_delta: bool,
) -> Result<Vec<SendProp>> { ) -> Result<Cow<[SendProp]>> {
match self.instance_baselines[baseline_index].get(entity_index) { match self.instance_baselines[baseline_index].get(entity_index) {
Some(baseline) if baseline.server_class == class_id && is_delta => { Some(baseline) if baseline.server_class == class_id && is_delta => {
Ok(baseline.props.clone()) Ok(Cow::Borrowed(&baseline.props))
} }
_ => match self.static_baselines.get(&class_id) { _ => match self.static_baselines.get(&class_id) {
Some(_static_baseline) => self.get_static_baseline(class_id, send_table), Some(_static_baseline) => {
Ok(Cow::Owned(self.get_static_baseline(class_id, send_table)?))
}
None => { None => {
#[cfg(feature = "trace")] #[cfg(feature = "trace")]
warn!( warn!(
class_id = display(class_id), class_id = display(class_id),
"class without static baseline" "class without static baseline"
); );
Ok(Vec::with_capacity(8)) Ok(Cow::Owned(Vec::new()))
} }
}, },
} }
@ -381,12 +395,13 @@ impl From<BaselineEntity> for PacketEntity {
PacketEntity { PacketEntity {
server_class: baseline.server_class, server_class: baseline.server_class,
entity_index: baseline.entity_id, entity_index: baseline.entity_id,
baseline_props: vec![],
props: baseline.props, props: baseline.props,
in_pvs: false, in_pvs: false,
update_type: UpdateType::Enter, update_type: UpdateType::Enter,
serial_number: baseline.serial, serial_number: baseline.serial,
delay: None, delay: None,
delta: None,
baseline_index: 0,
} }
} }
} }

View file

@ -52,6 +52,7 @@ impl EntityDump {
tick: u32, tick: u32,
classes: &[ServerClass], classes: &[ServerClass],
prop_names: &FnvHashMap<SendPropIdentifier, (SendTableName, SendPropName)>, prop_names: &FnvHashMap<SendPropIdentifier, (SendTableName, SendPropName)>,
state: &ParserState,
) -> Self { ) -> Self {
EntityDump { EntityDump {
tick, tick,
@ -59,7 +60,7 @@ impl EntityDump {
id: entity.entity_index, id: entity.entity_index,
pvs: entity.update_type.into(), pvs: entity.update_type.into(),
props: entity props: entity
.into_props() .props(state)
.map(|prop| { .map(|prop| {
let (table_name, prop_name) = &prop_names[&prop.identifier]; let (table_name, prop_name) = &prop_names[&prop.identifier];
(format!("{}.{}", table_name, prop_name), prop.value) (format!("{}.{}", table_name, prop_name), prop.value)
@ -126,7 +127,7 @@ impl MessageHandler for EntityDumper {
self.entities self.entities
.into_iter() .into_iter()
.map(|(tick, entity)| { .map(|(tick, entity)| {
EntityDump::from_entity(entity, tick, &state.server_classes, &prop_names) EntityDump::from_entity(entity, tick, &state.server_classes, &prop_names, state)
}) })
.collect() .collect()
} }