package com.hypixel.hytale.server.npc.asset.builder; import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.hypixel.hytale.codec.ExtraInfo; import com.hypixel.hytale.codec.schema.NamedSchema; import com.hypixel.hytale.codec.schema.SchemaContext; import com.hypixel.hytale.codec.schema.SchemaConvertable; import com.hypixel.hytale.codec.schema.config.ArraySchema; import com.hypixel.hytale.codec.schema.config.ObjectSchema; import com.hypixel.hytale.codec.schema.config.Schema; import com.hypixel.hytale.codec.schema.config.StringSchema; import com.hypixel.hytale.logger.HytaleLogger; import com.hypixel.hytale.logger.sentry.SkipSentryException; import com.hypixel.hytale.server.core.modules.interaction.interaction.config.RootInteraction; import com.hypixel.hytale.server.core.util.BsonUtil; import com.hypixel.hytale.server.npc.asset.builder.expression.BuilderExpression; import com.hypixel.hytale.server.npc.asset.builder.holder.StringHolder; import com.hypixel.hytale.server.npc.asset.builder.validators.StateStringValidator; import com.hypixel.hytale.server.npc.asset.builder.validators.StringNotEmptyValidator; import com.hypixel.hytale.server.npc.config.balancing.BalanceAsset; import com.hypixel.hytale.server.npc.util.expression.ExecutionContext; import com.hypixel.hytale.server.npc.util.expression.Scope; import com.hypixel.hytale.server.npc.util.expression.StdScope; import com.hypixel.hytale.server.npc.util.expression.ValueType; import it.unimi.dsi.fastutil.objects.Object2ObjectMap; import it.unimi.dsi.fastutil.objects.Object2ObjectMaps; import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; import it.unimi.dsi.fastutil.objects.ObjectArrayList; import it.unimi.dsi.fastutil.objects.ObjectIterator; import it.unimi.dsi.fastutil.objects.Object2ObjectMap.Entry; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.function.Consumer; import javax.annotation.Nonnull; import javax.annotation.Nullable; public class BuilderModifier { public static final String KEY_MODIFY = "Modify"; public static final String KEY_EXPORT_STATES = "_ExportStates"; public static final String KEY_INTERFACE_PARAMETERS = "_InterfaceParameters"; public static final String KEY_COMBAT_CONFIG = "_CombatConfig"; public static final String KEY_INTERACTION_VARS = "_InteractionVars"; private final Object2ObjectMap builderExpressionMap; private final StatePair[] exportedStateIndexes; private final StateMappingHelper stateHelper; private final String combatConfig; private final Map interactionVars; protected BuilderModifier( Object2ObjectMap builderExpressionMap, StatePair[] exportedStateIndexes, StateMappingHelper stateHelper, String combatConfig, Map interactionVars ) { this.builderExpressionMap = builderExpressionMap; this.exportedStateIndexes = exportedStateIndexes; this.stateHelper = stateHelper; this.combatConfig = combatConfig; this.interactionVars = interactionVars; } public String getCombatConfig() { return this.combatConfig; } public Map getInteractionVars() { return this.interactionVars; } public boolean isEmpty() { return this.builderExpressionMap.isEmpty(); } public int exportedStateCount() { return this.exportedStateIndexes.length; } public void applyComponentStateMap(@Nonnull BuilderSupport support) { support.setModifiedStateMap(this.stateHelper, this.exportedStateIndexes); } public void popComponentStateMap(@Nonnull BuilderSupport support) { support.popModifiedStateMap(); } @Nonnull public Scope createScope(@Nonnull BuilderSupport builderSupport, @Nonnull BuilderParameters builderParameters, Scope globalScope) { ExecutionContext executionContext = builderSupport.getExecutionContext(); return this.createScope(executionContext, builderParameters, globalScope); } @Nonnull public Scope createScope(ExecutionContext executionContext, @Nonnull BuilderParameters builderParameters, @Nullable Scope globalScope) { StdScope scope = builderParameters.createScope(); if (globalScope != null) { StdScope mergedScope = new StdScope(globalScope); mergedScope.merge(scope); scope = mergedScope; } StdScope finalScope = scope; ObjectIterator> iterator = Object2ObjectMaps.fastIterator(this.builderExpressionMap); while (iterator.hasNext()) { Entry pair = (Entry)iterator.next(); String name = (String)pair.getKey(); BuilderModifier.ExpressionHolder holder = (BuilderModifier.ExpressionHolder)pair.getValue(); ValueType valueType = builderParameters.getParameterType(name); BuilderExpression expression = holder.getExpression(builderParameters.getInterfaceCode()); if (expression != null) { if (valueType == ValueType.VOID) { throw new SkipSentryException(new IllegalStateException("Parameter " + name + " does not exist or is private")); } if (!ValueType.isAssignableType(expression.getType(), valueType)) { throw new SkipSentryException( new IllegalStateException("Parameter " + name + " has type " + expression.getType() + " but should be " + valueType) ); } expression.updateScope(finalScope, name, executionContext); } } return scope; } @Nonnull public static BuilderModifier fromJSON( @Nonnull JsonObject jsonObject, @Nonnull BuilderParameters builderParameters, @Nonnull StateMappingHelper helper, @Nonnull ExtraInfo extraInfo ) { JsonObject modify = null; JsonElement modifyObject = jsonObject.get("Modify"); if (modifyObject != null) { modify = BuilderBase.expectObject(modifyObject, "Modify"); } if (modify != null && !modify.entrySet().isEmpty()) { Object2ObjectMap map = new Object2ObjectOpenHashMap(); List exportedStateIndexes = new ObjectArrayList(); for (java.util.Map.Entry stringElementPair : modify.entrySet()) { String key = stringElementPair.getKey(); if (map.containsKey(key)) { throw new SkipSentryException(new IllegalStateException("Duplicate entry '" + key + "' in 'Modify' block")); } if (!key.equals("_InterfaceParameters") && !key.equals("_CombatConfig") && !key.equals("_InteractionVars")) { if (key.equals("_ExportStates")) { if (!stringElementPair.getValue().isJsonArray()) { throw new SkipSentryException(new IllegalStateException(String.format("%s in modifier block must be a Json Array", "_ExportStates"))); } StateStringValidator validator = StateStringValidator.requireMainState(); JsonArray array = stringElementPair.getValue().getAsJsonArray(); for (int i = 0; i < array.size(); i++) { String state = array.get(i).getAsString(); if (!validator.test(state)) { throw new SkipSentryException(new IllegalStateException(validator.errorMessage(state))); } String substate = validator.hasSubState() ? validator.getSubState() : helper.getDefaultSubState(); helper.getAndPutSetterIndex( validator.getMainState(), substate, (m, s) -> exportedStateIndexes.add(new StatePair(validator.getMainState(), m, s)) ); } } else { BuilderExpression expression = BuilderExpression.fromJSON(stringElementPair.getValue(), builderParameters, false); map.put(key, new BuilderModifier.ExpressionHolder(expression)); } } } JsonElement interfaceValue = modify.get("_InterfaceParameters"); if (interfaceValue != null) { JsonObject interfaceParameters = BuilderBase.expectObject(interfaceValue, "_InterfaceParameters"); for (java.util.Map.Entry interfaceEntry : interfaceParameters.entrySet()) { String interfaceKey = interfaceEntry.getKey(); JsonObject parameters = BuilderBase.expectObject(interfaceEntry.getValue()); for (java.util.Map.Entry parameterEntry : parameters.entrySet()) { BuilderModifier.ExpressionHolder holder = (BuilderModifier.ExpressionHolder)map.computeIfAbsent( parameterEntry.getKey(), keyx -> new BuilderModifier.ExpressionHolder() ); if (holder.hasInterfaceMappedExpression(interfaceKey)) { throw new SkipSentryException( new IllegalStateException("Duplicate entry '" + parameterEntry.getKey() + "' in 'Modify' block for interface '" + interfaceKey) ); } holder.addInterfaceMappedExpression(interfaceKey, BuilderExpression.fromJSON(parameterEntry.getValue(), builderParameters, false)); } } } String combatConfig = null; JsonElement combatConfigValue = modify.get("_CombatConfig"); if (combatConfigValue != null) { combatConfig = combatConfigValue.getAsString(); } Map interactionVars = null; JsonElement interactionVarsValue = modify.get("_InteractionVars"); if (interactionVarsValue != null) { interactionVars = RootInteraction.CHILD_ASSET_CODEC_MAP.decode(BsonUtil.translateJsonToBson(interactionVarsValue), extraInfo); extraInfo.getValidationResults()._processValidationResults(); extraInfo.getValidationResults().logOrThrowValidatorExceptions(HytaleLogger.getLogger()); } return new BuilderModifier(map, exportedStateIndexes.toArray(StatePair[]::new), helper, combatConfig, interactionVars); } else { return EmptyBuilderModifier.INSTANCE; } } public static void readModifierObject( @Nonnull JsonObject jsonObject, @Nonnull BuilderParameters builderParameters, @Nonnull StringHolder holder, @Nonnull Consumer referenceConsumer, @Nonnull Consumer builderModifierConsumer, @Nonnull StateMappingHelper helper, @Nonnull ExtraInfo extraInfo ) { holder.readJSON(BuilderBase.expectKey(jsonObject, "Reference"), StringNotEmptyValidator.get(), "Reference", builderParameters); BuilderModifier modifier = fromJSON(jsonObject, builderParameters, helper, extraInfo); referenceConsumer.accept(holder); builderModifierConsumer.accept(modifier); } @Nonnull public static Schema toSchema(@Nonnull SchemaContext context) { return context.refDefinition(BuilderModifier.SchemaGenerator.INSTANCE); } private static class ExpressionHolder { private final BuilderExpression expression; private Object2ObjectMap interfaceMappedExpressions; public ExpressionHolder() { this(null); } public ExpressionHolder(BuilderExpression expression) { this.expression = expression; } public boolean hasInterfaceMappedExpression(String interfaceKey) { return this.interfaceMappedExpressions != null && this.interfaceMappedExpressions.containsKey(interfaceKey); } public void addInterfaceMappedExpression(String interfaceKey, BuilderExpression expression) { if (this.interfaceMappedExpressions == null) { this.interfaceMappedExpressions = new Object2ObjectOpenHashMap(); } this.interfaceMappedExpressions.put(interfaceKey, expression); } public BuilderExpression getExpression(@Nullable String interfaceKey) { return interfaceKey != null && this.interfaceMappedExpressions != null && this.interfaceMappedExpressions.containsKey(interfaceKey) ? (BuilderExpression)this.interfaceMappedExpressions.get(interfaceKey) : this.expression; } } private static class SchemaGenerator implements SchemaConvertable, NamedSchema { @Nonnull public static BuilderModifier.SchemaGenerator INSTANCE = new BuilderModifier.SchemaGenerator(); @Nonnull @Override public String getSchemaName() { return "NPC:Type:BuilderModifier"; } @Nonnull @Override public Schema toSchema(@Nonnull SchemaContext context) { ObjectSchema s = new ObjectSchema(); s.setTitle("BuilderModifier"); LinkedHashMap props = new LinkedHashMap<>(); s.setProperties(props); props.put("_ExportStates", new ArraySchema(new StringSchema())); props.put("_InterfaceParameters", new ObjectSchema()); StringSchema combatConfig = new StringSchema(); combatConfig.setHytaleAssetRef(BalanceAsset.class.getSimpleName()); props.put("_CombatConfig", combatConfig); ObjectSchema interactionVars = new ObjectSchema(); interactionVars.setTitle("Map"); Schema childSchema = context.refDefinition(RootInteraction.CHILD_ASSET_CODEC); interactionVars.setAdditionalProperties(childSchema); props.put("_InteractionVars", interactionVars); s.setAdditionalProperties(BuilderExpression.toSchema(context)); return s; } } }