Address zesterer's comments

This commit is contained in:
Christof Petig 2023-10-17 22:10:19 +02:00
parent 22f8433d22
commit e22046c5f4
7 changed files with 95 additions and 78 deletions

View File

@ -27,7 +27,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- New arena building in desert cities, suitable for PVP, also NPCs like to watch the fights too
- The loading screen now displays status updates for singleplayer server and client initialization progress
- New Frost Gigas attacks & AI
- Weapons and armor load from plugins
- Allow plugins to add weapon and armor items
### Changed

View File

@ -25,5 +25,4 @@ walkdir = "2.3.2"
[features]
hot-reloading = ["assets_manager/hot-reloading"]
asset_tweak = ["dep:serde", "hot-reloading"]
hashbrown = ["dep:hashbrown"]
plugins = ["dep:serde", "dep:tar"]
plugins = ["dep:serde", "dep:tar", "dep:hashbrown"]

View File

@ -245,7 +245,7 @@ impl<V> Concatenate for Vec<V> {
}
}
#[cfg(feature = "hashbrown")]
#[cfg(feature = "plugins")]
impl<K: Eq + Hash, V, S: BuildHasher> Concatenate for hashbrown::HashMap<K, V, S> {
fn concatenate(mut self, b: Self) -> Self {
self.extend(b);

View File

@ -14,8 +14,18 @@ struct PluginEntry {
cache: AssetCache<Tar>,
}
/// The source combining filesystem and plugins (typically used via
/// CombinedCache)
/// The location of this asset
enum AssetSource {
FileSystem,
Plugin { index: usize },
}
struct SourceAndContents<'a>(AssetSource, FileContent<'a>);
/// This source combines assets loaded from the filesystem and from plugins.
/// It is typically used via the CombinedCache type.
///
/// A load will search through all sources and warn about unhandled duplicates.
pub struct CombinedSource {
fs: AssetCache<FileSystem>,
plugin_list: RwLock<Vec<PluginEntry>>,
@ -31,34 +41,40 @@ impl CombinedSource {
}
impl CombinedSource {
fn read_multiple(&self, id: &str, ext: &str) -> Vec<(Option<usize>, FileContent<'_>)> {
/// Look for an asset in all known sources
fn read_multiple(&self, id: &str, ext: &str) -> Vec<SourceAndContents<'_>> {
let mut result = Vec::new();
if let Ok(file_entry) = self.fs.raw_source().read(id, ext) {
result.push((None, file_entry));
result.push(SourceAndContents(AssetSource::FileSystem, file_entry));
}
for (n, p) in self.plugin_list.read().unwrap().iter().enumerate() {
if let Ok(entry) = p.cache.raw_source().read(id, ext) {
// the data is behind an RwLockReadGuard, so own it for returning
result.push((Some(n), match entry {
result.push(SourceAndContents(
AssetSource::Plugin { index: n },
match entry {
FileContent::Slice(s) => FileContent::Buffer(Vec::from(s)),
FileContent::Buffer(b) => FileContent::Buffer(b),
FileContent::Owned(s) => FileContent::Buffer(Vec::from(s.as_ref().as_ref())),
}));
FileContent::Owned(s) => {
FileContent::Buffer(Vec::from(s.as_ref().as_ref()))
},
},
));
}
}
result
}
// We don't want to keep the lock, so we clone
fn plugin_path(&self, index: Option<usize>) -> Option<PathBuf> {
if let Some(index) = index {
self.plugin_list
/// Return the path of a source
fn plugin_path(&self, index: &AssetSource) -> Option<PathBuf> {
match index {
AssetSource::FileSystem => Some(ASSETS_PATH.clone()),
AssetSource::Plugin { index } => self.plugin_list
.read()
.unwrap()
.get(index)
.map(|plugin| plugin.path.clone())
} else {
None
.get(*index)
// We don't want to keep the lock, so we clone
.map(|plugin| plugin.path.clone()),
}
}
}
@ -71,12 +87,11 @@ impl Source for CombinedSource {
Err(std::io::ErrorKind::NotFound.into())
} else {
if entries.len() > 1 {
let plugina = self.plugin_path(entries[0].0);
let pluginb = self.plugin_path(entries[1].0);
let patha = plugina.as_ref().unwrap_or(&ASSETS_PATH);
let pathb = pluginb.as_ref().unwrap_or(&ASSETS_PATH);
let patha = self.plugin_path(&entries[0].0);
let pathb = self.plugin_path(&entries[1].0);
tracing::error!("Duplicate asset {id} in {patha:?} and {pathb:?}");
}
// unconditionally return the first asset found
Ok(entries.swap_remove(0).1)
}
}
@ -119,7 +134,7 @@ impl CombinedCache {
/// Combine objects from filesystem and plugins
pub fn combine<T: Concatenate>(
&self,
load_from: impl Fn(AnyCache) -> Result<T, BoxedError>,
mut load_from: impl FnMut(AnyCache) -> Result<T, BoxedError>,
) -> Result<T, BoxedError> {
let mut result = load_from(self.0.raw_source().fs.as_any_cache());
// Report a severe error from the filesystem asset even if later overwritten by
@ -162,6 +177,8 @@ impl CombinedCache {
result
}
/// Add a tar archive (a plugin) to the system.
/// All files in that tar file become potential assets.
pub fn register_tar(&self, path: PathBuf) -> std::io::Result<()> {
let tar_source = Tar::from_path(&path)?;
let cache = AssetCache::with_source(tar_source);
@ -175,6 +192,7 @@ impl CombinedCache {
}
}
// Delegate all cache operations directly to the contained cache object
impl std::ops::Deref for CombinedCache {
type Target = AssetCache<CombinedSource>;

View File

@ -119,7 +119,7 @@ impl Backend {
fn read(&self, pos: u64, len: usize) -> std::io::Result<Vec<u8>> {
File::open(self.0.clone()).and_then(|file| {
let mut result = vec![0; len];
file.read_at(result.as_mut_slice(), pos)
file.read_exact_at(result.as_mut_slice(), pos)
.map(move |_num_bytes| result)
})
}

View File

@ -27,7 +27,7 @@ singleplayer = ["server"]
simd = ["vek/platform_intrinsics"]
tracy = ["common-frontend/tracy", "client/tracy"]
tracy-memory = ["tracy"] # enables heap profiling with tracy
plugins = ["client/plugins", "common-assets/plugins", "common-assets/hashbrown"]
plugins = ["client/plugins", "common-assets/plugins"]
egui-ui = ["voxygen-egui", "egui", "egui_wgpu_backend", "egui_winit_platform"]
shaderc-from-source = ["shaderc/build-from-source"]
discord = ["discord-sdk"]

View File

@ -178,7 +178,7 @@ macro_rules! make_vox_spec {
}
}
}
macro_rules! concatenate_tuple {
macro_rules! impl_concatenate_for_wrapper {
($name:ty) => {
impl Concatenate for $name {
fn concatenate(self, b: Self) -> Self { Self(self.0.concatenate(b.0)) }
@ -375,7 +375,7 @@ impl HumHeadSpec {
)
}
}
concatenate_tuple!(HumHeadSpec);
impl_concatenate_for_wrapper!(HumHeadSpec);
// Armor aspects should be in the same order, top to bottom.
// These seem overly split up, but wanted to keep the armor seperated
@ -398,43 +398,43 @@ impl<K: Hash + Eq, S> Concatenate for ArmorVoxSpecMap<K, S> {
}
#[derive(Deserialize)]
struct HumArmorShoulderSpec(ArmorVoxSpecMap<String, SidedArmorVoxSpec>);
concatenate_tuple!(HumArmorShoulderSpec);
impl_concatenate_for_wrapper!(HumArmorShoulderSpec);
#[derive(Deserialize)]
struct HumArmorChestSpec(ArmorVoxSpecMap<String, ArmorVoxSpec>);
concatenate_tuple!(HumArmorChestSpec);
impl_concatenate_for_wrapper!(HumArmorChestSpec);
#[derive(Deserialize)]
struct HumArmorHandSpec(ArmorVoxSpecMap<String, SidedArmorVoxSpec>);
concatenate_tuple!(HumArmorHandSpec);
impl_concatenate_for_wrapper!(HumArmorHandSpec);
#[derive(Deserialize)]
struct HumArmorBeltSpec(ArmorVoxSpecMap<String, ArmorVoxSpec>);
concatenate_tuple!(HumArmorBeltSpec);
impl_concatenate_for_wrapper!(HumArmorBeltSpec);
#[derive(Deserialize)]
struct HumArmorBackSpec(ArmorVoxSpecMap<String, ArmorVoxSpec>);
concatenate_tuple!(HumArmorBackSpec);
impl_concatenate_for_wrapper!(HumArmorBackSpec);
#[derive(Deserialize)]
struct HumArmorPantsSpec(ArmorVoxSpecMap<String, ArmorVoxSpec>);
concatenate_tuple!(HumArmorPantsSpec);
impl_concatenate_for_wrapper!(HumArmorPantsSpec);
#[derive(Deserialize)]
struct HumArmorFootSpec(ArmorVoxSpecMap<String, ArmorVoxSpec>);
concatenate_tuple!(HumArmorFootSpec);
impl_concatenate_for_wrapper!(HumArmorFootSpec);
#[derive(Deserialize)]
struct HumMainWeaponSpec(HashMap<ToolKey, ArmorVoxSpec>);
concatenate_tuple!(HumMainWeaponSpec);
impl_concatenate_for_wrapper!(HumMainWeaponSpec);
#[derive(Deserialize)]
struct HumModularComponentSpec(HashMap<String, ModularComponentSpec>);
concatenate_tuple!(HumModularComponentSpec);
impl_concatenate_for_wrapper!(HumModularComponentSpec);
#[derive(Deserialize)]
struct HumArmorLanternSpec(ArmorVoxSpecMap<String, ArmorVoxSpec>);
concatenate_tuple!(HumArmorLanternSpec);
impl_concatenate_for_wrapper!(HumArmorLanternSpec);
#[derive(Deserialize)]
struct HumArmorGliderSpec(ArmorVoxSpecMap<String, ArmorVoxSpec>);
concatenate_tuple!(HumArmorGliderSpec);
impl_concatenate_for_wrapper!(HumArmorGliderSpec);
#[derive(Deserialize)]
struct HumArmorHeadSpec(ArmorVoxSpecMap<(Species, BodyType, String), ArmorVoxSpec>);
concatenate_tuple!(HumArmorHeadSpec);
impl_concatenate_for_wrapper!(HumArmorHeadSpec);
#[derive(Deserialize)]
struct HumArmorTabardSpec(ArmorVoxSpecMap<String, ArmorVoxSpec>);
concatenate_tuple!(HumArmorTabardSpec);
impl_concatenate_for_wrapper!(HumArmorTabardSpec);
make_vox_spec!(
Body,
@ -1105,7 +1105,7 @@ fn mesh_hold() -> BoneMeshes {
//////
#[derive(Deserialize)]
struct QuadrupedSmallCentralSpec(HashMap<(QSSpecies, QSBodyType), SidedQSCentralVoxSpec>);
concatenate_tuple!(QuadrupedSmallCentralSpec);
impl_concatenate_for_wrapper!(QuadrupedSmallCentralSpec);
#[derive(Deserialize)]
struct SidedQSCentralVoxSpec {
@ -1123,7 +1123,7 @@ struct QuadrupedSmallCentralSubSpec {
#[derive(Deserialize)]
struct QuadrupedSmallLateralSpec(HashMap<(QSSpecies, QSBodyType), SidedQSLateralVoxSpec>);
concatenate_tuple!(QuadrupedSmallLateralSpec);
impl_concatenate_for_wrapper!(QuadrupedSmallLateralSpec);
#[derive(Deserialize)]
struct SidedQSLateralVoxSpec {
@ -1322,7 +1322,7 @@ impl QuadrupedSmallLateralSpec {
//////
#[derive(Deserialize)]
struct QuadrupedMediumCentralSpec(HashMap<(QMSpecies, QMBodyType), SidedQMCentralVoxSpec>);
concatenate_tuple!(QuadrupedMediumCentralSpec);
impl_concatenate_for_wrapper!(QuadrupedMediumCentralSpec);
#[derive(Deserialize)]
struct SidedQMCentralVoxSpec {
@ -1344,7 +1344,7 @@ struct QuadrupedMediumCentralSubSpec {
#[derive(Deserialize)]
struct QuadrupedMediumLateralSpec(HashMap<(QMSpecies, QMBodyType), SidedQMLateralVoxSpec>);
concatenate_tuple!(QuadrupedMediumLateralSpec);
impl_concatenate_for_wrapper!(QuadrupedMediumLateralSpec);
#[derive(Deserialize)]
struct SidedQMLateralVoxSpec {
leg_fl: QuadrupedMediumLateralSubSpec,
@ -1700,7 +1700,7 @@ impl QuadrupedMediumLateralSpec {
//////
#[derive(Deserialize)]
struct BirdMediumCentralSpec(HashMap<(BMSpecies, BMBodyType), SidedBMCentralVoxSpec>);
concatenate_tuple!(BirdMediumCentralSpec);
impl_concatenate_for_wrapper!(BirdMediumCentralSpec);
#[derive(Deserialize)]
struct SidedBMCentralVoxSpec {
@ -1718,7 +1718,7 @@ struct BirdMediumCentralSubSpec {
#[derive(Deserialize)]
struct BirdMediumLateralSpec(HashMap<(BMSpecies, BMBodyType), SidedBMLateralVoxSpec>);
concatenate_tuple!(BirdMediumLateralSpec);
impl_concatenate_for_wrapper!(BirdMediumLateralSpec);
#[derive(Deserialize)]
struct SidedBMLateralVoxSpec {
@ -1956,7 +1956,7 @@ impl BirdMediumLateralSpec {
//////
#[derive(Deserialize)]
struct TheropodCentralSpec(HashMap<(TSpecies, TBodyType), SidedTCentralVoxSpec>);
concatenate_tuple!(TheropodCentralSpec);
impl_concatenate_for_wrapper!(TheropodCentralSpec);
#[derive(Deserialize)]
struct SidedTCentralVoxSpec {
@ -1977,7 +1977,7 @@ struct TheropodCentralSubSpec {
}
#[derive(Deserialize)]
struct TheropodLateralSpec(HashMap<(TSpecies, TBodyType), SidedTLateralVoxSpec>);
concatenate_tuple!(TheropodLateralSpec);
impl_concatenate_for_wrapper!(TheropodLateralSpec);
#[derive(Deserialize)]
struct SidedTLateralVoxSpec {
@ -2288,7 +2288,7 @@ impl TheropodLateralSpec {
//////
#[derive(Deserialize)]
struct ArthropodCentralSpec(HashMap<(ASpecies, ABodyType), SidedACentralVoxSpec>);
concatenate_tuple!(ArthropodCentralSpec);
impl_concatenate_for_wrapper!(ArthropodCentralSpec);
#[derive(Deserialize)]
struct SidedACentralVoxSpec {
@ -2304,7 +2304,7 @@ struct ArthropodCentralSubSpec {
}
#[derive(Deserialize)]
struct ArthropodLateralSpec(HashMap<(ASpecies, ABodyType), SidedALateralVoxSpec>);
concatenate_tuple!(ArthropodLateralSpec);
impl_concatenate_for_wrapper!(ArthropodLateralSpec);
#[derive(Deserialize)]
struct SidedALateralVoxSpec {
@ -2690,7 +2690,7 @@ impl ArthropodLateralSpec {
//////
#[derive(Deserialize)]
struct FishMediumCentralSpec(HashMap<(FMSpecies, FMBodyType), SidedFMCentralVoxSpec>);
concatenate_tuple!(FishMediumCentralSpec);
impl_concatenate_for_wrapper!(FishMediumCentralSpec);
#[derive(Deserialize)]
struct SidedFMCentralVoxSpec {
@ -2709,7 +2709,7 @@ struct FishMediumCentralSubSpec {
}
#[derive(Deserialize)]
struct FishMediumLateralSpec(HashMap<(FMSpecies, FMBodyType), SidedFMLateralVoxSpec>);
concatenate_tuple!(FishMediumLateralSpec);
impl_concatenate_for_wrapper!(FishMediumLateralSpec);
#[derive(Deserialize)]
struct SidedFMLateralVoxSpec {
fin_l: FishMediumLateralSubSpec,
@ -2898,7 +2898,7 @@ impl FishMediumLateralSpec {
//////
#[derive(Deserialize)]
struct FishSmallCentralSpec(HashMap<(FSSpecies, FSBodyType), SidedFSCentralVoxSpec>);
concatenate_tuple!(FishSmallCentralSpec);
impl_concatenate_for_wrapper!(FishSmallCentralSpec);
#[derive(Deserialize)]
struct SidedFSCentralVoxSpec {
@ -2914,7 +2914,7 @@ struct FishSmallCentralSubSpec {
}
#[derive(Deserialize)]
struct FishSmallLateralSpec(HashMap<(FSSpecies, FSBodyType), SidedFSLateralVoxSpec>);
concatenate_tuple!(FishSmallLateralSpec);
impl_concatenate_for_wrapper!(FishSmallLateralSpec);
#[derive(Deserialize)]
struct SidedFSLateralVoxSpec {
fin_l: FishSmallLateralSubSpec,
@ -3045,25 +3045,25 @@ impl FishSmallLateralSpec {
#[derive(Deserialize)]
struct BipedSmallWeaponSpec(HashMap<ToolKey, ArmorVoxSpec>);
concatenate_tuple!(BipedSmallWeaponSpec);
impl_concatenate_for_wrapper!(BipedSmallWeaponSpec);
#[derive(Deserialize)]
struct BipedSmallArmorHeadSpec(ArmorVoxSpecMap<String, ArmorVoxSpec>);
concatenate_tuple!(BipedSmallArmorHeadSpec);
impl_concatenate_for_wrapper!(BipedSmallArmorHeadSpec);
#[derive(Deserialize)]
struct BipedSmallArmorHandSpec(ArmorVoxSpecMap<String, SidedArmorVoxSpec>);
concatenate_tuple!(BipedSmallArmorHandSpec);
impl_concatenate_for_wrapper!(BipedSmallArmorHandSpec);
#[derive(Deserialize)]
struct BipedSmallArmorFootSpec(ArmorVoxSpecMap<String, SidedArmorVoxSpec>);
concatenate_tuple!(BipedSmallArmorFootSpec);
impl_concatenate_for_wrapper!(BipedSmallArmorFootSpec);
#[derive(Deserialize)]
struct BipedSmallArmorChestSpec(ArmorVoxSpecMap<String, ArmorVoxSpec>);
concatenate_tuple!(BipedSmallArmorChestSpec);
impl_concatenate_for_wrapper!(BipedSmallArmorChestSpec);
#[derive(Deserialize)]
struct BipedSmallArmorPantsSpec(ArmorVoxSpecMap<String, ArmorVoxSpec>);
concatenate_tuple!(BipedSmallArmorPantsSpec);
impl_concatenate_for_wrapper!(BipedSmallArmorPantsSpec);
#[derive(Deserialize)]
struct BipedSmallArmorTailSpec(ArmorVoxSpecMap<String, ArmorVoxSpec>);
concatenate_tuple!(BipedSmallArmorTailSpec);
impl_concatenate_for_wrapper!(BipedSmallArmorTailSpec);
make_vox_spec!(
biped_small::Body,
struct BipedSmallSpec {
@ -3327,7 +3327,7 @@ impl BipedSmallWeaponSpec {
//////
#[derive(Deserialize)]
struct DragonCentralSpec(HashMap<(DSpecies, DBodyType), SidedDCentralVoxSpec>);
concatenate_tuple!(DragonCentralSpec);
impl_concatenate_for_wrapper!(DragonCentralSpec);
#[derive(Deserialize)]
struct SidedDCentralVoxSpec {
@ -3349,7 +3349,7 @@ struct DragonCentralSubSpec {
#[derive(Deserialize)]
struct DragonLateralSpec(HashMap<(DSpecies, DBodyType), SidedDLateralVoxSpec>);
concatenate_tuple!(DragonLateralSpec);
impl_concatenate_for_wrapper!(DragonLateralSpec);
#[derive(Deserialize)]
struct SidedDLateralVoxSpec {
@ -3700,7 +3700,7 @@ impl DragonLateralSpec {
//////
#[derive(Deserialize)]
struct BirdLargeCentralSpec(HashMap<(BLASpecies, BLABodyType), SidedBLACentralVoxSpec>);
concatenate_tuple!(BirdLargeCentralSpec);
impl_concatenate_for_wrapper!(BirdLargeCentralSpec);
#[derive(Deserialize)]
struct SidedBLACentralVoxSpec {
@ -3721,7 +3721,7 @@ struct BirdLargeCentralSubSpec {
#[derive(Deserialize)]
struct BirdLargeLateralSpec(HashMap<(BLASpecies, BLABodyType), SidedBLALateralVoxSpec>);
concatenate_tuple!(BirdLargeLateralSpec);
impl_concatenate_for_wrapper!(BirdLargeLateralSpec);
#[derive(Deserialize)]
struct SidedBLALateralVoxSpec {
@ -4105,7 +4105,7 @@ impl BirdLargeLateralSpec {
//////
#[derive(Deserialize)]
struct BipedLargeCentralSpec(HashMap<(BLSpecies, BLBodyType), SidedBLCentralVoxSpec>);
concatenate_tuple!(BipedLargeCentralSpec);
impl_concatenate_for_wrapper!(BipedLargeCentralSpec);
#[derive(Deserialize)]
struct SidedBLCentralVoxSpec {
@ -4125,7 +4125,7 @@ struct BipedLargeCentralSubSpec {
#[derive(Deserialize)]
struct BipedLargeLateralSpec(HashMap<(BLSpecies, BLBodyType), SidedBLLateralVoxSpec>);
concatenate_tuple!(BipedLargeLateralSpec);
impl_concatenate_for_wrapper!(BipedLargeLateralSpec);
#[derive(Deserialize)]
struct SidedBLLateralVoxSpec {
@ -4147,10 +4147,10 @@ struct BipedLargeLateralSubSpec {
}
#[derive(Deserialize)]
struct BipedLargeMainSpec(HashMap<ToolKey, ArmorVoxSpec>);
concatenate_tuple!(BipedLargeMainSpec);
impl_concatenate_for_wrapper!(BipedLargeMainSpec);
#[derive(Deserialize)]
struct BipedLargeSecondSpec(HashMap<ToolKey, ArmorVoxSpec>);
concatenate_tuple!(BipedLargeSecondSpec);
impl_concatenate_for_wrapper!(BipedLargeSecondSpec);
make_vox_spec!(
biped_large::Body,
struct BipedLargeSpec {
@ -4528,7 +4528,7 @@ impl BipedLargeSecondSpec {
//////
#[derive(Deserialize)]
struct GolemCentralSpec(HashMap<(GSpecies, GBodyType), SidedGCentralVoxSpec>);
concatenate_tuple!(GolemCentralSpec);
impl_concatenate_for_wrapper!(GolemCentralSpec);
#[derive(Deserialize)]
struct SidedGCentralVoxSpec {
@ -4547,7 +4547,7 @@ struct GolemCentralSubSpec {
#[derive(Deserialize)]
struct GolemLateralSpec(HashMap<(GSpecies, GBodyType), SidedGLateralVoxSpec>);
concatenate_tuple!(GolemLateralSpec);
impl_concatenate_for_wrapper!(GolemLateralSpec);
#[derive(Deserialize)]
struct SidedGLateralVoxSpec {
@ -4840,7 +4840,7 @@ impl GolemLateralSpec {
//////
#[derive(Deserialize)]
struct QuadrupedLowCentralSpec(HashMap<(QLSpecies, QLBodyType), SidedQLCentralVoxSpec>);
concatenate_tuple!(QuadrupedLowCentralSpec);
impl_concatenate_for_wrapper!(QuadrupedLowCentralSpec);
#[derive(Deserialize)]
struct SidedQLCentralVoxSpec {
@ -4861,7 +4861,7 @@ struct QuadrupedLowCentralSubSpec {
#[derive(Deserialize)]
struct QuadrupedLowLateralSpec(HashMap<(QLSpecies, QLBodyType), SidedQLLateralVoxSpec>);
concatenate_tuple!(QuadrupedLowLateralSpec);
impl_concatenate_for_wrapper!(QuadrupedLowLateralSpec);
#[derive(Deserialize)]
struct SidedQLLateralVoxSpec {
front_left: QuadrupedLowLateralSubSpec,
@ -5191,7 +5191,7 @@ impl ObjectCentralSpec {
(central, Vec3::from(spec.bone1.offset))
}
}
concatenate_tuple!(ObjectCentralSpec);
impl_concatenate_for_wrapper!(ObjectCentralSpec);
struct ModelWithOptionalIndex(String, u32);
@ -5229,7 +5229,7 @@ impl<'de> Deserialize<'de> for ModelWithOptionalIndex {
#[derive(Deserialize)]
struct ItemDropCentralSpec(HashMap<ItemKey, ModelWithOptionalIndex>);
concatenate_tuple!(ItemDropCentralSpec);
impl_concatenate_for_wrapper!(ItemDropCentralSpec);
make_vox_spec!(
item_drop::Body,