tag registry (#350)

- add Tags extractor
- add tags.json to extracted
- send `SynchronizeTagsS2c` packet on join
- fix encode

## Description

Adds a `TagsRegistry` resource that contains all the information needed
to build and send `SynchronizeTagsS2c` on join.

closes #349
This commit is contained in:
Carson McManus 2023-06-02 02:21:25 -04:00 committed by GitHub
parent c5557e744d
commit 975014e76b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 7445 additions and 30 deletions

View file

@ -67,7 +67,7 @@ use valence_instance::packet::{
ChunkLoadDistanceS2c, ChunkRenderDistanceCenterS2c, UnloadChunkS2c,
};
use valence_instance::{ClearInstanceChangesSet, Instance, WriteUpdatePacketsToInstancesSet};
use valence_registry::{RegistryCodec, RegistryCodecSet};
use valence_registry::{RegistryCodec, RegistryCodecSet, TagsRegistry};
pub mod action;
pub mod chat;
@ -678,6 +678,7 @@ struct ClientJoinQuery {
fn initial_join(
codec: Res<RegistryCodec>,
tags: Res<TagsRegistry>,
mut clients: Query<ClientJoinQuery, Added<Client>>,
instances: Query<&Instance>,
mut commands: Commands,
@ -724,6 +725,8 @@ fn initial_join(
last_death_location,
});
q.client.enc.append_bytes(tags.sync_tags_packet());
/*
// TODO: enable all the features?
q.client.write_packet(&FeatureFlags {

View file

@ -142,7 +142,7 @@ impl Default for CoreSettings {
}
/// Contains global server state accessible as a [`Resource`].
#[derive(Resource)]
#[derive(Resource, Default)]
pub struct Server {
/// Incremented on every tick.
current_tick: i64,

View file

@ -1372,26 +1372,3 @@ pub mod map {
}
}
}
// TODO: Move this to valence_registry?
pub mod synchronize_tags {
use super::*;
#[derive(Clone, Debug, Encode, Decode, Packet)]
#[packet(id = packet_id::SYNCHRONIZE_TAGS_S2C)]
pub struct SynchronizeTagsS2c<'a> {
pub tags: Vec<TagGroup<'a>>,
}
#[derive(Clone, PartialEq, Eq, Debug, Encode, Decode)]
pub struct TagGroup<'a> {
pub kind: Ident<Cow<'a, str>>,
pub tags: Vec<Tag<'a>>,
}
#[derive(Clone, PartialEq, Eq, Debug, Encode, Decode)]
pub struct Tag<'a> {
pub name: Ident<Cow<'a, str>>,
pub entries: Vec<VarInt>,
}
}

View file

@ -2,12 +2,13 @@ use std::io::{Read, Write};
use anyhow::bail;
use byteorder::ReadBytesExt;
use serde::Deserialize;
use thiserror::Error;
use crate::protocol::{Decode, Encode};
/// An `i32` encoded with variable length.
#[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
#[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Deserialize)]
#[repr(transparent)]
pub struct VarInt(pub i32);

View file

@ -8,4 +8,6 @@ tracing.workspace = true
valence_core.workspace = true
valence_nbt.workspace = true
bevy_ecs.workspace = true
bevy_app.workspace = true
bevy_app.workspace = true
serde.workspace = true
serde_json.workspace = true

View file

@ -25,6 +25,10 @@ use tracing::error;
use valence_core::ident::Ident;
use valence_nbt::{compound, Compound, List, Value};
mod tags;
pub use tags::*;
pub struct RegistryPlugin;
/// The [`SystemSet`] where the [`RegistryCodec`] cache is rebuilt. Systems that
@ -35,8 +39,11 @@ pub struct RegistryCodecSet;
impl Plugin for RegistryPlugin {
fn build(&self, app: &mut bevy_app::App) {
app.init_resource::<RegistryCodec>()
.init_resource::<TagsRegistry>()
.configure_set(RegistryCodecSet.in_base_set(CoreSet::PostUpdate))
.add_system(cache_registry_codec.in_set(RegistryCodecSet));
.add_startup_system(init_tags_registry.in_set(RegistryCodecSet))
.add_system(cache_registry_codec.in_set(RegistryCodecSet))
.add_system(cache_tags_packet.in_set(RegistryCodecSet));
}
}

View file

@ -0,0 +1,84 @@
use std::borrow::Cow;
use bevy_ecs::prelude::*;
use serde::Deserialize;
use valence_core::ident::Ident;
use valence_core::protocol::encode::{PacketWriter, WritePacket};
use valence_core::protocol::var_int::VarInt;
use valence_core::protocol::{packet_id, Decode, Encode, Packet};
use valence_core::Server;
#[derive(Clone, Debug, Encode, Decode, Packet)]
#[packet(id = packet_id::SYNCHRONIZE_TAGS_S2C)]
pub struct SynchronizeTagsS2c<'a> {
pub registries: Cow<'a, [Registry]>,
}
#[derive(Debug, Resource, Default)]
pub struct TagsRegistry {
pub registries: Vec<Registry>,
cached_packet: Vec<u8>,
}
#[derive(Debug, Deserialize, Clone, PartialEq, Eq, Encode, Decode)]
pub struct Registry {
pub registry: Ident<String>,
pub tags: Vec<TagEntry>,
}
#[derive(Debug, Deserialize, Clone, PartialEq, Eq, Encode, Decode)]
pub struct TagEntry {
pub name: Ident<String>,
pub entries: Vec<VarInt>,
}
impl<'a> TagsRegistry {
pub(crate) fn build_synchronize_tags(&'a self) -> SynchronizeTagsS2c<'a> {
SynchronizeTagsS2c {
registries: Cow::Borrowed(&self.registries),
}
}
pub fn sync_tags_packet(&self) -> &Vec<u8> {
&self.cached_packet
}
}
pub(crate) fn init_tags_registry(mut tags: ResMut<TagsRegistry>) {
let registries =
serde_json::from_str::<Vec<Registry>>(include_str!("../../../extracted/tags.json"))
.expect("tags.json is invalid");
tags.registries = registries;
}
pub(crate) fn cache_tags_packet(server: Res<Server>, tags: ResMut<TagsRegistry>) {
if tags.is_changed() {
let tags = tags.into_inner();
let packet = tags.build_synchronize_tags();
let mut bytes = vec![];
let mut scratch = vec![];
let mut writer =
PacketWriter::new(&mut bytes, server.compression_threshold(), &mut scratch);
writer.write_packet(&packet);
tags.cached_packet = bytes;
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::RegistryPlugin;
#[test]
fn smoke_test() {
let mut app = bevy_app::App::new();
app.add_plugin(RegistryPlugin);
app.insert_resource(Server::default());
app.update();
let tags_registry = app.world.get_resource::<TagsRegistry>().unwrap();
let packet = tags_registry.build_synchronize_tags();
assert!(!packet.registries.is_empty());
assert!(!tags_registry.cached_packet.is_empty());
}
}

7235
extracted/tags.json Normal file

File diff suppressed because it is too large Load diff

View file

@ -74,9 +74,9 @@ public class Main implements ModInitializer {
}
ServerLifecycleEvents.SERVER_STARTING.register(server -> {
LOGGER.info("Server starting, Extracting registry codec...");
LOGGER.info("Server starting, Running startup extractors...");
// TODO: make `Codec` implement `Extractor`
var codecExtractor = new Codec(server);
try {
var out = outputDirectory.resolve(codecExtractor.fileName());
var compound = codecExtractor.extract();
@ -92,6 +92,22 @@ public class Main implements ModInitializer {
LOGGER.error("Extractor for \"" + codecExtractor.fileName() + "\" failed.", e);
}
var startupExtractors = new Extractor[]{
new Tags(server),
};
for (var ext : startupExtractors) {
try {
var out = outputDirectory.resolve(ext.fileName());
var fileWriter = new FileWriter(out.toFile(), StandardCharsets.UTF_8);
gson.toJson(ext.extract(), fileWriter);
fileWriter.close();
LOGGER.info("Wrote " + out.toAbsolutePath());
} catch (Exception e) {
LOGGER.error("Extractor for \"" + ext.fileName() + "\" failed.", e);
}
}
LOGGER.info("Done.");
server.shutdown();
});

View file

@ -0,0 +1,90 @@
package rs.valence.extractor.extractors;
import com.mojang.datafixers.util.Pair;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import net.minecraft.entity.SpawnGroup;
import net.minecraft.util.Identifier;
import net.minecraft.util.collection.Weighted;
import net.minecraft.registry.Registries;
import net.minecraft.registry.Registry;
import net.minecraft.registry.RegistryKey;
import net.minecraft.registry.RegistryKeys;
import net.minecraft.registry.RegistryWrapper;
import net.minecraft.registry.BuiltinRegistries;
import net.minecraft.registry.DynamicRegistryManager;
import net.minecraft.server.MinecraftServer;
import rs.valence.extractor.Main;
import net.minecraft.registry.tag.TagKey;
import net.minecraft.registry.CombinedDynamicRegistries;
import net.minecraft.registry.ServerDynamicRegistryType;
import net.minecraft.registry.entry.RegistryEntry;
import net.minecraft.registry.entry.RegistryEntryList;
import net.minecraft.registry.SerializableRegistries;
import java.util.Map;
import java.util.HashMap;
import java.util.stream.Collectors;
public class Tags implements Main.Extractor {
private CombinedDynamicRegistries<ServerDynamicRegistryType> dynamicRegistryManager;
public Tags(MinecraftServer server) {
this.dynamicRegistryManager = server.getCombinedDynamicRegistries();
}
@Override
public String fileName() {
return "tags.json";
}
@Override
public JsonElement extract() {
var tagsJson = new JsonArray();
Map<RegistryKey<? extends Registry<?>>, Map<Identifier, JsonArray>> registryTags =
SerializableRegistries.streamRegistryManagerEntries(this.dynamicRegistryManager)
.map(registry -> Pair.of(registry.key(), serializeTags(registry.value())))
.filter(pair -> !(pair.getSecond()).isEmpty())
.collect(Collectors.toMap(Pair::getFirst, Pair::getSecond));
for (var registry : registryTags.entrySet()) {
var registryIdent = registry.getKey().getValue().toString();
var tagGroupJson = new JsonObject();
var tagGroupTagsJson = new JsonArray();
for (var tag : registry.getValue().entrySet()) {
var tagJson = new JsonObject();
var ident = tag.getKey().toString();
var raw_ids = tag.getValue();
tagJson.addProperty("name", ident);
tagJson.add("entries", raw_ids);
tagGroupTagsJson.add(tagJson);
}
tagGroupJson.addProperty("registry", registryIdent.toString());
tagGroupJson.add("tags", tagGroupTagsJson);
tagsJson.add(tagGroupJson);
}
return tagsJson;
}
private static <T> Map<Identifier, JsonArray> serializeTags(Registry<T> registry) {
HashMap<Identifier, JsonArray> map = new HashMap<Identifier, JsonArray>();
registry.streamTagsAndEntries().forEach(pair -> {
RegistryEntryList<T> registryEntryList = (RegistryEntryList<T>)pair.getSecond();
JsonArray intList = new JsonArray(registryEntryList.size());
for (RegistryEntry<T> registryEntry : registryEntryList) {
if (registryEntry.getType() != RegistryEntry.Type.REFERENCE) {
throw new IllegalStateException("Can't serialize unregistered value " + registryEntry);
}
intList.add(registry.getRawId(registryEntry.value()));
}
map.put(((TagKey)pair.getFirst()).id(), intList);
});
return map;
}
}