Merge pull request #11393 from ilguerro/fix/10548-villager-memories

Save villager brain memories like vanilla
This commit is contained in:
rdbt authored and GitHub committed 2026-11-06 22:09:51 +00:00
commit 6590656a9a
2 files changed
+228 -20

No files matched your search

+219 -14
View File
@@ -290,6 +290,8 @@ pub struct Ext {
pub cure: Option<super::zombie_villager::Cure>,
/// The villager's brain, built at its first tick.
pub brain: Option<Box<super::villager_brain::Brain>>,
/// Saved memories awaiting the first brain initialization.
pub saved_memories: Option<Box<super::villager_brain::Memories>>,
}
pub fn ext(m: &Mob) -> Option<&Ext> {
m.extra.villager.as_deref()
@@ -838,29 +840,133 @@ pub fn write_nbt(m: &Mob, map: &mut horizon_world::nbt::Compound) {
}
/// The memories of `Brain.memories` the villager keeps besides positions.
pub fn write_memories(m: &Mob, memories: &mut Vec<(&'static str, horizon_world::nbt::Tag)>) {
use super::villager_brain::{Memories, NEVER, Slot};
use horizon_world::nbt::Tag;
fn push<T>(
out: &mut Vec<(&'static str, Tag)>,
key: &'static str,
slot: &Slot<T>,
encode: impl FnOnce(&T) -> Tag,
) {
if let Some(value) = slot.get() {
let mut fields = vec![("value", encode(value))];
if slot.ttl() != NEVER {
fields.push(("ttl", Tag::Long(slot.ttl())));
}
out.push((key, Tag::compound(fields)));
}
}
let Some(e) = ext(m) else { return };
if let Some(t) = e.last_slept {
let fallback = Memories::default();
let mm = e
.brain
.as_ref()
.map(|b| &b.mem)
.or(e.saved_memories.as_deref())
.unwrap_or(&fallback);
// The three established position fields are written by entity_nbt; replace
// them only when the live brain has a value (and therefore a possible TTL).
for (key, slot) in [
("minecraft:home", &mm.home),
("minecraft:job_site", &mm.job_site),
("minecraft:meeting_point", &mm.meeting_point),
("minecraft:potential_job_site", &mm.potential_job_site),
] {
if slot.present() {
memories.retain(|(k, _)| *k != key);
push(memories, key, slot, |p| {
Tag::compound([
("dimension", Tag::String("minecraft:overworld".into())),
("pos", Tag::IntArray(p.to_vec())),
])
});
}
}
push(
memories,
"minecraft:golem_detected_recently",
&mm.golem_detected_recently,
|v| Tag::Byte(i8::from(*v)),
);
if mm.last_slept.present() {
push(memories, "minecraft:last_slept", &mm.last_slept, |v| {
Tag::Long(*v)
});
} else if e.brain.is_none()
&& e.saved_memories.is_none()
&& let Some(t) = e.last_slept
{
memories.push((
"minecraft:last_slept",
Tag::compound([("value", Tag::Long(t))]),
));
}
push(memories, "minecraft:last_woken", &mm.last_woken, |v| {
Tag::Long(*v)
});
}
pub fn read_nbt(m: &mut Mob, tag: &horizon_world::nbt::Tag) {
use horizon_world::nbt::Tag;
let int = |t: Option<&Tag>| match t {
Some(Tag::Int(v)) => Some(i64::from(*v)),
Some(Tag::Byte(v)) => Some(i64::from(*v)),
Some(Tag::Short(v)) => Some(i64::from(*v)),
Some(Tag::Long(v)) => Some(*v),
_ => None,
};
let last_slept = int(tag
.get("Brain")
.and_then(|b| b.get("memories"))
.and_then(|b| b.get("minecraft:last_slept"))
.and_then(|j| j.get("value")));
fn int(t: Option<&Tag>) -> Option<i64> {
match t {
Some(Tag::Int(v)) => Some(i64::from(*v)),
Some(Tag::Byte(v)) => Some(i64::from(*v)),
Some(Tag::Short(v)) => Some(i64::from(*v)),
Some(Tag::Long(v)) => Some(*v),
_ => None,
}
}
let memories = tag.get("Brain").and_then(|b| b.get("memories"));
let mut saved = super::villager_brain::Memories::default();
fn read<T>(
memories: Option<&Tag>,
key: &str,
slot: &mut super::villager_brain::Slot<T>,
decode: impl FnOnce(&Tag) -> Option<T>,
) {
if let Some(entry) = memories.and_then(|m| m.get(key))
&& let Some(value) = entry.get("value").and_then(decode)
{
// `ExpirableValue.codec`: a lenient optional `Codec.LONG`.
match int(entry.get("ttl")) {
Some(ttl) => slot.set_ttl(value, ttl),
None => slot.set(value),
}
}
}
for (key, slot) in [
("minecraft:home", &mut saved.home),
("minecraft:job_site", &mut saved.job_site),
("minecraft:meeting_point", &mut saved.meeting_point),
(
"minecraft:potential_job_site",
&mut saved.potential_job_site,
),
] {
read(memories, key, slot, |v| match v.get("pos") {
Some(Tag::IntArray(p)) if p.len() == 3 => Some([p[0], p[1], p[2]]),
_ => None,
});
}
read(
memories,
"minecraft:golem_detected_recently",
&mut saved.golem_detected_recently,
|v| int(Some(v)).map(|v| v != 0),
);
read(
memories,
"minecraft:last_slept",
&mut saved.last_slept,
|v| int(Some(v)),
);
read(
memories,
"minecraft:last_woken",
&mut saved.last_woken,
|v| int(Some(v)),
);
let e = ext_mut(m);
e.food_level = int(tag.get("FoodLevel")).unwrap_or(0) as i32;
e.gossips = tag
@@ -868,7 +974,8 @@ pub fn read_nbt(m: &mut Mob, tag: &horizon_world::nbt::Tag) {
.map(crate::gossip::Gossips::from_nbt)
.unwrap_or_default();
e.last_gossip_decay = int(tag.get("LastGossipDecay")).unwrap_or(0);
e.last_slept = last_slept;
e.last_slept = saved.last_slept.value();
e.saved_memories = Some(Box::new(saved));
// LivingEntity.readAdditionalSaveData: asleep in the bed again (the
// pose; `ai_step` turns it the way the bed faces, or wakes it when the
// bed is gone, as `baseTick`'s first tick does).
@@ -893,6 +1000,104 @@ pub fn read_nbt(m: &mut Mob, tag: &horizon_world::nbt::Tag) {
mod tests {
use super::*;
fn memory_fixture() -> Mob {
let mut m = villager();
super::super::villager_brain::init(&mut m, 0, 0, 1);
let mm = &mut ext_mut(&mut m).brain.as_mut().unwrap().mem;
mm.golem_detected_recently.set_ttl(true, 42);
mm.last_slept.set_ttl(101, 17);
mm.last_woken.set(202);
mm.last_worked_at_poi.set_ttl(303, 7);
mm.potential_job_site.set_ttl([4, 5, 6], 9);
m
}
#[test]
fn villager_memory_nbt_shape() {
use horizon_world::nbt::Tag;
let m = memory_fixture();
let mut entries = Vec::new();
write_memories(&m, &mut entries);
let actual = Tag::compound(entries);
let expected = Tag::compound([
(
"minecraft:golem_detected_recently",
Tag::compound([("value", Tag::Byte(1)), ("ttl", Tag::Long(42))]),
),
(
"minecraft:last_slept",
Tag::compound([("value", Tag::Long(101)), ("ttl", Tag::Long(17))]),
),
(
"minecraft:last_woken",
Tag::compound([("value", Tag::Long(202))]),
),
(
"minecraft:potential_job_site",
Tag::compound([
(
"value",
Tag::compound([
("dimension", Tag::String("minecraft:overworld".into())),
("pos", Tag::IntArray(vec![4, 5, 6])),
]),
),
("ttl", Tag::Long(9)),
]),
),
]);
assert_eq!(actual, expected);
}
#[test]
fn villager_memory_round_trip() {
use horizon_world::nbt::Tag;
let m = memory_fixture();
let mut entries = Vec::new();
write_memories(&m, &mut entries);
entries.push((
"minecraft:last_worked_at_poi",
Tag::compound([("value", Tag::Long(303)), ("ttl", Tag::Long(7))]),
));
let tag = Tag::compound([(
"Brain",
Tag::compound([("memories", Tag::compound(entries))]),
)]);
let mut loaded = villager();
read_nbt(&mut loaded, &tag);
super::super::villager_brain::init(&mut loaded, 500, 0, 1);
let mm = &mut ext_mut(&mut loaded).brain.as_mut().unwrap().mem;
assert_eq!(
(
mm.golem_detected_recently.value(),
mm.golem_detected_recently.ttl()
),
(Some(true), 42)
);
assert_eq!(
(mm.last_slept.value(), mm.last_slept.ttl()),
(Some(101), 17)
);
assert_eq!(
(mm.last_woken.value(), mm.last_woken.ttl()),
(Some(202), i64::MAX)
);
assert_eq!(
(mm.last_worked_at_poi.value(), mm.last_worked_at_poi.ttl()),
(None, i64::MAX)
);
assert_eq!(
(mm.potential_job_site.value(), mm.potential_job_site.ttl()),
(Some([4, 5, 6]), 9)
);
for _ in 0..42 {
mm.golem_detected_recently.tick();
}
assert!(mm.golem_detected_recently.present());
mm.golem_detected_recently.tick();
assert!(!mm.golem_detected_recently.present());
}
fn villager() -> Mob {
use crate::mobs::{Anim, Brain, Flags, MobType, Part, Species};
use std::sync::Arc;
@@ -435,12 +435,15 @@ fn make_brain(m: &mut Mob, packed: Memories, shuffle_seed: i64, w_times: (i64, i
/// Builds the villager's brain as its constructor does, at this game time:
/// the memories it remembers (job site, bed, bell) come from the mob.
pub fn init(m: &mut Mob, game_time: i64, day_time: i64, shuffle_seed: i64) {
let mut packed = Memories::default();
packed.job_site.set_opt(m.extra.home);
packed.home.set_opt(m.extra.bed);
packed.meeting_point.set_opt(m.extra.meeting);
if let Some(t) = super::villager::ext(m).and_then(|e| e.last_slept) {
packed.last_slept.set(t);
let saved = super::villager::ext_mut(m).saved_memories.take();
let mut packed = saved.as_deref().cloned().unwrap_or_default();
if saved.is_none() {
packed.job_site.set_opt(m.extra.home);
packed.home.set_opt(m.extra.bed);
packed.meeting_point.set_opt(m.extra.meeting);
if let Some(t) = super::villager::ext(m).and_then(|e| e.last_slept) {
packed.last_slept.set(t);
}
}
let b = make_brain(m, packed, shuffle_seed, (game_time, day_time));
super::villager::ext_mut(m).brain = Some(Box::new(b));