Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ project produces its result item at the station.
outputs, and the output's result is fixed when the project begins.
- **Experiment-driven discovery** — items carry primary and secondary aspects that
add points to the project, while unneeded aspects are eventually rejected.
- **Random aspect pools** — an output can list an `aspect_pool` and a `pick` count,
so each project draws that many required aspects from the pool when it begins.
- **Hidden recipes** — required aspects sit among decoy rows in a shuffled station
menu, and both their identity and the product are revealed as research progresses.
- **Character focus** — experiments spend RPCharacters focus points, and a configurable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public final class StationSaveData {
private Map<String, String> aspect_states = new HashMap<>();
private List<String> tested_items = new ArrayList<>();
private List<String> aspect_slot_order = new ArrayList<>();
private List<String> pool_aspects = new ArrayList<>();

public String getWorld() {
return world;
Expand Down Expand Up @@ -167,4 +168,12 @@ public List<String> getAspectSlotOrder() {
public void setAspectSlotOrder(List<String> aspectSlotOrder) {
this.aspect_slot_order = aspectSlotOrder != null ? aspectSlotOrder : new ArrayList<>();
}

public List<String> getPoolAspects() {
return pool_aspects;
}

public void setPoolAspects(List<String> poolAspects) {
this.pool_aspects = poolAspects != null ? poolAspects : new ArrayList<>();
}
}
28 changes: 26 additions & 2 deletions src/main/java/net/tfminecraft/research/database/StationStore.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import java.io.IOException;
import java.io.Reader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.UUID;

Expand All @@ -17,6 +18,7 @@
import net.tfminecraft.research.loader.InputLoader;
import net.tfminecraft.research.loader.OutputLoader;
import net.tfminecraft.research.model.ActiveProject;
import net.tfminecraft.research.model.AspectState;
import net.tfminecraft.research.model.OutputDef;
import net.tfminecraft.research.model.ResearchStation;
import net.tfminecraft.research.util.AspectSlotOrder;
Expand Down Expand Up @@ -84,6 +86,7 @@ public static boolean saveStation(ResearchStation station) {
data.setAspectStates(new java.util.HashMap<>(project.getAspectStates()));
data.setTestedItems(new java.util.ArrayList<>(project.getTestedExperimentItems()));
data.setAspectSlotOrder(new java.util.ArrayList<>(project.getAspectSlotOrder()));
data.setPoolAspects(new java.util.ArrayList<>(project.getPoolAspects()));

try {
DurableFiles.write(file.toPath(), GSON.toJson(data));
Expand Down Expand Up @@ -163,11 +166,32 @@ private static ResearchStation loadFile(File file) {
}
OutputDef outputDef = OutputLoader.getById(data.getResolvedOutputId());
String context = "station " + file.getName();
List<String> savedPool = data.getPoolAspects() != null ? data.getPoolAspects() : List.of();
List<String> rejected = new ArrayList<>();
for (String aspectId : outputDef.getAspectPool().keySet()) {
if (project.getAspectState(aspectId) == AspectState.REJECTED) {
rejected.add(aspectId);
}
}
project.setPoolAspects(outputDef.reconcilePoolAspects(savedPool, rejected));
for (String aspectId : project.getPoolAspects()) {
// A new draw the station rejected while it was off-recipe could never be confirmed.
if (!savedPool.contains(aspectId) && rejected.contains(aspectId)) {
project.getAspectStates().remove(aspectId);
project.getAspectPoints().remove(aspectId);
}
}
if (!project.getPoolAspects().equals(savedPool)) {
Research.plugin.getLogger().warning("[Research] Pool aspects for " + context + " changed from "
+ savedPool + " to " + project.getPoolAspects() + " to match output '"
+ outputDef.getId() + "'.");
}
List<String> savedOrder = data.getAspectSlotOrder();
Collection<String> recipe = outputDef.getRecipeAspects(project).keySet();
if (savedOrder == null || savedOrder.isEmpty()) {
project.setAspectSlotOrder(AspectSlotOrder.reconcileRowAssignments(outputDef, null, context));
project.setAspectSlotOrder(AspectSlotOrder.reconcileRowAssignments(recipe, null, context));
} else {
project.setAspectSlotOrder(AspectSlotOrder.reconcileRowAssignments(outputDef, savedOrder, context));
project.setAspectSlotOrder(AspectSlotOrder.reconcileRowAssignments(recipe, savedOrder, context));
}
ResearchStation station = new ResearchStation(location, owner, project);
station.setOwnerCharacterId(data.getOwnerCharacterId());
Expand Down
53 changes: 45 additions & 8 deletions src/main/java/net/tfminecraft/research/loader/OutputLoader.java
Original file line number Diff line number Diff line change
Expand Up @@ -88,28 +88,65 @@ private boolean validate(OutputDef def, File configFile) {
ok &= validateItemRef(def.getId(), "mystery_display", def.getMysteryDisplayItemRef(), configFile);
ok &= validateItemRef(def.getId(), "product_display", def.getProductDisplayItemRef(), configFile);

if (def.getAspects().size() > GridLayout.ASPECT_ROW_COUNT) {
int recipeSize = def.getAspects().size() + def.getAspectPoolPick();
if (recipeSize > GridLayout.ASPECT_ROW_COUNT) {
Research.plugin.getLogger().severe("[Research] Output '" + def.getId() + "' has "
+ def.getAspects().size() + " aspects but the station GUI supports "
+ recipeSize + " aspects but the station GUI supports "
+ GridLayout.ASPECT_ROW_COUNT + " in " + configFile.getName());
ok = false;
}

for (Map.Entry<String, AspectRequirement> entry : def.getAspects().entrySet()) {
ok &= validateAspects(def.getId(), "", def.getAspects(), configFile);
ok &= validateAspectPool(def, configFile);
ok &= validateResult(def, configFile);
return ok;
}

private boolean validateAspects(String outputId, String field, Map<String, AspectRequirement> aspects,
File configFile) {
boolean ok = true;
for (Map.Entry<String, AspectRequirement> entry : aspects.entrySet()) {
String aspectId = entry.getKey();
if (AspectLoader.getById(aspectId) == null) {
Research.plugin.getLogger().severe("[Research] Output '" + def.getId() + "' references unknown aspect '"
+ aspectId + "' in " + configFile.getName());
Research.plugin.getLogger().severe("[Research] Output '" + outputId + "' references unknown "
+ field + "aspect '" + aspectId + "' in " + configFile.getName());
ok = false;
}
if (entry.getValue().getRequiredPoints() <= 0) {
Research.plugin.getLogger().severe("[Research] Output '" + def.getId() + "' aspect '" + aspectId
+ "' must have required_points > 0 in " + configFile.getName());
Research.plugin.getLogger().severe("[Research] Output '" + outputId + "' " + field + "aspect '"
+ aspectId + "' must have required_points > 0 in " + configFile.getName());
ok = false;
}
}
return ok;
}

ok &= validateResult(def, configFile);
private boolean validateAspectPool(OutputDef def, File configFile) {
int pick = def.getAspectPoolPick();
Map<String, AspectRequirement> pool = def.getAspectPool();
if (pool.isEmpty()) {
if (pick != 0) {
Research.plugin.getLogger().severe("[Research] Output '" + def.getId()
+ "' sets aspect_pool.pick but lists no aspect_pool.aspects in " + configFile.getName());
return false;
}
return true;
}

boolean ok = validateAspects(def.getId(), "pool ", pool, configFile);
if (pick < 1 || pick > pool.size()) {
Research.plugin.getLogger().severe("[Research] Output '" + def.getId() + "' aspect_pool.pick must be"
+ " between 1 and " + pool.size() + " (the pool size), got " + pick + " in "
+ configFile.getName());
ok = false;
}
for (String aspectId : pool.keySet()) {
if (def.getAspects().containsKey(aspectId)) {
Research.plugin.getLogger().severe("[Research] Output '" + def.getId() + "' lists aspect '"
+ aspectId + "' in both aspects and aspect_pool in " + configFile.getName());
ok = false;
}
}
return ok;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,10 @@ public void populateMain(Player player, ResearchStation station, Inventory inven
inventory.setItem(GridLayout.SLOT_PRODUCT, productItem);
}

Map<String, AspectRequirement> recipe = output.getRecipeAspects(activeProject);
List<String> rowAssignments = activeProject.getAspectSlotOrder();
if (!activeProject.hasAspectSlotOrder()) {
rowAssignments = AspectSlotOrder.reconcileRowAssignments(output, null,
rowAssignments = AspectSlotOrder.reconcileRowAssignments(recipe.keySet(), null,
"station " + station.getProject().getResolvedOutputId());
activeProject.setAspectSlotOrder(rowAssignments);
}
Expand All @@ -111,7 +112,7 @@ public void populateMain(Player player, ResearchStation station, Inventory inven
continue;
}

AspectRequirement requirement = output.getAspects().get(aspectId);
AspectRequirement requirement = recipe.get(aspectId);
if (requirement == null) {
inventory.setItem(GridLayout.getAspectLockSlot(row), buildAspectRowDivider());
renderInactiveProgressRow(inventory, row);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -457,7 +457,7 @@ private boolean applyExperimentAspect(Player player, ActiveProject active, Outpu
if (active.getAspectState(aspectId) == AspectState.REJECTED) {
return false;
}
AspectRequirement requirement = outputDef.getAspects().get(aspectId);
AspectRequirement requirement = outputDef.getRecipeAspects(active).get(aspectId);
if (requirement != null) {
int required = requirement.getRequiredPoints();
int before = active.getAspectPoints(aspectId);
Expand Down Expand Up @@ -518,7 +518,7 @@ private void reconcileAspectState(Player player, ActiveProject active, OutputDef
active.setAspectState(aspectId, AspectState.TESTING);
}

AspectRequirement requirement = outputDef.getAspects().get(aspectId);
AspectRequirement requirement = outputDef.getRecipeAspects(active).get(aspectId);
int points = active.getAspectPoints(aspectId);
int required = requirement.getRequiredPoints();
if (DiscoveryScaling.meetsConfirmThreshold(player, points, required)) {
Expand Down Expand Up @@ -609,7 +609,7 @@ private void sendResultGrantedMessage(Player player, ItemStack item) {
}

private boolean isRecipeComplete(Player player, OutputDef outputDef, ActiveProject active) {
for (Map.Entry<String, AspectRequirement> entry : outputDef.getAspects().entrySet()) {
for (Map.Entry<String, AspectRequirement> entry : outputDef.getRecipeAspects(active).entrySet()) {
String aspectId = entry.getKey();
if (active.getAspectState(aspectId) != AspectState.CONFIRMED) {
return false;
Expand Down Expand Up @@ -664,7 +664,8 @@ private ResearchStation startProject(Player player, Location location, Equipment
}

ActiveProject project = new ActiveProject(input.getId(), output.getId(), resolvedResultRef);
project.setAspectSlotOrder(AspectSlotOrder.buildRowAssignments(output));
project.setPoolAspects(output.rollPoolAspects());
project.setAspectSlotOrder(AspectSlotOrder.buildRowAssignments(output.getRecipeAspects(project).keySet()));
ResearchStation station = new ResearchStation(blockLocation(location), player.getUniqueId(), project);
station.setOwnerCharacterId(playerManager.getActiveCharacterId(player));
stations.add(station);
Expand Down
21 changes: 21 additions & 0 deletions src/main/java/net/tfminecraft/research/model/ActiveProject.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package net.tfminecraft.research.model;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
Expand All @@ -24,6 +25,7 @@ public final class ActiveProject {
private final Map<String, Integer> aspectTestCounts = new HashMap<>();
private final Set<String> testedExperimentItems = new HashSet<>();
private final List<String> aspectSlotOrder = new ArrayList<>();
private final List<String> poolAspects = new ArrayList<>();

public ActiveProject(String inputId, String resolvedOutputId, String resolvedResultRef) {
this.inputId = inputId;
Expand Down Expand Up @@ -111,6 +113,25 @@ public boolean hasAspectSlotOrder() {
return aspectSlotOrder.size() == GridLayout.ASPECT_ROW_COUNT;
}

/**
* Aspects this project drew from its output's {@code aspect_pool}, fixed when it started.
*/
public List<String> getPoolAspects() {
return Collections.unmodifiableList(poolAspects);
}

public void setPoolAspects(List<String> aspectIds) {
poolAspects.clear();
if (aspectIds == null) {
return;
}
for (String aspectId : aspectIds) {
if (aspectId != null && !aspectId.isBlank() && !poolAspects.contains(aspectId)) {
poolAspects.add(aspectId);
}
}
}

public AspectState getAspectState(String aspectId) {
String raw = aspectStates.get(aspectId);
if (raw == null || raw.isBlank()) {
Expand Down
94 changes: 87 additions & 7 deletions src/main/java/net/tfminecraft/research/model/OutputDef.java
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package net.tfminecraft.research.model;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ThreadLocalRandom;

import org.bukkit.configuration.ConfigurationSection;

Expand All @@ -16,6 +19,8 @@ public final class OutputDef {
private final String productDisplayItemRef;
private final int productRevealAfterConfirmedAspects;
private final Map<String, AspectRequirement> aspects;
private final Map<String, AspectRequirement> aspectPool;
private final int aspectPoolPick;
private final ResultSpec result;

public OutputDef(String id, ConfigurationSection config) {
Expand All @@ -33,13 +38,11 @@ public OutputDef(String id, ConfigurationSection config) {
? reveal.getInt("base_after_confirmed_aspects", 0)
: 0;

this.aspects = new LinkedHashMap<>();
ConfigurationSection aspectsSection = config.getConfigurationSection("aspects");
if (aspectsSection != null) {
for (String aspectId : aspectsSection.getKeys(false)) {
aspects.put(aspectId, new AspectRequirement(aspectsSection.getConfigurationSection(aspectId)));
}
}
this.aspects = readAspects(config.getConfigurationSection("aspects"));

ConfigurationSection pool = config.getConfigurationSection("aspect_pool");
this.aspectPool = readAspects(pool != null ? pool.getConfigurationSection("aspects") : null);
this.aspectPoolPick = pool != null ? pool.getInt("pick", 0) : 0;

ConfigurationSection resultSection = config.getConfigurationSection("result");
this.result = resultSection != null ? new ResultSpec(resultSection) : new ResultSpec(null);
Expand Down Expand Up @@ -69,10 +72,87 @@ public Map<String, AspectRequirement> getAspects() {
return Collections.unmodifiableMap(aspects);
}

public Map<String, AspectRequirement> getAspectPool() {
return Collections.unmodifiableMap(aspectPool);
}

public int getAspectPoolPick() {
return aspectPoolPick;
}

/**
* Draws {@code aspect_pool.pick} distinct aspects from the pool for a new project.
*/
public List<String> rollPoolAspects() {
List<String> ids = new ArrayList<>(aspectPool.keySet());
Collections.shuffle(ids, ThreadLocalRandom.current());
return new ArrayList<>(ids.subList(0, Math.max(0, Math.min(aspectPoolPick, ids.size()))));
}

/**
* Keeps the saved draws still in the pool (up to {@code pick}) and draws again to fill any gap,
* so a station survives pool edits without losing aspects it already researched.
*/
public List<String> reconcilePoolAspects(List<String> saved) {
return reconcilePoolAspects(saved, List.of());
}

/**
* Like {@link #reconcilePoolAspects(List)}, but refills from {@code avoid} only once the rest of the
* pool is used up (a station passes the aspects it already rejected).
*/
public List<String> reconcilePoolAspects(List<String> saved, Collection<String> avoid) {
List<String> kept = new ArrayList<>();
if (saved != null) {
for (String aspectId : saved) {
if (kept.size() < aspectPoolPick && aspectPool.containsKey(aspectId) && !kept.contains(aspectId)) {
kept.add(aspectId);
}
}
}
List<String> remaining = new ArrayList<>(aspectPool.keySet());
remaining.removeAll(kept);
Collections.shuffle(remaining, ThreadLocalRandom.current());
remaining.sort(Comparator.comparing(avoid::contains));
for (String aspectId : remaining) {
if (kept.size() >= aspectPoolPick) {
break;
}
kept.add(aspectId);
}
return kept;
}

/**
* The aspects a project must complete: every fixed aspect, then the pool aspects it drew.
*/
public Map<String, AspectRequirement> getRecipeAspects(ActiveProject project) {
Map<String, AspectRequirement> recipe = new LinkedHashMap<>(aspects);
if (project != null) {
for (String aspectId : project.getPoolAspects()) {
AspectRequirement requirement = aspectPool.get(aspectId);
if (requirement != null) {
recipe.putIfAbsent(aspectId, requirement);
}
}
}
return recipe;
}

public ResultSpec getResult() {
return result;
}

private static Map<String, AspectRequirement> readAspects(ConfigurationSection section) {
Map<String, AspectRequirement> read = new LinkedHashMap<>();
if (section != null) {
for (String aspectId : section.getKeys(false)) {
read.put(aspectId, new AspectRequirement(section.getConfigurationSection(aspectId)));
}
}
return read;
}

public static final class ResultSpec {
private final String itemRef;
private final String templateRef;
Expand Down
Loading
Loading