diff --git a/common/src/main/java/io/github/gaming32/bingo/data/goal/BingoGoal.java b/common/src/main/java/io/github/gaming32/bingo/data/goal/BingoGoal.java index a7b6d47f..ce981ae1 100644 --- a/common/src/main/java/io/github/gaming32/bingo/data/goal/BingoGoal.java +++ b/common/src/main/java/io/github/gaming32/bingo/data/goal/BingoGoal.java @@ -14,6 +14,8 @@ import io.github.gaming32.bingo.data.progresstrackers.EmptyProgressTracker; import io.github.gaming32.bingo.data.progresstrackers.ProgressTracker; import io.github.gaming32.bingo.data.subs.BingoSub; +import io.github.gaming32.bingo.data.subs.ParsedOrSub; +import io.github.gaming32.bingo.data.subs.SubstitutionContext; import io.github.gaming32.bingo.util.BingoCodecs; import io.github.gaming32.bingo.util.BingoUtil; import net.minecraft.advancements.AdvancementRequirements; @@ -32,9 +34,9 @@ import net.minecraft.resources.ResourceKey; import net.minecraft.resources.ResourceLocation; import net.minecraft.util.ExtraCodecs; -import net.minecraft.util.RandomSource; import java.util.Collection; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.Map; import java.util.Optional; @@ -45,15 +47,15 @@ public class BingoGoal { public static final Codec CODEC = RecordCodecBuilder.create( instance -> instance.group( Codec.unboundedMap(Codec.STRING, BingoSub.CODEC).optionalFieldOf("bingo_subs", Map.of()).forGetter(BingoGoal::getSubs), - Codec.unboundedMap(Codec.STRING, Codec.PASSTHROUGH).fieldOf("criteria").forGetter(BingoGoal::getCriteria), + Codec.unboundedMap(Codec.STRING, ParsedOrSub.codec(Criterion.CODEC)).fieldOf("criteria").forGetter(BingoGoal::getCriteria), AdvancementRequirements.CODEC.optionalFieldOf("requirements").forGetter(g -> Optional.of(g.requirements)), ProgressTracker.CODEC.optionalFieldOf("progress", EmptyProgressTracker.INSTANCE).forGetter(BingoGoal::getProgress), - BingoCodecs.optionalDynamicField("required_count", BingoCodecs.EMPTY_DYNAMIC.createInt(1)).forGetter(BingoGoal::getRequiredCount), + ParsedOrSub.optionalCodec(ExtraCodecs.POSITIVE_INT, "required_count", 1).forGetter(BingoGoal::getRequiredCount), RegistryCodecs.homogeneousList(BingoRegistries.TAG).optionalFieldOf("tags", HolderSet.empty()).forGetter(BingoGoal::getTags), - Codec.PASSTHROUGH.fieldOf("name").forGetter(BingoGoal::getName), - BingoCodecs.optionalDynamicField("tooltip").forGetter(BingoGoal::getTooltip), + ParsedOrSub.codec(ComponentSerialization.CODEC).fieldOf("name").forGetter(BingoGoal::getName), + ParsedOrSub.codec(ComponentSerialization.CODEC).optionalFieldOf("tooltip").forGetter(BingoGoal::getTooltip), ResourceLocation.CODEC.optionalFieldOf("tooltip_icon").forGetter(BingoGoal::getTooltipIcon), - BingoCodecs.optionalDynamicField("icon").forGetter(BingoGoal::getIcon), + ParsedOrSub.optionalCodec(GoalIcon.CODEC, "icon", EmptyIcon.INSTANCE).forGetter(BingoGoal::getIcon), BingoCodecs.optionalPositiveInt("infrequency").forGetter(BingoGoal::getInfrequency), BingoCodecs.minifiedSetField(Codec.STRING, "antisynergy").forGetter(BingoGoal::getAntisynergy), BingoCodecs.minifiedSetField(Codec.STRING, "catalyst").forGetter(BingoGoal::getCatalyst), @@ -64,15 +66,15 @@ public class BingoGoal { ).validate(BingoGoal::validate); private final Map subs; - private final Map> criteria; + private final Map>> criteria; private final AdvancementRequirements requirements; private final ProgressTracker progress; - private final Dynamic requiredCount; + private final ParsedOrSub requiredCount; private final HolderSet tags; - private final Dynamic name; - private final Dynamic tooltip; + private final ParsedOrSub name; + private final Optional> tooltip; private final Optional tooltipIcon; - private final Dynamic icon; + private final ParsedOrSub icon; private final OptionalInt infrequency; private final Set antisynergy; private final Set catalyst; @@ -84,15 +86,15 @@ public class BingoGoal { public BingoGoal( Map subs, - Map> criteria, + Map>> criteria, Optional requirements, ProgressTracker progress, - Dynamic requiredCount, + ParsedOrSub requiredCount, HolderSet tags, - Dynamic name, - Dynamic tooltip, + ParsedOrSub name, + Optional> tooltip, Optional tooltipIcon, - Dynamic icon, + ParsedOrSub icon, OptionalInt infrequency, Collection antisynergy, Collection catalyst, @@ -127,8 +129,10 @@ public BingoGoal( boolean requiresClient = false; final var triggerCodec = ResourceKey.codec(Registries.TRIGGER_TYPE); - for (final Dynamic criterion : criteria.values()) { - final var triggerKey = criterion.get("trigger") + for (final var criterion : criteria.values()) { + final var triggerKey = criterion + .serialized() + .get("trigger") .flatMap(triggerCodec::parse) .result() .orElse(null); @@ -143,43 +147,52 @@ public BingoGoal( } public DataResult validate() { - final DataResult requirementsResult = requirements.validate(criteria.keySet()); - if (requirementsResult.error().isPresent()) { - return DataResult.error(requirementsResult.error().get()::message); + var result = DataResult.success(this); + + final var availableSubs = HashSet.newHashSet(subs.size()); + var substitutionContext = SubstitutionContext.createValidationContext(availableSubs); + for (final var sub : subs.entrySet()) { + result = BingoUtil.combineError(result, sub.getValue().validate(substitutionContext)); + availableSubs.add(sub.getKey()); } final var triggerCodec = ResourceKey.codec(Registries.TRIGGER_TYPE); - for (final Dynamic criterion : criteria.values()) { - final var triggerKey = criterion.get("trigger").flatMap(triggerCodec::parse); - if (triggerKey.isError()) { - //noinspection OptionalGetWithoutIsPresent - return DataResult.error(triggerKey.error().get().messageSupplier()); - } + for (final var criterion : criteria.values()) { + result = BingoUtil.combineError(result, criterion.serialized().get("trigger").flatMap(triggerCodec::parse)); + result = BingoUtil.combineError(result, criterion.validate(substitutionContext)); } + result = BingoUtil.combineError(result, requirements.validate(criteria.keySet())); + result = BingoUtil.combineError(result, progress.validate(this)); + result = BingoUtil.combineError(result, requiredCount.validate(substitutionContext)); + for (final var tag : tags) { final BingoTag.SpecialType type = tag.value().specialType(); if (type != BingoTag.SpecialType.NONE && type != specialType) { - return DataResult.error(() -> "Inconsistent specialTypes: " + type + " does not match " + specialType); + result = BingoUtil.combineError(result, () -> "Inconsistent specialTypes: " + type + " does not match " + specialType); } } - if (specialType == BingoTag.SpecialType.FINISH && requirements.size() != 1) { - return DataResult.error(() -> "\"finish\" goals must have only ORed requirements"); + + result = BingoUtil.combineError(result, name.validate(substitutionContext)); + + if (tooltip.isPresent()) { + result = BingoUtil.combineError(result, tooltip.get().validate(substitutionContext)); } - final DataResult progressResult = progress.validate(this); - if (progressResult.error().isPresent()) { - return DataResult.error(progressResult.error().get()::message); + result = BingoUtil.combineError(result, icon.validate(substitutionContext)); + + if (specialType == BingoTag.SpecialType.FINISH && requirements.size() != 1) { + result = BingoUtil.combineError(result, () -> "\"finish\" goals must have only ORed requirements"); } - return DataResult.success(this); + return result; } public Map getSubs() { return subs; } - public Map> getCriteria() { + public Map>> getCriteria() { return criteria; } @@ -191,7 +204,7 @@ public ProgressTracker getProgress() { return progress; } - public Dynamic getRequiredCount() { + public ParsedOrSub getRequiredCount() { return requiredCount; } @@ -199,11 +212,11 @@ public HolderSet getTags() { return tags; } - public Dynamic getName() { + public ParsedOrSub getName() { return name; } - public Dynamic getTooltip() { + public Optional> getTooltip() { return tooltip; } @@ -211,7 +224,7 @@ public Optional getTooltipIcon() { return tooltipIcon; } - public Dynamic getIcon() { + public ParsedOrSub getIcon() { return icon; } @@ -243,50 +256,36 @@ public boolean isRequiredOnClient() { return requiredOnClient; } - public Map> buildSubs(RandomSource rand) { + public Map> buildSubs(SubstitutionContext context) { final Map> result = new LinkedHashMap<>(); for (final var entry : subs.entrySet()) { - result.put(entry.getKey(), entry.getValue().substitute(result, rand)); + result.put(entry.getKey(), entry.getValue().substitute(context)); } return ImmutableMap.copyOf(result); } - public MutableComponent buildName(Map> referable, RandomSource rand) { - return BingoUtil.ensureHasFallback(BingoUtil.fromDynamic( - ComponentSerialization.CODEC, - GoalSubstitutionSystem.performSubstitutions(name, referable, rand) - ).copy()); + public MutableComponent buildName(SubstitutionContext context) { + return BingoUtil.ensureHasFallback(name.substituteOrThrow(context).copy()); } - public Optional buildTooltip(Map> referable, RandomSource rand) { - if (tooltip.getValue() == tooltip.getOps().empty()) { - return Optional.empty(); - } - return Optional.of( - BingoUtil.ensureHasFallback(BingoUtil.fromDynamic( - ComponentSerialization.CODEC, - GoalSubstitutionSystem.performSubstitutions(tooltip, referable, rand) - ).copy()) - ); + public Optional buildTooltip(SubstitutionContext context) { + return tooltip.map(t -> BingoUtil.ensureHasFallback(t.substituteOrThrow(context).copy())); } - public GoalIcon buildIcon(Map> referable, RandomSource rand) { - if (icon.getValue() == icon.getOps().empty()) { - return EmptyIcon.INSTANCE; - } - return BingoUtil.fromDynamic(GoalIcon.CODEC, GoalSubstitutionSystem.performSubstitutions(icon, referable, rand)); + public GoalIcon buildIcon(SubstitutionContext context) { + return icon.substituteOrThrow(context); } - public Map> buildCriteria(Map> referable, RandomSource rand) { + public Map> buildCriteria(SubstitutionContext context) { final ImmutableMap.Builder> result = ImmutableMap.builderWithExpectedSize(criteria.size()); for (final var entry : criteria.entrySet()) { - result.put(entry.getKey(), BingoUtil.fromDynamic(Criterion.CODEC, GoalSubstitutionSystem.performSubstitutions(entry.getValue(), referable, rand))); + result.put(entry.getKey(), entry.getValue().substituteOrThrow(context)); } return result.build(); } - public int buildRequiredCount(Map> referable, RandomSource rand) { - return BingoUtil.fromDynamic(ExtraCodecs.POSITIVE_INT, GoalSubstitutionSystem.performSubstitutions(requiredCount, referable, rand)); + public int buildRequiredCount(SubstitutionContext context) { + return requiredCount.substituteOrThrow(context); } public static GoalBuilder builder(ResourceLocation id) { diff --git a/common/src/main/java/io/github/gaming32/bingo/data/goal/GoalBuilder.java b/common/src/main/java/io/github/gaming32/bingo/data/goal/GoalBuilder.java index 66134407..9797aaa7 100644 --- a/common/src/main/java/io/github/gaming32/bingo/data/goal/GoalBuilder.java +++ b/common/src/main/java/io/github/gaming32/bingo/data/goal/GoalBuilder.java @@ -12,13 +12,13 @@ import io.github.gaming32.bingo.data.BingoTag; import io.github.gaming32.bingo.data.JsonSubber; import io.github.gaming32.bingo.data.icons.BlockIcon; +import io.github.gaming32.bingo.data.icons.EmptyIcon; import io.github.gaming32.bingo.data.icons.GoalIcon; import io.github.gaming32.bingo.data.progresstrackers.CriterionProgressTracker; import io.github.gaming32.bingo.data.progresstrackers.EmptyProgressTracker; import io.github.gaming32.bingo.data.progresstrackers.ProgressTracker; import io.github.gaming32.bingo.data.subs.BingoSub; -import io.github.gaming32.bingo.util.BingoCodecs; -import io.github.gaming32.bingo.util.BingoUtil; +import io.github.gaming32.bingo.data.subs.ParsedOrSub; import net.minecraft.advancements.AdvancementRequirements; import net.minecraft.advancements.Criterion; import net.minecraft.core.HolderLookup; @@ -28,10 +28,10 @@ import net.minecraft.network.chat.ComponentSerialization; import net.minecraft.resources.ResourceKey; import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.ExtraCodecs; import net.minecraft.world.level.ItemLike; import net.minecraft.world.level.block.Block; -import java.util.Map; import java.util.Optional; import java.util.OptionalInt; import java.util.function.Consumer; @@ -39,18 +39,23 @@ public final class GoalBuilder { public static final ThreadLocal> JSON_OPS = ThreadLocal.withInitial(() -> JsonOps.INSTANCE); + private static final ParsedOrSub DEFAULT_REQUIRED_COUNT = + ParsedOrSub.fromParsed(ExtraCodecs.POSITIVE_INT, 1); + private static final ParsedOrSub DEFAULT_ICON = + ParsedOrSub.fromParsed(GoalIcon.CODEC, EmptyIcon.INSTANCE); + private final ResourceLocation id; private final ImmutableMap.Builder subs = ImmutableMap.builder(); - private final ImmutableMap.Builder> criteria = ImmutableMap.builder(); + private final ImmutableMap.Builder>> criteria = ImmutableMap.builder(); private Optional requirements = Optional.empty(); private ProgressTracker progress = EmptyProgressTracker.INSTANCE; - private Dynamic requiredCount = BingoCodecs.EMPTY_DYNAMIC.createInt(1); + private ParsedOrSub requiredCount = DEFAULT_REQUIRED_COUNT; private AdvancementRequirements.Strategy requirementsStrategy = AdvancementRequirements.Strategy.AND; private final ImmutableSet.Builder> tags = ImmutableSet.builder(); - private Optional> name = Optional.empty(); - private Dynamic tooltip = BingoCodecs.EMPTY_DYNAMIC; + private Optional> name = Optional.empty(); + private Optional> tooltip = Optional.empty(); private Optional tooltipIcon = Optional.empty(); - private Dynamic icon = BingoCodecs.EMPTY_DYNAMIC; + private ParsedOrSub icon = DEFAULT_ICON; private OptionalInt infrequency = OptionalInt.empty(); private ImmutableSet.Builder antisynergy = ImmutableSet.builder(); private final ImmutableSet.Builder catalyst = ImmutableSet.builder(); @@ -67,14 +72,14 @@ public GoalBuilder sub(String key, BingoSub sub) { } public GoalBuilder criterion(String key, Criterion criterion) { - return criterion(key, criterion, subber -> { - }); + criteria.put(key, ParsedOrSub.fromParsed(Criterion.CODEC, criterion, JSON_OPS.get())); + return this; } public GoalBuilder criterion(String key, Criterion criterion, Consumer subber) { JsonSubber json = new JsonSubber(Criterion.CODEC.encodeStart(JSON_OPS.get(), criterion).getOrThrow()); subber.accept(json); - this.criteria.put(key, new Dynamic<>(JsonOps.INSTANCE, json.json())); + this.criteria.put(key, ParsedOrSub.parse(Criterion.CODEC, new Dynamic<>(JsonOps.INSTANCE, json.json()))); return this; } @@ -98,12 +103,12 @@ public GoalBuilder progress(String criterion) { } public GoalBuilder requiredCount(int requiredCount) { - this.requiredCount = BingoCodecs.EMPTY_DYNAMIC.createInt(requiredCount); + this.requiredCount = ParsedOrSub.fromParsed(ExtraCodecs.POSITIVE_INT, requiredCount, JSON_OPS.get()); return this; } public GoalBuilder requiredCount(BingoSub requiredCountSub) { - this.requiredCount = BingoUtil.toDynamic(BingoSub.INNER_CODEC, requiredCountSub); + this.requiredCount = ParsedOrSub.fromSub(requiredCountSub, ExtraCodecs.POSITIVE_INT, JSON_OPS.get()); return this; } @@ -118,14 +123,14 @@ public GoalBuilder name(@Translatable(prefix = "bingo.goal.") String name) { } public GoalBuilder name(Component name) { - return this.name(name, subber -> { - }); + this.name = Optional.of(ParsedOrSub.fromParsed(ComponentSerialization.CODEC, name, JSON_OPS.get())); + return this; } public GoalBuilder name(Component name, Consumer subber) { JsonSubber json = new JsonSubber(ComponentSerialization.CODEC.encodeStart(JSON_OPS.get(), name).getOrThrow()); subber.accept(json); - this.name = Optional.of(new Dynamic<>(JsonOps.INSTANCE, json.json())); + this.name = Optional.of(ParsedOrSub.parse(ComponentSerialization.CODEC, new Dynamic<>(JsonOps.INSTANCE, json.json()))); return this; } @@ -134,14 +139,14 @@ public GoalBuilder tooltip(@Translatable(prefix = "bingo.goal.", suffix = ".tool } public GoalBuilder tooltip(Component tooltip) { - return this.tooltip(tooltip, subber -> { - }); + this.tooltip = Optional.of(ParsedOrSub.fromParsed(ComponentSerialization.CODEC, tooltip, JSON_OPS.get())); + return this; } public GoalBuilder tooltip(Component tooltip, Consumer subber) { JsonSubber json = new JsonSubber(ComponentSerialization.CODEC.encodeStart(JSON_OPS.get(), tooltip).getOrThrow()); subber.accept(json); - this.tooltip = new Dynamic<>(JsonOps.INSTANCE, json.json()); + this.tooltip = Optional.of(ParsedOrSub.parse(ComponentSerialization.CODEC, new Dynamic<>(JsonOps.INSTANCE, json.json()))); return this; } @@ -151,8 +156,7 @@ public GoalBuilder tooltipIcon(ResourceLocation tooltipIcon) { } public GoalBuilder icon(Object icon) { - return icon(icon, subber -> { - }); + return icon(GoalIcon.infer(icon)); } public GoalBuilder icon(Object icon, Consumer subber) { @@ -160,8 +164,7 @@ public GoalBuilder icon(Object icon, Consumer subber) { } public GoalBuilder icon(Block icon, ItemLike fallback) { - return icon(icon, fallback, subber -> { - }); + return icon(BlockIcon.ofBlockAndItem(icon, fallback)); } public GoalBuilder icon(Block icon, ItemLike fallback, Consumer subber) { @@ -169,14 +172,14 @@ public GoalBuilder icon(Block icon, ItemLike fallback, Consumer subb } public GoalBuilder icon(GoalIcon icon) { - return icon(icon, subber -> { - }); + this.icon = ParsedOrSub.fromParsed(GoalIcon.CODEC, icon, JSON_OPS.get()); + return this; } public GoalBuilder icon(GoalIcon icon, Consumer subber) { JsonSubber jsonSubber = new JsonSubber(GoalIcon.CODEC.encodeStart(JSON_OPS.get(), icon).getOrThrow()); subber.accept(jsonSubber); - this.icon = new Dynamic<>(JsonOps.INSTANCE, jsonSubber.json()); + this.icon = ParsedOrSub.parse(GoalIcon.CODEC, new Dynamic<>(JsonOps.INSTANCE, jsonSubber.json())); return this; } @@ -211,7 +214,7 @@ public GoalBuilder difficulty(ResourceKey difficulty) { } public GoalHolder build(HolderLookup.Provider registries) { - final Map> criteria = this.criteria.build(); + final var criteria = this.criteria.build(); return new GoalHolder(id, new BingoGoal( subs.buildOrThrow(), criteria, diff --git a/common/src/main/java/io/github/gaming32/bingo/data/goal/GoalHolder.java b/common/src/main/java/io/github/gaming32/bingo/data/goal/GoalHolder.java index 80144db7..9210795c 100644 --- a/common/src/main/java/io/github/gaming32/bingo/data/goal/GoalHolder.java +++ b/common/src/main/java/io/github/gaming32/bingo/data/goal/GoalHolder.java @@ -1,6 +1,7 @@ package io.github.gaming32.bingo.data.goal; import com.mojang.serialization.Dynamic; +import io.github.gaming32.bingo.data.subs.SubstitutionContext; import io.github.gaming32.bingo.game.ActiveGoal; import net.minecraft.network.chat.Component; import net.minecraft.network.chat.HoverEvent; @@ -13,9 +14,11 @@ public record GoalHolder(ResourceLocation id, BingoGoal goal) { public ActiveGoal build(RandomSource rand) { - final Map> subs = goal.buildSubs(rand); - final Optional tooltip = goal.buildTooltip(subs, rand); - final MutableComponent name = goal.buildName(subs, rand); + final Map> subs = goal.buildSubs(new SubstitutionContext(Map.of(), rand)); + final var context = new SubstitutionContext(subs, rand); + + final Optional tooltip = goal.buildTooltip(context); + final MutableComponent name = goal.buildName(context); tooltip.ifPresent(t -> name.withStyle(s -> s .withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, t)) )); @@ -23,9 +26,9 @@ public ActiveGoal build(RandomSource rand) { id, name, tooltip, goal.getTooltipIcon(), - goal.buildIcon(subs, rand), - goal.buildCriteria(subs, rand), - goal.buildRequiredCount(subs, rand), + goal.buildIcon(context), + goal.buildCriteria(context), + goal.buildRequiredCount(context), Optional.of(goal.getDifficulty()), goal.getRequirements(), goal.getSpecialType(), diff --git a/common/src/main/java/io/github/gaming32/bingo/data/goal/GoalSubstitutionSystem.java b/common/src/main/java/io/github/gaming32/bingo/data/goal/GoalSubstitutionSystem.java deleted file mode 100644 index f046f2df..00000000 --- a/common/src/main/java/io/github/gaming32/bingo/data/goal/GoalSubstitutionSystem.java +++ /dev/null @@ -1,43 +0,0 @@ -package io.github.gaming32.bingo.data.goal; - -import com.google.common.collect.ImmutableMap; -import com.mojang.datafixers.util.Pair; -import com.mojang.serialization.Dynamic; -import io.github.gaming32.bingo.data.subs.BingoSub; -import io.github.gaming32.bingo.util.BingoUtil; -import net.minecraft.util.RandomSource; - -import java.util.Map; - -public class GoalSubstitutionSystem { - private static final String TYPE_FIELD = "bingo_type"; - - public static Dynamic performSubstitutions( - Dynamic value, - Map> referable, - RandomSource rand - ) { - final var asList = value.asStreamOpt(); - if (asList.result().isPresent()) { - return value.createList( - asList.result().get().map(d -> performSubstitutions(d, referable, rand).convert(d.getOps())) - ); - } - - if (value.get(TYPE_FIELD).result().isPresent()) { - return BingoUtil.fromDynamic(BingoSub.INNER_CODEC, value).substitute(referable, rand); - } - - final var asMap = value.asMapOpt(); - if (asMap.result().isPresent()) { - return value.createMap(asMap.result().get() - .collect(ImmutableMap.toImmutableMap( - Pair::getFirst, - e -> performSubstitutions(e.getSecond(), referable, rand).convert(e.getSecond().getOps()) - )) - ); - } - - return value; - } -} diff --git a/common/src/main/java/io/github/gaming32/bingo/data/subs/BingoSub.java b/common/src/main/java/io/github/gaming32/bingo/data/subs/BingoSub.java index 448555e0..619f7a6f 100644 --- a/common/src/main/java/io/github/gaming32/bingo/data/subs/BingoSub.java +++ b/common/src/main/java/io/github/gaming32/bingo/data/subs/BingoSub.java @@ -2,14 +2,13 @@ import com.mojang.datafixers.util.Either; import com.mojang.serialization.Codec; +import com.mojang.serialization.DataResult; import com.mojang.serialization.Dynamic; import io.github.gaming32.bingo.util.BingoCodecs; import io.github.gaming32.bingo.util.BingoUtil; -import net.minecraft.util.RandomSource; import net.minecraft.util.valueproviders.ConstantInt; import net.minecraft.util.valueproviders.UniformInt; -import java.util.Map; import java.util.function.Function; import java.util.stream.Stream; @@ -22,7 +21,7 @@ public interface BingoSub { .dispatch(BingoSub::type, BingoSubType::codec) ).xmap( either -> either.map(SubBingoSub::new, Function.identity()), - sub -> sub instanceof SubBingoSub subSub ? Either.left(subSub.key()) : Either.right(sub) + sub -> sub instanceof SubBingoSub(String key) ? Either.left(key) : Either.right(sub) ); Codec INNER_CODEC = BingoSubType.REGISTER @@ -30,7 +29,11 @@ public interface BingoSub { .byNameCodec() .dispatch("bingo_type", BingoSub::type, BingoSubType::codec); - Dynamic substitute(Map> referable, RandomSource rand); + Dynamic substitute(SubstitutionContext context); + + default DataResult validate(SubstitutionContext context) { + return DataResult.success(this); + } BingoSubType type(); diff --git a/common/src/main/java/io/github/gaming32/bingo/data/subs/CompoundBingoSub.java b/common/src/main/java/io/github/gaming32/bingo/data/subs/CompoundBingoSub.java index 5c13de90..066af3d6 100644 --- a/common/src/main/java/io/github/gaming32/bingo/data/subs/CompoundBingoSub.java +++ b/common/src/main/java/io/github/gaming32/bingo/data/subs/CompoundBingoSub.java @@ -2,18 +2,18 @@ import com.google.common.collect.ImmutableMultiset; import com.google.common.collect.Multisets; +import com.mojang.serialization.DataResult; import com.mojang.serialization.Dynamic; import com.mojang.serialization.MapCodec; import com.mojang.serialization.codecs.RecordCodecBuilder; +import io.github.gaming32.bingo.util.BingoUtil; import net.minecraft.util.ExtraCodecs; -import net.minecraft.util.RandomSource; import net.minecraft.util.StringRepresentable; import org.jetbrains.annotations.NotNull; import java.util.Collections; import java.util.List; import java.util.Locale; -import java.util.Map; import java.util.function.BinaryOperator; import java.util.function.Function; import java.util.stream.Stream; @@ -36,15 +36,22 @@ public CompoundBingoSub(ElementType elementType, Operator operator, BingoSub... } @Override - public Dynamic substitute(Map> referable, RandomSource rand) { + public Dynamic substitute(SubstitutionContext context) { final var op = elementType.accumulator.apply(operator); - Dynamic result = factors.getFirst().substitute(referable, rand); + Dynamic result = factors.getFirst().substitute(context); for (int i = 1; i < factors.size(); i++) { - result = op.apply(result, factors.get(i).substitute(referable, rand)); + result = op.apply(result, factors.get(i).substitute(context)); } return result; } + @Override + public DataResult validate(SubstitutionContext context) { + return factors.stream() + .map(sub -> sub.validate(context)) + .reduce(DataResult.success(this), BingoUtil::combineError, (a, b) -> a); + } + @Override public BingoSubType type() { return BingoSubType.COMPOUND.get(); diff --git a/common/src/main/java/io/github/gaming32/bingo/data/subs/IntBingoSub.java b/common/src/main/java/io/github/gaming32/bingo/data/subs/IntBingoSub.java index 73452ebb..8a1d144f 100644 --- a/common/src/main/java/io/github/gaming32/bingo/data/subs/IntBingoSub.java +++ b/common/src/main/java/io/github/gaming32/bingo/data/subs/IntBingoSub.java @@ -2,22 +2,16 @@ import com.mojang.serialization.Dynamic; import com.mojang.serialization.MapCodec; -import io.github.gaming32.bingo.util.BingoCodecs; -import net.minecraft.util.RandomSource; import net.minecraft.util.valueproviders.IntProvider; -import java.util.Map; - public record IntBingoSub(IntProvider provider) implements BingoSub { public static final MapCodec CODEC = IntProvider.CODEC .fieldOf("value") .xmap(IntBingoSub::new, IntBingoSub::provider); @Override - public Dynamic substitute(Map> referable, RandomSource rand) { - return referable.isEmpty() - ? BingoCodecs.EMPTY_DYNAMIC.createInt(provider.sample(rand)) - : referable.values().iterator().next().createInt(provider.sample(rand)); + public Dynamic substitute(SubstitutionContext context) { + return context.getFactoryDynamic().createInt(provider.sample(context.rand())); } @Override diff --git a/common/src/main/java/io/github/gaming32/bingo/data/subs/ParsedOrSub.java b/common/src/main/java/io/github/gaming32/bingo/data/subs/ParsedOrSub.java new file mode 100644 index 00000000..ba597212 --- /dev/null +++ b/common/src/main/java/io/github/gaming32/bingo/data/subs/ParsedOrSub.java @@ -0,0 +1,63 @@ +package io.github.gaming32.bingo.data.subs; + +import com.mojang.datafixers.util.Either; +import com.mojang.serialization.Codec; +import com.mojang.serialization.DataResult; +import com.mojang.serialization.Dynamic; +import com.mojang.serialization.DynamicOps; +import com.mojang.serialization.MapCodec; +import io.github.gaming32.bingo.util.BingoCodecs; +import io.github.gaming32.bingo.util.BingoUtil; + +import java.util.function.Function; + +public record ParsedOrSub(Dynamic serialized, Either, Codec> valueOrCodec) { + public static MapCodec> optionalCodec(Codec valueCodec, String key, T defaultValue) { + return codec(valueCodec).optionalFieldOf(key, fromParsed(valueCodec, defaultValue)); + } + + public static Codec> codec(Codec valueCodec) { + return Codec.PASSTHROUGH.xmap(data -> parse(valueCodec, data), ParsedOrSub::serialized); + } + + public static ParsedOrSub parse(Codec valueCodec, Dynamic data) { + if (SubstitutionEngine.hasSubstitutions(data)) { + return new ParsedOrSub<>(data, Either.right(valueCodec)); + } + return new ParsedOrSub<>(data, Either.left(valueCodec.parse(data))); + } + + public static ParsedOrSub fromParsed(Codec valueCodec, T value) { + return fromParsed(valueCodec, value, BingoCodecs.DEFAULT_OPS); + } + + public static ParsedOrSub fromParsed(Codec valueCodec, T value, DynamicOps ops) { + return new ParsedOrSub<>(BingoUtil.toDynamic(valueCodec, value, ops), Either.left(DataResult.success(value))); + } + + public static ParsedOrSub fromSub(BingoSub sub, Codec valueCodec) { + return fromSub(sub, valueCodec, BingoCodecs.DEFAULT_OPS); + } + + public static ParsedOrSub fromSub(BingoSub sub, Codec valueCodec, DynamicOps ops) { + return new ParsedOrSub<>(BingoUtil.toDynamic(BingoSub.INNER_CODEC, sub, ops), Either.right(valueCodec)); + } + + public DataResult> validate(SubstitutionContext context) { + return valueOrCodec.map( + Function.identity(), + r -> SubstitutionEngine.validateSubstitutions(serialized, context) + ).map(x -> this); + } + + public DataResult substitute(SubstitutionContext context) { + return valueOrCodec.map( + Function.identity(), + r -> r.parse(SubstitutionEngine.performSubstitutions(serialized, context)) + ); + } + + public T substituteOrThrow(SubstitutionContext context) { + return substitute(context).getOrThrow(IllegalArgumentException::new); + } +} diff --git a/common/src/main/java/io/github/gaming32/bingo/data/subs/SubBingoSub.java b/common/src/main/java/io/github/gaming32/bingo/data/subs/SubBingoSub.java index e1b30fbc..cd07526b 100644 --- a/common/src/main/java/io/github/gaming32/bingo/data/subs/SubBingoSub.java +++ b/common/src/main/java/io/github/gaming32/bingo/data/subs/SubBingoSub.java @@ -1,11 +1,9 @@ package io.github.gaming32.bingo.data.subs; import com.mojang.serialization.Codec; +import com.mojang.serialization.DataResult; import com.mojang.serialization.Dynamic; import com.mojang.serialization.MapCodec; -import net.minecraft.util.RandomSource; - -import java.util.Map; public record SubBingoSub(String key) implements BingoSub { public static final MapCodec CODEC = Codec.STRING @@ -13,14 +11,22 @@ public record SubBingoSub(String key) implements BingoSub { .xmap(SubBingoSub::new, SubBingoSub::key); @Override - public Dynamic substitute(Map> referable, RandomSource rand) { - final Dynamic value = referable.get(key); + public Dynamic substitute(SubstitutionContext context) { + final Dynamic value = context.referable().get(key); if (value == null) { throw new IllegalArgumentException("Unresolved reference in bingo goal: " + key); } return value; } + @Override + public DataResult validate(SubstitutionContext context) { + if (!context.referable().containsKey(key)) { + return DataResult.error(() -> "Unresolved reference in bingo goal: " + key); + } + return DataResult.success(this); + } + @Override public BingoSubType type() { return BingoSubType.SUB.get(); diff --git a/common/src/main/java/io/github/gaming32/bingo/data/subs/SubstitutionContext.java b/common/src/main/java/io/github/gaming32/bingo/data/subs/SubstitutionContext.java new file mode 100644 index 00000000..2a9e1360 --- /dev/null +++ b/common/src/main/java/io/github/gaming32/bingo/data/subs/SubstitutionContext.java @@ -0,0 +1,22 @@ +package io.github.gaming32.bingo.data.subs; + +import com.google.common.collect.Maps; +import com.mojang.serialization.Dynamic; +import io.github.gaming32.bingo.util.BingoCodecs; +import net.minecraft.util.RandomSource; + +import java.util.Map; +import java.util.Set; + +public record SubstitutionContext(Map> referable, RandomSource rand) { + public static SubstitutionContext createValidationContext(Set keys) { + return new SubstitutionContext( + Maps.asMap(keys, k -> BingoCodecs.EMPTY_DYNAMIC), + RandomSource.createNewThreadLocalInstance() + ); + } + + public Dynamic getFactoryDynamic() { + return referable.isEmpty() ? BingoCodecs.EMPTY_DYNAMIC : referable.values().iterator().next(); + } +} diff --git a/common/src/main/java/io/github/gaming32/bingo/data/subs/SubstitutionEngine.java b/common/src/main/java/io/github/gaming32/bingo/data/subs/SubstitutionEngine.java new file mode 100644 index 00000000..9e2490bb --- /dev/null +++ b/common/src/main/java/io/github/gaming32/bingo/data/subs/SubstitutionEngine.java @@ -0,0 +1,75 @@ +package io.github.gaming32.bingo.data.subs; + +import com.mojang.datafixers.util.Pair; +import com.mojang.datafixers.util.Unit; +import com.mojang.serialization.DataResult; +import com.mojang.serialization.Dynamic; +import io.github.gaming32.bingo.util.BingoUtil; + +import java.util.function.Function; +import java.util.stream.Stream; + +public class SubstitutionEngine { + private static final String TYPE_FIELD = "bingo_type"; + + public static boolean hasSubstitutions(Dynamic value) { + final var asList = value.asStreamOpt(); + if (asList.result().isPresent()) { + return asList.result().get().anyMatch(SubstitutionEngine::hasSubstitutions); + } + + if (value.get(TYPE_FIELD).result().isPresent()) { + return true; + } + + return value.asMapOpt() + .result() + .stream() + .flatMap(Function.identity()) + .map(Pair::getSecond) + .anyMatch(SubstitutionEngine::hasSubstitutions); + } + + public static Dynamic performSubstitutions(Dynamic value, SubstitutionContext context) { + final var asList = value.asStreamOpt(); + if (asList.result().isPresent()) { + return value.createList( + asList.result().get().map(d -> performSubstitutions(d, context).convert(d.getOps())) + ); + } + + if (value.get(TYPE_FIELD).result().isPresent()) { + return BingoUtil.fromDynamic(BingoSub.INNER_CODEC, value).substitute(context); + } + + return value.updateMapValues(pair -> pair.mapSecond(subValue -> + performSubstitutions(subValue, context).convert(subValue.getOps()) + )); + } + + public static DataResult> validateSubstitutions(Dynamic value, SubstitutionContext context) { + final var asList = value.asStreamOpt(); + if (asList.result().isPresent()) { + return validateStream(asList.result().get(), context).map(x -> value); + } + + if (value.get(TYPE_FIELD).result().isPresent()) { + return BingoSub.INNER_CODEC.parse(value) + .flatMap(sub -> sub.validate(context)) + .map(x -> value); + } + + final var asMap = value.asMapOpt(); + if (asMap.result().isPresent()) { + return validateStream(asMap.result().get().map(Pair::getSecond), context).map(x -> value); + } + + return DataResult.success(value); + } + + private static DataResult validateStream(Stream> stream, SubstitutionContext context) { + return stream + .map(d -> validateSubstitutions(d, context)) + .reduce(DataResult.success(Unit.INSTANCE), BingoUtil::combineError, (a, b) -> a); + } +} diff --git a/common/src/main/java/io/github/gaming32/bingo/data/subs/WrapBingoSub.java b/common/src/main/java/io/github/gaming32/bingo/data/subs/WrapBingoSub.java index 27a6369e..1d10f714 100644 --- a/common/src/main/java/io/github/gaming32/bingo/data/subs/WrapBingoSub.java +++ b/common/src/main/java/io/github/gaming32/bingo/data/subs/WrapBingoSub.java @@ -2,15 +2,13 @@ import com.google.gson.JsonElement; import com.mojang.serialization.Codec; +import com.mojang.serialization.DataResult; import com.mojang.serialization.Dynamic; import com.mojang.serialization.JsonOps; import com.mojang.serialization.MapCodec; import io.github.gaming32.bingo.data.JsonSubber; -import io.github.gaming32.bingo.data.goal.GoalSubstitutionSystem; import net.minecraft.Util; -import net.minecraft.util.RandomSource; -import java.util.Map; import java.util.function.Consumer; public record WrapBingoSub(Dynamic value) implements BingoSub { @@ -23,8 +21,13 @@ public WrapBingoSub(JsonElement value, Consumer subber) { } @Override - public Dynamic substitute(Map> referable, RandomSource rand) { - return GoalSubstitutionSystem.performSubstitutions(value, referable, rand); + public Dynamic substitute(SubstitutionContext context) { + return SubstitutionEngine.performSubstitutions(value, context); + } + + @Override + public DataResult validate(SubstitutionContext context) { + return SubstitutionEngine.validateSubstitutions(value, context).map(x -> this); } @Override diff --git a/common/src/main/java/io/github/gaming32/bingo/game/BingoBoard.java b/common/src/main/java/io/github/gaming32/bingo/game/BingoBoard.java index 67cf68b4..8fb7ed29 100644 --- a/common/src/main/java/io/github/gaming32/bingo/game/BingoBoard.java +++ b/common/src/main/java/io/github/gaming32/bingo/game/BingoBoard.java @@ -1,18 +1,19 @@ package io.github.gaming32.bingo.game; import com.mojang.serialization.Codec; +import com.mojang.serialization.DataResult; import com.mojang.serialization.codecs.RecordCodecBuilder; import io.github.gaming32.bingo.data.BingoDifficulty; import io.github.gaming32.bingo.data.BingoRegistries; import io.github.gaming32.bingo.data.BingoTag; import io.github.gaming32.bingo.data.goal.GoalHolder; import io.github.gaming32.bingo.data.goal.GoalManager; -import io.github.gaming32.bingo.util.BingoCodecs; import io.github.gaming32.bingo.util.BingoUtil; import io.github.gaming32.bingo.util.ResourceLocations; import io.netty.buffer.ByteBuf; import it.unimi.dsi.fastutil.objects.Object2IntMap; import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; +import net.minecraft.Util; import net.minecraft.core.Holder; import net.minecraft.core.HolderLookup; import net.minecraft.core.HolderSet; @@ -41,12 +42,8 @@ public class BingoBoard { public static final int MAX_SIZE = 7; public static final int DEFAULT_SIZE = 5; - public static final Codec PERSISTENCE_CODEC = RecordCodecBuilder.create( - instance -> instance.group( - Codec.INT.fieldOf("size").forGetter(BingoBoard::getSize), - BingoCodecs.array(Teams.CODEC, Teams.class).fieldOf("states").forGetter(BingoBoard::getStates), - BingoCodecs.array(ActiveGoal.PERSISTENCE_CODEC, ActiveGoal.class).fieldOf("goals").forGetter(BingoBoard::getGoals) - ).apply(instance, BingoBoard::create) + public static final Codec PERSISTENCE_CODEC = PartiallyParsed.CODEC.xmap( + BingoBoard::create, PartiallyParsed::create ); private final int size; @@ -66,11 +63,13 @@ private BingoBoard(int size) { toGoalIndex.defaultReturnValue(-1); } - private static BingoBoard create(int size, Teams[] states, ActiveGoal[] goals) { + private static BingoBoard create(PartiallyParsed parsed) { + final var size = parsed.size; final BingoBoard board = new BingoBoard(size); - System.arraycopy(states, 0, board.states, 0, size * size); + parsed.states.toArray(board.states); + parsed.goals.toArray(board.goals); for (int i = 0; i < size * size; i++) { - final ActiveGoal goal = board.goals[i] = goals[i]; // TODO: Fix AIOOBE in exceptional cases + final ActiveGoal goal = board.goals[i]; board.byVanillaId.put(generateVanillaId(i), goal); board.toGoalIndex.put(goal, i); } @@ -458,4 +457,27 @@ public String toString() { return "Teams[" + Integer.toBinaryString(bits) + "]"; } } + + private record PartiallyParsed(int size, List states, List goals) { + static final Codec CODEC = RecordCodecBuilder.create( + instance -> instance.group( + Codec.INT.fieldOf("size").forGetter(PartiallyParsed::size), + Teams.CODEC.listOf().fieldOf("states").forGetter(PartiallyParsed::states), + ActiveGoal.PERSISTENCE_CODEC.listOf().fieldOf("goals").forGetter(PartiallyParsed::goals) + ).apply(instance, PartiallyParsed::new) + ).validate(PartiallyParsed::validate); + + static PartiallyParsed create(BingoBoard board) { + return new PartiallyParsed(board.size, List.of(board.states), List.of(board.goals)); + } + + private DataResult validate() { + // fixedSize does create a shortened partial result, but we only care about it having a partial, as the + // shortening is also handled in create() above. + var result = DataResult.success(this); + result = BingoUtil.combineError(result, Util.fixedSize(states, size * size)); + result = BingoUtil.combineError(result, Util.fixedSize(goals, size * size)); + return result; + } + } } diff --git a/common/src/main/java/io/github/gaming32/bingo/util/BingoCodecs.java b/common/src/main/java/io/github/gaming32/bingo/util/BingoCodecs.java index f8cc8ed6..6974c87f 100644 --- a/common/src/main/java/io/github/gaming32/bingo/util/BingoCodecs.java +++ b/common/src/main/java/io/github/gaming32/bingo/util/BingoCodecs.java @@ -30,6 +30,7 @@ import net.minecraft.world.item.ItemStack; import java.lang.reflect.Array; +import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.OptionalInt; @@ -189,9 +190,13 @@ public static MapCodec> optionalDynamicField(String name, Dynamic ); } - @SuppressWarnings("unchecked") public static Codec array(Codec elementCodec, Class aClass) { - return elementCodec.listOf().xmap( + return arrayFromList(elementCodec.listOf(), aClass); + } + + @SuppressWarnings("unchecked") + public static Codec arrayFromList(Codec> listCodec, Class aClass) { + return listCodec.xmap( list -> list.toArray((A[])Array.newInstance(aClass, list.size())), ImmutableList::copyOf ); diff --git a/common/src/main/java/io/github/gaming32/bingo/util/BingoUtil.java b/common/src/main/java/io/github/gaming32/bingo/util/BingoUtil.java index dbeb1acd..ab64876b 100644 --- a/common/src/main/java/io/github/gaming32/bingo/util/BingoUtil.java +++ b/common/src/main/java/io/github/gaming32/bingo/util/BingoUtil.java @@ -7,6 +7,7 @@ import com.google.gson.JsonElement; import com.mojang.datafixers.util.Either; import com.mojang.serialization.Codec; +import com.mojang.serialization.DataResult; import com.mojang.serialization.Dynamic; import com.mojang.serialization.DynamicOps; import com.mojang.serialization.JsonOps; @@ -37,6 +38,7 @@ import java.util.Objects; import java.util.Set; import java.util.function.Function; +import java.util.function.Supplier; import java.util.stream.Collector; import java.util.stream.Stream; @@ -267,4 +269,12 @@ public static Set copyAndAdd(Set set, T value) { public static Multimap copyAndPut(Multimap map, K k, V v) { return ImmutableMultimap.builder().putAll(map).put(k, v).build(); } + + public static DataResult combineError(DataResult current, DataResult other) { + return current.apply2((a, b) -> a, other); + } + + public static DataResult combineError(DataResult current, Supplier error) { + return combineError(current, DataResult.error(error)); + } }