package com.hypixel.hytale.server.npc.role.builders; import com.google.gson.JsonElement; import com.hypixel.hytale.math.vector.Vector3d; import com.hypixel.hytale.server.core.asset.type.attitude.Attitude; import com.hypixel.hytale.server.core.asset.type.blockset.config.BlockSet; import com.hypixel.hytale.server.npc.NPCPlugin; import com.hypixel.hytale.server.npc.asset.builder.Builder; import com.hypixel.hytale.server.npc.asset.builder.BuilderCodecObjectHelper; import com.hypixel.hytale.server.npc.asset.builder.BuilderCombatConfig; import com.hypixel.hytale.server.npc.asset.builder.BuilderDescriptorState; import com.hypixel.hytale.server.npc.asset.builder.BuilderFactory; import com.hypixel.hytale.server.npc.asset.builder.BuilderInfo; import com.hypixel.hytale.server.npc.asset.builder.BuilderObjectListHelper; import com.hypixel.hytale.server.npc.asset.builder.BuilderObjectReferenceHelper; import com.hypixel.hytale.server.npc.asset.builder.BuilderSupport; import com.hypixel.hytale.server.npc.asset.builder.BuilderTemplateInteractionVars; import com.hypixel.hytale.server.npc.asset.builder.BuilderValidationHelper; import com.hypixel.hytale.server.npc.asset.builder.FeatureEvaluatorHelper; import com.hypixel.hytale.server.npc.asset.builder.InstructionContextHelper; import com.hypixel.hytale.server.npc.asset.builder.InstructionType; import com.hypixel.hytale.server.npc.asset.builder.SpawnableWithModelBuilder; import com.hypixel.hytale.server.npc.asset.builder.holder.AssetArrayHolder; import com.hypixel.hytale.server.npc.asset.builder.holder.AssetHolder; import com.hypixel.hytale.server.npc.asset.builder.holder.BooleanHolder; import com.hypixel.hytale.server.npc.asset.builder.holder.EnumHolder; import com.hypixel.hytale.server.npc.asset.builder.holder.FloatHolder; import com.hypixel.hytale.server.npc.asset.builder.holder.IntHolder; import com.hypixel.hytale.server.npc.asset.builder.holder.StringArrayHolder; import com.hypixel.hytale.server.npc.asset.builder.holder.StringHolder; import com.hypixel.hytale.server.npc.asset.builder.validators.AssetValidator; import com.hypixel.hytale.server.npc.asset.builder.validators.DoubleRangeValidator; import com.hypixel.hytale.server.npc.asset.builder.validators.DoubleSingleValidator; import com.hypixel.hytale.server.npc.asset.builder.validators.IntRangeValidator; import com.hypixel.hytale.server.npc.asset.builder.validators.IntSingleValidator; import com.hypixel.hytale.server.npc.asset.builder.validators.StringArrayNoEmptyStringsValidator; import com.hypixel.hytale.server.npc.asset.builder.validators.StringNotEmptyValidator; import com.hypixel.hytale.server.npc.asset.builder.validators.StringNullOrNotEmptyValidator; import com.hypixel.hytale.server.npc.asset.builder.validators.TagSetExistsValidator; import com.hypixel.hytale.server.npc.asset.builder.validators.asset.AttitudeGroupExistsValidator; import com.hypixel.hytale.server.npc.asset.builder.validators.asset.BlockSetExistsValidator; import com.hypixel.hytale.server.npc.asset.builder.validators.asset.ItemAttitudeGroupExistsValidator; import com.hypixel.hytale.server.npc.asset.builder.validators.asset.ItemDropListExistsValidator; import com.hypixel.hytale.server.npc.asset.builder.validators.asset.ItemExistsValidator; import com.hypixel.hytale.server.npc.asset.builder.validators.asset.ModelExistsValidator; import com.hypixel.hytale.server.npc.asset.builder.validators.asset.RootInteractionValidator; import com.hypixel.hytale.server.npc.config.AttitudeGroup; import com.hypixel.hytale.server.npc.config.ItemAttitudeGroup; import com.hypixel.hytale.server.npc.config.balancing.BalanceAsset; import com.hypixel.hytale.server.npc.decisionmaker.stateevaluator.StateEvaluator; import com.hypixel.hytale.server.npc.instructions.Instruction; import com.hypixel.hytale.server.npc.movement.controllers.BuilderMotionControllerMapUtil; import com.hypixel.hytale.server.npc.movement.controllers.MotionController; import com.hypixel.hytale.server.npc.movement.controllers.builders.BuilderMotionControllerBase; import com.hypixel.hytale.server.npc.role.Role; import com.hypixel.hytale.server.npc.role.RoleDebugFlags; import com.hypixel.hytale.server.npc.role.SpawnEffect; import com.hypixel.hytale.server.npc.role.support.WorldSupport; import com.hypixel.hytale.server.npc.statetransition.StateTransitionController; import com.hypixel.hytale.server.npc.util.expression.ExecutionContext; import com.hypixel.hytale.server.npc.util.expression.Scope; import com.hypixel.hytale.server.npc.validators.NPCLoadTimeValidationHelper; import com.hypixel.hytale.server.spawning.ISpawnable; import com.hypixel.hytale.server.spawning.SpawnTestResult; import com.hypixel.hytale.server.spawning.SpawningContext; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import it.unimi.dsi.fastutil.ints.IntSet; import java.util.EnumSet; import java.util.List; import java.util.Map; import java.util.Objects; import javax.annotation.Nonnull; import javax.annotation.Nullable; import org.checkerframework.checker.nullness.compatqual.NullableDecl; public class BuilderRole extends SpawnableWithModelBuilder implements SpawnEffect { protected String[] displayNames; protected final AssetHolder appearance = new AssetHolder(); protected final AssetHolder dropListId = new AssetHolder(); protected final IntHolder maxHealth = new IntHolder(); protected String startState; protected String defaultSubState; protected int startStateIndex; protected int startSubStateIndex; protected final EnumSet parsedDebugFlags = EnumSet.noneOf(RoleDebugFlags.class); protected String debugFlags; protected double inertia; protected double knockbackScale; protected String opaqueBlockSet; protected boolean applyAvoidance; protected double entityAvoidanceStrength; protected double collisionDistance; protected double collisionForceFalloff; protected double collisionRadius; protected float collisionViewAngle; protected double separationDistance; protected double separationWeight; protected double separationDistanceTarget; protected double separationNearRadiusTarget; protected double separationFarRadiusTarget; protected final BooleanHolder applySeparation = new BooleanHolder(); protected boolean stayInEnvironment; protected String allowedEnvironments; protected final StringArrayHolder flockSpawnTypes = new StringArrayHolder(); protected final BooleanHolder flockSpawnTypeRandom = new BooleanHolder(); protected final StringArrayHolder flockAllowedRoles = new StringArrayHolder(); protected final BooleanHolder canLeadFlock = new BooleanHolder(); protected final FloatHolder spawnLockTime = new FloatHolder(); protected double flockWeightAlignment; protected double flockWeightSeparation; protected double flockWeightCohesion; protected double flockInfluenceRange; protected boolean corpseStaysInFlock; protected final BooleanHolder invulnerable = new BooleanHolder(); protected final BooleanHolder breathesInAir = new BooleanHolder(); protected final BooleanHolder breathesInWater = new BooleanHolder(); protected final AssetArrayHolder hotbarItems = new AssetArrayHolder(); protected final AssetArrayHolder offHandItems = new AssetArrayHolder(); protected final AssetHolder inventoryItemsDropList = new AssetHolder(); protected final IntHolder defaultOffHandSlot = new IntHolder(); protected boolean pickupDropOnDeath; protected String[] armor; protected double deathAnimationTime; protected float despawnAnimationTime; @Nonnull protected AssetHolder deathInteraction = new AssetHolder(); protected Role.AvoidanceMode avoidanceMode; protected boolean disableDamageFlock; protected final AssetArrayHolder disableDamageGroups = new AssetArrayHolder(); protected String spawnParticles; protected double[] spawnParticleOffset; protected double spawnViewDistance; protected int inventorySlots; protected int hotbarSlots; protected int offHandSlots; protected final EnumHolder defaultPlayerAttitude = new EnumHolder<>(); protected final EnumHolder defaultNPCAttitude = new EnumHolder<>(); protected final AssetHolder attitudeGroup = new AssetHolder(); protected final AssetHolder itemAttitudeGroup = new AssetHolder(); protected Int2ObjectMap busyStates; protected final BuilderObjectReferenceHelper> motionControllers = new BuilderObjectReferenceHelper<>( BuilderMotionControllerMapUtil.CLASS_REFERENCE, this ); protected final BuilderObjectListHelper instructionList = new BuilderObjectListHelper<>(Instruction.class, this); protected final BuilderObjectReferenceHelper interactionInstruction = new BuilderObjectReferenceHelper<>(Instruction.class, this); protected final BuilderObjectReferenceHelper deathInstruction = new BuilderObjectReferenceHelper<>(Instruction.class, this); protected final BuilderObjectReferenceHelper stateTransitionController = new BuilderObjectReferenceHelper<>( StateTransitionController.class, this ); protected final StringHolder initialMotionController = new StringHolder(); protected final BuilderCodecObjectHelper stateEvaluator = new BuilderCodecObjectHelper<>(StateEvaluator.class, StateEvaluator.CODEC, null); protected final BuilderCombatConfig combatConfig = new BuilderCombatConfig(BalanceAsset.CHILD_ASSET_CODEC, BalanceAsset.VALIDATOR_CACHE.getValidator()); protected final BuilderTemplateInteractionVars interactionVars = new BuilderTemplateInteractionVars(); protected final BooleanHolder isMemory = new BooleanHolder(); protected final StringHolder memoriesCategory = new StringHolder(); protected final StringHolder memoriesNameOverride = new StringHolder(); protected final StringHolder nameTranslationKey = new StringHolder(); @Nonnull @Override public String getShortDescription() { return "Generic role for NPC"; } @Nonnull @Override public String getLongDescription() { return "Generic role for NPC with a core planner and list of Motion controllers."; } @Nonnull @Override public BuilderDescriptorState getBuilderDescriptorState() { return BuilderDescriptorState.Stable; } @Nonnull public Role build(@Nonnull BuilderSupport builderSupport) { return new Role(this, builderSupport); } @Override public boolean validate( String configName, @Nonnull NPCLoadTimeValidationHelper validationHelper, @Nonnull ExecutionContext context, Scope globalScope, @Nonnull List errors ) { validationHelper.setInventorySizes(this.inventorySlots, this.hotbarSlots, this.offHandSlots); boolean hotbarValid; try { String[] hotbar = this.hotbarItems.get(context); hotbarValid = hotbar == null || hotbar.length == 0 || validationHelper.validateHotbarHasSlot(hotbar.length - 1, "HotbarItems", errors); } catch (IllegalStateException var14) { errors.add(String.format("%s: %s", configName, var14.getMessage())); hotbarValid = false; } boolean offHandValid; try { String[] offHand = this.offHandItems.get(context); offHandValid = (offHand == null || offHand.length == 0 || validationHelper.validateOffHandHasSlot(offHand.length - 1, "OffHandItems", errors)) & validationHelper.validateOffHandHasSlot(this.defaultOffHandSlot.get(context), "DefaultOffHandSlot", errors); } catch (IllegalStateException var13) { errors.add(String.format("%s: %s", configName, var13.getMessage())); offHandValid = false; } boolean validInitialMotionController = true; String mc = this.initialMotionController.get(context); if (mc != null) { BuilderFactory factory = this.builderManager.getFactory(MotionController.class); try { BuilderMotionControllerBase builder = (BuilderMotionControllerBase)factory.createBuilder(mc); validationHelper.requireMotionControllerType(builder.getClassType()); } catch (IllegalArgumentException var12) { errors.add(String.format("%s: Unable to set InitialMotionController to %s as no such MotionController exists", configName, mc)); validInitialMotionController = false; } } return super.validate(configName, validationHelper, context, globalScope, errors) & hotbarValid & offHandValid & validInitialMotionController & this.instructionList.validate(configName, validationHelper, this.builderManager, context, globalScope, errors) & this.interactionInstruction.validate(configName, validationHelper, this.builderManager, context, globalScope, errors) & this.deathInstruction.validate(configName, validationHelper, this.builderManager, context, globalScope, errors) & this.stateTransitionController.validate(configName, validationHelper, this.builderManager, context, globalScope, errors) & this.motionControllers.validate(configName, validationHelper, this.builderManager, context, globalScope, errors) & this.combatConfig.validate(configName, validationHelper, context, errors); } @Nonnull public BuilderRole readConfig(@Nonnull JsonElement data) { super.readCommonConfig(data); this.setNotComponent(); this.requireInt(data, "MaxHealth", this.maxHealth, IntSingleValidator.greater0(), BuilderDescriptorState.Stable, "Max health", null); this.getString(data, "Debug", e -> this.debugFlags = e, "", null, BuilderDescriptorState.WorkInProgress, "Debugging flags", null); this.requireAsset(data, "Appearance", this.appearance, ModelExistsValidator.required(), BuilderDescriptorState.Stable, "Model to use for rendering", null); this.getStringArray( data, "DisplayNames", a -> this.displayNames = a, null, null, StringArrayNoEmptyStringsValidator.get(), BuilderDescriptorState.Stable, "List of possible display names to choose from", null ); this.requireString( data, "NameTranslationKey", this.nameTranslationKey, StringNotEmptyValidator.get(), BuilderDescriptorState.Stable, "The translation key for this NPC's name", null ); this.getAsset( data, "OpaqueBlockSet", v -> this.opaqueBlockSet = v, "Opaque", BlockSetExistsValidator.withConfig(AssetValidator.CanBeEmpty), BuilderDescriptorState.Stable, "Blocks blocking line of sight", null ); this.getDouble(data, "Inertia", d -> this.inertia = d, 1.0, DoubleSingleValidator.greater(0.1), BuilderDescriptorState.Experimental, "Inertia", null); this.getDouble( data, "KnockbackScale", v -> this.knockbackScale = v, 1.0, DoubleSingleValidator.greaterEqual0(), BuilderDescriptorState.Stable, "Scale factor for knockback", "Scale factor for knockback. Values greater 1 increase knockback. Smaller values decrease it." ); this.getInt( data, "InventorySize", v -> this.inventorySlots = v, 0, IntRangeValidator.between(0, 36), BuilderDescriptorState.Stable, "Number of available inventory slots", null ); this.getInt( data, "HotbarSize", v -> this.hotbarSlots = v, 3, IntRangeValidator.between(3, 8), BuilderDescriptorState.Stable, "Number of available hotbar slots", null ); this.getInt( data, "OffHandSlots", v -> this.offHandSlots = v, 0, IntRangeValidator.between(0, 4), BuilderDescriptorState.Stable, "The number of slots for off-hand items", null ); this.getAssetArray( data, "HotbarItems", this.hotbarItems, null, 0, 8, ItemExistsValidator.orDroplistWithConfig(AssetValidator.ListCanBeEmpty), BuilderDescriptorState.Stable, "Hotbar items (e.g. primary weapon, secondary weapon, etc)", null ); this.getAssetArray( data, "OffHandItems", this.offHandItems, null, 0, 8, ItemExistsValidator.withConfig(AssetValidator.ListCanBeEmpty), BuilderDescriptorState.Stable, "Off-hand items (e.g. shields, torches, etc)", null ); this.getAsset( data, "PossibleInventoryItems", this.inventoryItemsDropList, null, ItemDropListExistsValidator.withConfig(AssetValidator.CanBeEmpty), BuilderDescriptorState.Stable, "A droplist defining the possible items the NPCs inventory could contain", null ); this.getInt( data, "DefaultOffHandSlot", this.defaultOffHandSlot, -1, IntRangeValidator.between(-1, 4), BuilderDescriptorState.Stable, "The default off-hand item slot (-1 is empty)", null ); this.getAssetArray( data, "Armor", a -> this.armor = a, null, null, ItemExistsValidator.withConfig(AssetValidator.ListCanBeEmpty), BuilderDescriptorState.WorkInProgress, "Armor items", null ); this.getAsset( data, "DropList", this.dropListId, null, ItemDropListExistsValidator.withConfig(AssetValidator.CanBeEmpty), BuilderDescriptorState.Stable, "Drop list to spawn when killed", null ); this.getString(data, "StartState", s -> this.startState = s, "start", StringNotEmptyValidator.get(), BuilderDescriptorState.Stable, "Initial state", null); this.getDefaultSubState( data, "DefaultSubState", v -> this.defaultSubState = v, StringNotEmptyValidator.get(), BuilderDescriptorState.Stable, "The default sub state to reference when transitioning to a main state without a specified sub state", null ); this.getDouble( data, "CollisionDistance", d -> this.collisionDistance = d, 5.0, DoubleSingleValidator.greater0(), BuilderDescriptorState.Stable, "Collision lookahead", null ); this.getDouble( data, "CollisionForceFalloff", d -> this.collisionForceFalloff = d, 2.0, DoubleSingleValidator.greater0(), BuilderDescriptorState.Experimental, "Falloff rate for collision force", null ); this.getDouble(data, "CollisionRadius", d -> this.collisionRadius = d, -1.0, null, BuilderDescriptorState.Experimental, "Collision radius override", null); this.getFloat( data, "CollisionViewAngle", d -> this.collisionViewAngle = d, 320.0F, DoubleRangeValidator.between(0.0, 360.0), BuilderDescriptorState.Experimental, "Collision detection view cone", null ); this.getDouble( data, "SeparationDistance", d -> this.separationDistance = d, 3.0, DoubleSingleValidator.greater0(), BuilderDescriptorState.Experimental, "Desired separation distance", null ); this.getDouble( data, "SeparationWeight", d -> this.separationWeight = d, 1.0, DoubleSingleValidator.greaterEqual0(), BuilderDescriptorState.Experimental, "Blend factor separation", null ); this.getDouble( data, "SeparationDistanceTarget", d -> this.separationDistanceTarget = d, 1.0, DoubleSingleValidator.greaterEqual0(), BuilderDescriptorState.Experimental, "Desired separation distance when close to target", null ); this.getDouble( data, "SeparationNearRadiusTarget", d -> this.separationNearRadiusTarget = d, 1.0, DoubleSingleValidator.greater0(), BuilderDescriptorState.Experimental, "Distance when using SeparationDistanceTarget", null ); this.getDouble( data, "SeparationFarRadiusTarget", d -> this.separationFarRadiusTarget = d, 5.0, DoubleSingleValidator.greater0(), BuilderDescriptorState.Experimental, "Use normal separation distance from further than this distance", null ); this.getBoolean(data, "ApplyAvoidance", b -> this.applyAvoidance = b, false, BuilderDescriptorState.Experimental, "Apply avoidance steering force", null); this.getBoolean(data, "ApplySeparation", this.applySeparation, false, BuilderDescriptorState.Experimental, "Apply separation steering force", null); this.getEnum( data, "AvoidanceMode", e -> this.avoidanceMode = e, Role.AvoidanceMode.class, Role.AvoidanceMode.Any, BuilderDescriptorState.Experimental, "Abilities to use for avoidance", null ); this.getDouble( data, "EntityAvoidanceStrength", d -> this.entityAvoidanceStrength = d, 1.0, DoubleSingleValidator.greaterEqual0(), BuilderDescriptorState.Experimental, "Blending factor avoidance", null ); this.getBoolean( data, "StayInEnvironment", b -> this.stayInEnvironment = b, false, BuilderDescriptorState.Experimental, "Stay in spawning environment", null ); this.getString( data, "AllowedEnvironments", s -> this.allowedEnvironments = s, null, StringNullOrNotEmptyValidator.get(), BuilderDescriptorState.Experimental, "Allowed environment to walk in", null ); this.getStringArray( data, "FlockSpawnTypes", this.flockSpawnTypes, null, 0, Integer.MAX_VALUE, StringArrayNoEmptyStringsValidator.get(), BuilderDescriptorState.WorkInProgress, "Types of NPC this flock should consist off", null ); this.getBoolean( data, "FlockSpawnTypesRandom", this.flockSpawnTypeRandom, false, BuilderDescriptorState.WorkInProgress, "Create a randomized flock if true else spawn in order of FlockSpawnTypes", null ); this.getStringArray( data, "FlockAllowedNPC", this.flockAllowedRoles, null, 0, Integer.MAX_VALUE, StringArrayNoEmptyStringsValidator.get(), BuilderDescriptorState.Experimental, "List of NPCs allowed in flock", null ); this.getBoolean(data, "FlockCanLead", this.canLeadFlock, false, BuilderDescriptorState.Experimental, "This NPC can be flock leader", null); this.getDouble( data, "FlockWeightAlignment", v -> this.flockWeightAlignment = v, 1.0, DoubleSingleValidator.greater0(), BuilderDescriptorState.Experimental, "Blending flock alignment", null ); this.getDouble( data, "FlockWeightSeparation", v -> this.flockWeightSeparation = v, 1.0, DoubleSingleValidator.greater0(), BuilderDescriptorState.Experimental, "Blending flock separation", null ); this.getDouble( data, "FlockWeightCohesion", v -> this.flockWeightCohesion = v, 1.0, DoubleSingleValidator.greater0(), BuilderDescriptorState.Experimental, "Blending flock cohesion", null ); this.getDouble( data, "FlockInfluenceRange", v -> this.flockInfluenceRange = v, 10.0, DoubleSingleValidator.greater0(), BuilderDescriptorState.Experimental, "Influence radius flock forces", null ); this.getBoolean( data, "DisableDamageFlock", b -> this.disableDamageFlock = b, true, BuilderDescriptorState.WorkInProgress, "If true disables combat damage from flock members", null ); this.getAssetArray( data, "DisableDamageGroups", this.disableDamageGroups, null, 0, Integer.MAX_VALUE, TagSetExistsValidator.withConfig(AssetValidator.ListCanBeEmpty), BuilderDescriptorState.WorkInProgress, "Members in this list of group won't cause damage", null ); this.getExistentStateSet( data, "BusyStates", s -> this.busyStates = s, this.stateHelper, BuilderDescriptorState.Stable, "States during which this NPC is busy and can't be interacted with", null ); this.getCodecObject( data, "CombatConfig", this.combatConfig, BuilderDescriptorState.Stable, "The combat configuration providing optional combat action evaluator", null ); this.getBoolean(data, "Invulnerable", this.invulnerable, false, BuilderDescriptorState.Stable, "Makes NPC ignore damage", null); this.getBoolean(data, "BreathesInAir", this.breathesInAir, true, BuilderDescriptorState.WorkInProgress, "Can breath in air", null); this.getBoolean(data, "BreathesInWater", this.breathesInWater, false, BuilderDescriptorState.WorkInProgress, "Can breath in fluid/water", null); this.getBoolean(data, "PickupDropOnDeath", t -> this.pickupDropOnDeath = t, false, BuilderDescriptorState.Stable, "Drop last picked item on death", null); this.getDouble( data, "DeathAnimationTime", d -> this.deathAnimationTime = d, 5.0, DoubleSingleValidator.greaterEqual0(), BuilderDescriptorState.Experimental, "How long to let the death animation play before removing", null ); this.getAsset( data, "DeathInteraction", this.deathInteraction, null, RootInteractionValidator.withConfig(AssetValidator.CanBeEmpty), BuilderDescriptorState.Experimental, "Interaction to run on death", null ); this.getFloat( data, "DespawnAnimationTime", d -> this.despawnAnimationTime = d, 0.8F, DoubleSingleValidator.greaterEqual0(), BuilderDescriptorState.Experimental, "How long to let the despawn animation play before removing", null ); this.getString( data, "SpawnParticles", v -> this.spawnParticles = v, null, null, BuilderDescriptorState.Experimental, "Particle system when spawning", null ); this.getVector3d( data, "SpawnParticlesOffset", v -> this.spawnParticleOffset = v, null, null, BuilderDescriptorState.Experimental, "Displacement from foot point to spawn relative to NPC heading", null ); this.getDouble( data, "SpawnViewDistance", v -> this.spawnViewDistance = v, 75.0, DoubleSingleValidator.greater0(), BuilderDescriptorState.Experimental, "View distance for spawn particle", null ); this.getEnum( data, "DefaultPlayerAttitude", this.defaultPlayerAttitude, Attitude.class, Attitude.HOSTILE, BuilderDescriptorState.Stable, "The default attitude of this NPC towards players", null ); this.getEnum( data, "DefaultNPCAttitude", this.defaultNPCAttitude, Attitude.class, Attitude.NEUTRAL, BuilderDescriptorState.Stable, "The default attitude of this NPC towards other NPCs", null ); this.getAsset( data, "AttitudeGroup", this.attitudeGroup, null, AttitudeGroupExistsValidator.withConfig(EnumSet.of(AssetValidator.Config.NULLABLE)), BuilderDescriptorState.Stable, "The attitude group towards other NPCs this NPC belongs to (often species related)", null ); this.getAsset( data, "ItemAttitudeGroup", this.itemAttitudeGroup, null, ItemAttitudeGroupExistsValidator.withConfig(EnumSet.of(AssetValidator.Config.NULLABLE)), BuilderDescriptorState.Stable, "This NPC's item attitudes", null ); this.getBoolean( data, "CorpseStaysInFlock", b -> this.corpseStaysInFlock = b, false, BuilderDescriptorState.Stable, "Whether the NPC should stay in the flock until corpse removal or be removed at the moment of death", null ); this.validateAny(this.breathesInAir, this.breathesInWater); this.registerStateSetter(this.startState, this.defaultSubState, (m, s) -> { this.startStateIndex = m; this.startSubStateIndex = s; }); if (this.debugFlags != null && !this.debugFlags.isEmpty()) { this.parsedDebugFlags.addAll(this.toDebugFlagSet("RoleDebugFlags", this.debugFlags)); } this.requireObject( data, "MotionControllerList", this.motionControllers, BuilderDescriptorState.Stable, "Motion controllers", null, new BuilderValidationHelper(this.fileName, null, this.internalReferenceResolver, null, null, this.extraInfo, this.evaluators, this.readErrors) ); this.getArray( data, "Instructions", this.instructionList, null, BuilderDescriptorState.WorkInProgress, "List of instructions", null, new BuilderValidationHelper( this.fileName, null, this.internalReferenceResolver, this.stateHelper, new InstructionContextHelper(InstructionType.Default), this.extraInfo, this.evaluators, this.readErrors ) ); this.getObject( data, "InteractionInstruction", this.interactionInstruction, BuilderDescriptorState.Stable, "Interaction instruction", "An instruction designed to evaluate and set which players can interact with an NPC, along with setting correct states upon interaction", new BuilderValidationHelper( this.fileName, null, this.internalReferenceResolver, this.stateHelper, new InstructionContextHelper(InstructionType.Interaction), this.extraInfo, this.evaluators, this.readErrors ) ); this.getObject( data, "DeathInstruction", this.deathInstruction, BuilderDescriptorState.Stable, "Death instruction", "An instruction which will run only when the NPC is dead until they are removed", new BuilderValidationHelper( this.fileName, null, this.internalReferenceResolver, this.stateHelper, new InstructionContextHelper(InstructionType.Death), this.extraInfo, this.evaluators, this.readErrors ) ); this.getObject( data, "StateTransitions", this.stateTransitionController, BuilderDescriptorState.Stable, "State transition actions", "A set of state transitions and the actions that will be executed during them", new BuilderValidationHelper( this.fileName, new FeatureEvaluatorHelper().lock(), this.internalReferenceResolver, this.stateHelper, new InstructionContextHelper(InstructionType.StateTransitions), this.extraInfo, this.evaluators, this.readErrors ) ); this.getCodecObject(data, "StateEvaluator", this.stateEvaluator, BuilderDescriptorState.Stable, "A state evaluator", null); this.getString( data, "InitialMotionController", this.initialMotionController, null, StringNullOrNotEmptyValidator.get(), BuilderDescriptorState.Stable, "The initial motion controller to set", "The initial motion controller to set. If omitted and there are multiple, one will be chosen at random." ); this.getCodecObject(data, "InteractionVars", this.interactionVars, BuilderDescriptorState.Stable, "Interaction vars to be used in interactions.", null); this.getBoolean(data, "IsMemory", this.isMemory, false, BuilderDescriptorState.Stable, "Used to define if the NPC has a Memory to record.", null); this.getString( data, "MemoriesCategory", this.memoriesCategory, "Other", StringNullOrNotEmptyValidator.get(), BuilderDescriptorState.Stable, "Category to put the NPC in, as part of the Memories Plugin", null ); this.getString( data, "MemoriesNameOverride", this.memoriesNameOverride, "", null, BuilderDescriptorState.Stable, "Overrides the Memory name when set.", null ); this.getFloat( data, "SpawnLockTime", this.spawnLockTime, 1.5, DoubleSingleValidator.greaterEqual0(), BuilderDescriptorState.Stable, "How long the NPC should be locked and unable to perform behavior when first spawned", null ); StateEvaluator stateEvaluator = this.stateEvaluator.build(); if (stateEvaluator != null) { this.evaluators.add(stateEvaluator); stateEvaluator.prepareOptions(this.stateHelper); this.stateHelper.setHasStateEvaluator(); } return this; } @Nonnull @Override public String getIdentifier() { BuilderInfo builderInfo = NPCPlugin.get().getBuilderInfo(this); Objects.requireNonNull(builderInfo, "Have builder but can't get builderInfo for it"); return builderInfo.getKeyName(); } @Nonnull @Override public SpawnTestResult canSpawn(@Nonnull SpawningContext context) { Builder> builder = this.motionControllers.getBuilder(this.builderManager, context.getExecutionContext(), this); if (builder instanceof ISpawnable) { SpawnTestResult result = ((ISpawnable)builder).canSpawn(context); if (result != SpawnTestResult.TEST_OK) { return result; } if (!context.canBreathe(this.breathesInAir.get(context.getExecutionContext()), this.breathesInWater.get(context.getExecutionContext()))) { return SpawnTestResult.FAIL_NOT_BREATHABLE; } } return SpawnTestResult.TEST_OK; } @Nonnull @Override public Class category() { return Role.class; } @Override public String getSpawnModelName(ExecutionContext context, Scope modifierScope) { return this.appearance.get(context); } @Nullable @Override public Scope createModifierScope(ExecutionContext executionContext) { return null; } @Nonnull @Override public Scope createExecutionScope() { return this.getBuilderParameters().createScope(); } @Override public void markNeedsReload() { NPCPlugin.get().setRoleBuilderNeedsReload(this); } @Override public String getSpawnParticles() { return this.spawnParticles; } @Override public Vector3d getSpawnParticleOffset() { return createVector3d(this.spawnParticleOffset, Vector3d.ZERO::clone); } @Override public double getSpawnViewDistance() { return this.spawnViewDistance; } @Override public final boolean isEnabled(ExecutionContext context) { return true; } public int getMaxHealth(@Nonnull BuilderSupport builderSupport) { return this.maxHealth.get(builderSupport.getExecutionContext()); } @Nullable public String[] getDisplayNames() { return this.displayNames; } public String getNameTranslationKey(BuilderSupport support) { return this.nameTranslationKey.get(support.getExecutionContext()); } public String getAppearance(@Nonnull BuilderSupport builderSupport) { return this.appearance.get(builderSupport.getExecutionContext()); } public boolean isBreathesInAir(BuilderSupport support) { return this.breathesInAir.get(support.getExecutionContext()); } public boolean isBreathesInWater(BuilderSupport support) { return this.breathesInWater.get(support.getExecutionContext()); } public int getOpaqueBlockSet() { int index = BlockSet.getAssetMap().getIndex(this.opaqueBlockSet); if (index == Integer.MIN_VALUE) { throw new IllegalArgumentException("Unknown key! " + this.opaqueBlockSet); } else { return index; } } public double getInertia() { return this.inertia; } public double getKnockbackScale() { return this.knockbackScale; } @Nullable public String[] getHotbarItems(@Nonnull BuilderSupport support) { return this.hotbarItems.get(support.getExecutionContext()); } @Nullable public String[] getOffHandItems(@Nonnull BuilderSupport support) { return this.offHandItems.get(support.getExecutionContext()); } public String getInventoryItemsDropList(@Nonnull BuilderSupport support) { return this.inventoryItemsDropList.get(support.getExecutionContext()); } public String[] getArmor() { return this.armor; } public boolean isPickupDropOnDeath() { return this.pickupDropOnDeath; } public String getDropListId(@Nonnull BuilderSupport builderSupport) { return this.dropListId.get(builderSupport.getExecutionContext()); } public String getStartState() { return this.startState; } public int getStartStateIndex() { return this.startStateIndex; } public int getStartSubStateIndex() { return this.startSubStateIndex; } public double getCollisionDistance() { return this.collisionDistance; } public double getCollisionForceFalloff() { return this.collisionForceFalloff; } public boolean isAvoidingEntities() { return this.applyAvoidance; } public Role.AvoidanceMode getAvoidanceMode() { return this.avoidanceMode; } public double getCollisionRadius() { return this.collisionRadius; } public double getSeparationDistance() { return this.separationDistance; } public double getSeparationWeight() { return this.separationWeight; } public double getSeparationDistanceTarget() { return this.separationDistanceTarget; } public double getSeparationNearRadiusTarget() { return this.separationNearRadiusTarget; } public double getSeparationFarRadiusTarget() { return this.separationFarRadiusTarget; } public boolean isApplySeparation(BuilderSupport support) { return this.applySeparation.get(support.getExecutionContext()); } public boolean isStayingInEnvironment() { return this.stayInEnvironment; } public String getAllowedEnvironments() { return this.allowedEnvironments; } public double getEntityAvoidanceStrength() { return this.entityAvoidanceStrength; } @Nullable public String[] getFlockSpawnTypes(@Nonnull BuilderSupport support) { return this.flockSpawnTypes.get(support.getExecutionContext()); } public boolean isFlockSpawnTypeRandom(@Nonnull BuilderSupport support) { return this.flockSpawnTypeRandom.get(support.getExecutionContext()); } @Nonnull public String[] getFlockAllowedRoles(@Nonnull BuilderSupport support) { return this.nonNull(this.flockAllowedRoles.get(support.getExecutionContext())); } public boolean isCanLeadFlock(@Nonnull BuilderSupport support) { return this.canLeadFlock.get(support.getExecutionContext()); } public double getFlockWeightAlignment() { return this.flockWeightAlignment; } public double getFlockWeightSeparation() { return this.flockWeightSeparation; } public double getFlockWeightCohesion() { return this.flockWeightCohesion; } public double getFlockInfluenceRange() { return this.flockInfluenceRange; } @Nonnull public EnumSet getDebugFlags() { return this.parsedDebugFlags; } public float getCollisionViewAngle() { return (float) (Math.PI / 180.0) * this.collisionViewAngle; } @Nullable public String getBalanceAsset(@Nonnull BuilderSupport support) { return this.combatConfig.build(support.getExecutionContext()); } public double getDeathAnimationTime() { return this.deathAnimationTime; } @Nullable public String getDeathInteraction(@Nonnull BuilderSupport builderSupport) { String val = this.deathInteraction.get(builderSupport.getExecutionContext()); return val != null && !val.isEmpty() ? val : null; } public float getDespawnAnimationTime() { return this.despawnAnimationTime; } public boolean isDisableDamageFlock() { return this.disableDamageFlock; } public int[] getDisableDamageGroups(@Nonnull BuilderSupport support) { return WorldSupport.createTagSetIndexArray(this.disableDamageGroups.get(support.getExecutionContext())); } public boolean isInvulnerable(BuilderSupport support) { return this.invulnerable.get(support.getExecutionContext()); } public int getInventorySlots() { return this.inventorySlots; } public int getHotbarSlots() { return this.hotbarSlots; } public int getOffHandSlots() { return this.offHandSlots; } public byte getDefaultOffHandSlot(@Nonnull BuilderSupport support) { return (byte)this.defaultOffHandSlot.get(support.getExecutionContext()); } public Int2ObjectMap getBusyStates() { return this.busyStates; } public Attitude getDefaultPlayerAttitude(@Nonnull BuilderSupport support) { return this.defaultPlayerAttitude.get(support.getExecutionContext()); } public Attitude getDefaultNPCAttitude(@Nonnull BuilderSupport support) { return this.defaultNPCAttitude.get(support.getExecutionContext()); } public int getAttitudeGroup(@Nonnull BuilderSupport support) { String groupName = this.attitudeGroup.get(support.getExecutionContext()); return AttitudeGroup.getAssetMap().getIndex(groupName); } public int getItemAttitudeGroup(@Nonnull BuilderSupport support) { String groupName = this.itemAttitudeGroup.get(support.getExecutionContext()); return ItemAttitudeGroup.getAssetMap().getIndex(groupName); } public boolean isCorpseStaysInFlock() { return this.corpseStaysInFlock; } @Nullable public Map getMotionControllerMap(@Nonnull BuilderSupport support) { return this.motionControllers.build(support); } public String getInitialMotionController(@Nonnull BuilderSupport support) { return this.initialMotionController.get(support.getExecutionContext()); } @Nullable public List getInstructionList(@Nonnull BuilderSupport support) { support.setCurrentInstructionContext(InstructionType.Default); return this.instructionList.build(support); } @Nullable public Instruction getInteractionInstruction(@Nonnull BuilderSupport support) { support.setCurrentInstructionContext(InstructionType.Interaction); return this.interactionInstruction.build(support); } @Nullable public Instruction getDeathInstruction(@Nonnull BuilderSupport support) { support.setCurrentInstructionContext(InstructionType.Death); return this.deathInstruction.build(support); } @Nullable public StateTransitionController getStateTransitionController(@Nonnull BuilderSupport support) { support.setCurrentInstructionContext(InstructionType.Default); return this.stateTransitionController.build(support); } public void registerStateEvaluator(@Nonnull BuilderSupport support) { support.setStateEvaluator(this.stateEvaluator.build()); } @Nullable public Map getInteractionVars(@Nonnull BuilderSupport support) { return this.interactionVars.build(support.getExecutionContext()); } public boolean isMemory(ExecutionContext context) { return this.isMemory.get(context); } @Override public boolean isMemory(ExecutionContext context, Scope modifierScope) { return this.isMemory.get(context); } @Override public String getMemoriesCategory(ExecutionContext context, Scope modifierScope) { return this.memoriesCategory.get(context); } public String getMemoriesNameOverride(ExecutionContext context) { return this.memoriesNameOverride.get(context); } @Override public String getMemoriesNameOverride(ExecutionContext context, @NullableDecl Scope modifierScope) { return this.memoriesNameOverride.get(context); } @Nonnull @Override public String getNameTranslationKey(ExecutionContext context, @NullableDecl Scope modifierScope) { return this.nameTranslationKey.get(context); } public float getSpawnLockTime(BuilderSupport support) { return this.spawnLockTime.get(support.getExecutionContext()); } @Override protected void runLoadTimeValidationHelper0( String configName, NPCLoadTimeValidationHelper loadTimeValidationHelper, ExecutionContext context, @Nonnull List errors ) { NPCPlugin npcModule = NPCPlugin.get(); String[] spawnTypes = this.flockSpawnTypes.get(context); boolean success = true; if (spawnTypes != null) { for (String type : spawnTypes) { if (!npcModule.hasRoleName(type)) { success = false; errors.add(String.format("%s: No such role '%s' for %s", configName, type, this.flockSpawnTypes.getName())); } } } String[] allowedTypes = this.flockAllowedRoles.get(context); if (allowedTypes != null) { for (String typex : allowedTypes) { if (!npcModule.hasRoleName(typex)) { success = false; errors.add(String.format("%s: No such role '%s' for %s", configName, typex, this.flockAllowedRoles.getName())); } } } if (!success) { throw new IllegalStateException( String.format("Invalid roles referenced for %s or %s", this.flockSpawnTypes.getName(), this.flockAllowedRoles.getName()) ); } } }