Merge pull request #41 from TheLouisHong/master

heat/cool prediction: continued important algorithm bug fix for prediction
This commit is contained in:
Patrick Watts
2024-11-20 18:35:54 +04:00
committed by GitHub
9 changed files with 639 additions and 576 deletions

View File

@@ -23,6 +23,8 @@ public class EasyGiantsFoundryClientIDs
// 3 -
protected static final int VARBIT_GAME_STAGE = 13914;
protected static final int VARBIT_PREFORM_STORED = 13947;
protected static final int WIDGET_HEAT_PARENT = 49414153;
protected static final int WIDGET_LOW_HEAT_PARENT = 49414163;
protected static final int WIDGET_MED_HEAT_PARENT = 49414164;

View File

@@ -1,7 +1,6 @@
package com.toofifty.easygiantsfoundry;
import java.awt.Color;
import net.runelite.client.config.Config;
import net.runelite.client.config.ConfigGroup;
import net.runelite.client.config.ConfigItem;
@@ -247,6 +246,18 @@ public interface EasyGiantsFoundryConfig extends Config
return true;
}
@ConfigItem(
keyName = "storageHighlight",
name = "Highlight Preform Storage",
description = "Highlight Storage when it contains a preform.",
position = 10,
section = highlightList
)
default boolean highlightStorage()
{
return true;
}
@ConfigSection(
name = "Info Panel",
description = "Settings for the Info Panel overlay",
@@ -502,6 +513,34 @@ public interface EasyGiantsFoundryConfig extends Config
description = "Advanced Settings",
position = 5
)
String generalSettings = "generalSettings";
String advancedSettings = "generalSettings";
@Range(
max = 50
)
@ConfigItem(
keyName = "heatActionBuffer", // renamed to reset player's settings for previous bugged implementation
name = "Lava/Waterfall Padding Ticks",
description = "Units in ticks; buffers more than optimal heat when in lava/waterfall calculations to compensate for heat decay when the player is afk or running/walking slower than optimal.",
position = 0,
section = advancedSettings
)
default int heatActionPadTicks()
{
return 4;
}
@ConfigItem(
keyName = "debugging",
name = "Show Debugging",
description = "Shows debugging visuals used for development",
position = 0,
section = advancedSettings,
warning = "Only used for development."
)
default boolean debugging()
{
return false;
}
}

View File

@@ -17,18 +17,9 @@ import net.runelite.api.GameState;
import net.runelite.api.InventoryID;
import net.runelite.api.Item;
import net.runelite.api.ItemContainer;
import net.runelite.api.MenuAction;
import net.runelite.api.Skill;
import net.runelite.api.events.GameObjectDespawned;
import net.runelite.api.events.GameObjectSpawned;
import net.runelite.api.events.GameStateChanged;
import net.runelite.api.events.GameTick;
import net.runelite.api.events.ItemContainerChanged;
import net.runelite.api.events.MenuOptionClicked;
import net.runelite.api.events.NpcDespawned;
import net.runelite.api.events.NpcSpawned;
import net.runelite.api.events.ScriptPostFired;
import net.runelite.api.events.StatChanged;
import net.runelite.api.events.VarbitChanged;
import net.runelite.api.events.*;
import net.runelite.api.widgets.Widget;
import net.runelite.client.Notifier;
import net.runelite.client.callback.ClientThread;
@@ -58,6 +49,7 @@ public class EasyGiantsFoundryPlugin extends Plugin
private static final int CRUCIBLE = 44776;
private static final int MOULD_JIG = 44777;
private static final int STORAGE = 44778;
private static final int KOVAC_NPC = 11472;
@@ -156,10 +148,47 @@ public class EasyGiantsFoundryPlugin extends Plugin
case CRUCIBLE:
overlay3d.crucible = gameObject;
break;
case STORAGE:
overlay3d.storage = gameObject;
break;
}
}
@Subscribe
public void onGameObjectDespawned(GameObjectDespawned event)
{
GameObject gameObject = event.getGameObject();
switch (gameObject.getId())
{
case POLISHING_WHEEL:
state.setEnabled(false);
overlay3d.polishingWheel = null;
break;
case GRINDSTONE:
overlay3d.grindstone = null;
break;
case LAVA_POOL:
overlay3d.lavaPool = null;
break;
case WATERFALL:
overlay3d.waterfall = null;
break;
case TRIP_HAMMER:
overlay3d.tripHammer = null;
break;
case MOULD_JIG:
overlay3d.mouldJig = null;
break;
case CRUCIBLE:
overlay3d.crucible = null;
break;
case STORAGE:
overlay3d.storage = null;
break;
}
}
@Subscribe
public void onGameStateChanged(GameStateChanged event)
{
@@ -202,36 +231,6 @@ public class EasyGiantsFoundryPlugin extends Plugin
}
}
@Subscribe
public void onGameObjectDespawned(GameObjectDespawned event)
{
GameObject gameObject = event.getGameObject();
switch (gameObject.getId())
{
case POLISHING_WHEEL:
state.setEnabled(false);
overlay3d.polishingWheel = null;
break;
case GRINDSTONE:
overlay3d.grindstone = null;
break;
case LAVA_POOL:
overlay3d.lavaPool = null;
break;
case WATERFALL:
overlay3d.waterfall = null;
break;
case TRIP_HAMMER:
overlay3d.tripHammer = null;
break;
case MOULD_JIG:
overlay3d.mouldJig = null;
break;
case CRUCIBLE:
overlay3d.crucible = null;
break;
}
}
@Subscribe
public void onNpcSpawned(NpcSpawned event)
@@ -268,9 +267,31 @@ public class EasyGiantsFoundryPlugin extends Plugin
}
}
public void onMenuEntryAdded(MenuEntryAdded event)
{
if (event.getOption().startsWith("Heat-preform") || event.getOption().startsWith("Dunk-preform"))
{
}
else if (event.getOption().startsWith("Cool-preform") || event.getOption().startsWith("Quench-preform")) {
}
}
@Subscribe
public void onMenuOptionClicked(MenuOptionClicked event)
{
clientThread.invokeAtTickEnd(() ->
{
if (!(event.getMenuAction() == MenuAction.GAME_OBJECT_FIRST_OPTION
|| event.getMenuAction() == MenuAction.GAME_OBJECT_SECOND_OPTION
|| event.getMenuAction() == MenuAction.GAME_OBJECT_THIRD_OPTION
|| event.getMenuAction() == MenuAction.GAME_OBJECT_FOURTH_OPTION
|| event.getMenuAction() == MenuAction.GAME_OBJECT_FIFTH_OPTION
|| event.getMenuAction() == MenuAction.WIDGET_TARGET_ON_GAME_OBJECT
|| event.getMenuAction() == MenuAction.WALK))
{
return;
}
if (!state.isEnabled()) return;
if (event.getMenuTarget().contains("Crucible "))
@@ -290,28 +311,30 @@ public class EasyGiantsFoundryPlugin extends Plugin
// start the HeatActionStateMachine when varbit begins to update in onVarbitChanged()
if (event.getMenuOption().startsWith("Heat-preform"))
{
state.heatingCoolingState.stop();
state.heatingCoolingState.setup(7, 0, "heats");
state.heatActionStateMachine.stop();
state.heatActionStateMachine.setup(false, true, "heats");
}
else if (event.getMenuOption().startsWith("Dunk-preform"))
{
state.heatingCoolingState.stop();
state.heatingCoolingState.setup(27, 2, "dunks");
state.heatActionStateMachine.stop();
state.heatActionStateMachine.setup(true, true, "dunks");
}
else if (event.getMenuOption().startsWith("Cool-preform"))
{
state.heatingCoolingState.stop();
state.heatingCoolingState.setup(-7, 0, "cools");
state.heatActionStateMachine.stop();
state.heatActionStateMachine.setup(false, false, "cools");
}
else if (event.getMenuOption().startsWith("Quench-preform"))
{
state.heatingCoolingState.stop();
state.heatingCoolingState.setup(-27, -2, "quenches");
state.heatActionStateMachine.stop();
state.heatActionStateMachine.setup(true, false, "quenches");
}
else // canceled heating/cooling, stop the heating state-machine
else if (!state.heatActionStateMachine.isIdle()) // canceled heating/cooling, stop the heating state-machine
{
state.heatingCoolingState.stop();
state.heatActionStateMachine.stop();
}
});
}
@Subscribe
@@ -333,8 +356,8 @@ public class EasyGiantsFoundryPlugin extends Plugin
// show mould score on Mould UI Title
Widget mouldParent = client.getWidget(47054850);
Integer mouldScore = state.getMouldScore();
if (mouldParent != null && mouldScore != null)
int mouldScore = state.getMouldScore();
if (mouldParent != null && mouldScore >= 0)
{
Widget title = Objects.requireNonNull(mouldParent.getChild(1));
@@ -362,24 +385,41 @@ public class EasyGiantsFoundryPlugin extends Plugin
state.setMouldScore(-1);
}
// start the heating state-machine when the varbit updates
// if heat varbit updated and the user clicked, start the state-machine
if (event.getVarbitId() == VARBIT_HEAT && state.heatingCoolingState.getActionName() != null)
if (event.getVarbitId() == VARBIT_HEAT)
{
// ignore passive heat decay, one heat per two ticks
if (event.getValue() - previousHeat != -1)
int delta = event.getValue() - previousHeat;
// sign check: num * num > 0 == same sign
if (delta != -1)
{
if (state.heatActionStateMachine.getActionname() != null)
{
// if the state-machine is idle, start it
if (state.heatingCoolingState.isIdle())
if (state.heatActionStateMachine.isIdle())
{
state.heatingCoolingState.start(state, config, state.getHeatAmount());
state.heatActionStateMachine.start(state, config, previousHeat);
}
state.heatActionStateMachine.onTick();
}
state.heatingCoolingState.onTick();
if (config.debugging())
{
client.addChatMessage(ChatMessageType.GAMEMESSAGE, "",
"Heat: <col=FF0000>" + event.getValue() + "</col>" +
"Delta: <col=00FFFF>" + delta + "</col> " +
"Heating Ticks: <col=00FFFF>" + state.heatActionStateMachine.heatingTicks + "</col>" +
" Cooling Ticks: <col=00FFFF>" + state.heatActionStateMachine.coolingTicks + "</col>" +
" Remaining Ticks: <col=00FFFF>" + state.heatActionStateMachine.getRemainingDuration(), "");
}
}
// client.addChatMessage(ChatMessageType.GAMEMESSAGE, "", "Delta: <col=00FFFF>" + delta + "</col> ", "");
previousHeat = event.getValue();
}
}
@Subscribe
protected void onConfigChanged(ConfigChanged configChanged)

View File

@@ -44,7 +44,7 @@ public class EasyGiantsFoundryState
@Setter
@Getter
private int lastKnownCrucibleScore = -1; // will be set when "Pour"ed
private int lastKnownCrucibleScore = -1; // will be set when "Pour"ed (because the crucible will be empty then)
private final List<Stage> stages = new ArrayList<>();
private double heatRangeRatio = 0;
@@ -153,19 +153,19 @@ public class EasyGiantsFoundryState
int heat = getHeatAmount();
int[] low = getLowHeatRange();
if (heat > low[0] && heat < low[1])
if (heat >= low[0] && heat <= low[1])
{
return Heat.LOW;
}
int[] med = getMedHeatRange();
if (heat > med[0] && heat < med[1])
if (heat >= med[0] && heat <= med[1])
{
return Heat.MED;
}
int[] high = getHighHeatRange();
if (heat > high[0] && heat < high[1])
if (heat >= high[0] && heat <= high[1])
{
return Heat.HIGH;
}
@@ -326,7 +326,7 @@ public class EasyGiantsFoundryState
int[] range = getCurrentHeatRange();
int actions = 0;
int heat = getHeatAmount();
while (heat > range[0] && heat < range[1])
while (heat >= range[0] && heat <= range[1])
{
actions++;
heat += stage.getHeatChange();
@@ -335,5 +335,5 @@ public class EasyGiantsFoundryState
return actions;
}
public HeatActionStateMachine heatingCoolingState = new HeatActionStateMachine();
public HeatActionStateMachine heatActionStateMachine = new HeatActionStateMachine();
}

View File

@@ -1,7 +1,6 @@
package com.toofifty.easygiantsfoundry;
import static com.toofifty.easygiantsfoundry.EasyGiantsFoundryClientIDs.VARBIT_GAME_STAGE;
import static com.toofifty.easygiantsfoundry.EasyGiantsFoundryClientIDs.WIDGET_PROGRESS_PARENT;
import static com.toofifty.easygiantsfoundry.EasyGiantsFoundryClientIDs.*;
import static com.toofifty.easygiantsfoundry.EasyGiantsFoundryHelper.getHeatColor;
import static com.toofifty.easygiantsfoundry.MouldHelper.SWORD_TYPE_1_VARBIT;
import static com.toofifty.easygiantsfoundry.MouldHelper.SWORD_TYPE_2_VARBIT;
@@ -17,6 +16,7 @@ import javax.inject.Inject;
import net.runelite.api.Client;
import net.runelite.api.GameObject;
import net.runelite.api.MenuEntry;
import net.runelite.api.NPC;
import net.runelite.api.Perspective;
import net.runelite.api.Point;
@@ -32,6 +32,7 @@ public class FoundryOverlay3D extends Overlay
{
private static final int HAND_IN_WIDGET = 49414221;
private static final int CRUCIBLE_CAPACITY = 28;
private final ModelOutlineRenderer modelOutlineRenderer;
GameObject tripHammer;
@@ -41,6 +42,7 @@ public class FoundryOverlay3D extends Overlay
GameObject waterfall;
GameObject mouldJig;
GameObject crucible;
GameObject storage;
NPC kovac;
private final Client client;
@@ -110,6 +112,15 @@ public class FoundryOverlay3D extends Overlay
drawKovacIfHandIn(graphics);
}
if (client.getVarbitValue(VARBIT_PREFORM_STORED) == 1)
{
if (config.highlightStorage())
{
drawStorage(graphics);
}
return null;
}
if (state.getCurrentStage() == null)
{
if (config.highlightMould())
@@ -126,13 +137,12 @@ public class FoundryOverlay3D extends Overlay
drawPreformScoreIfPoured(graphics);
}
return null;
}
Stage stage = state.getCurrentStage();
GameObject stageObject = getStageObject(stage);
if (stageObject == null)
if (stageObject == null || graphics == null)
{
return null;
}
@@ -151,20 +161,30 @@ public class FoundryOverlay3D extends Overlay
drawObjectOutline(graphics, stageObject, color);
}
if ((stage.getHeat() != heat || !state.heatingCoolingState.isIdle()) && config.highlightWaterAndLava())
// !state.heatingCoolingState.isIdle()
// if the stage heat is already in range, but player still wants to do heat changes
if ((stage.getHeat() != heat || !state.heatActionStateMachine.isIdle()) && config.highlightWaterAndLava())
{
drawHeatChangers(graphics);
}
if (state.heatingCoolingState.isCooling())
// mouse hover over preview
else if (config.drawLavaWaterInfoOverlay())
{
drawHeatingCoolingOverlay(graphics, waterfall);
}
if (state.heatingCoolingState.isHeating())
MenuEntry[] menuEntries = client.getMenuEntries();
if (menuEntries.length != 0)
{
drawHeatingCoolingOverlay(graphics, lavaPool);
}
MenuEntry hoveredMenu = menuEntries[menuEntries.length - 1];
if (hoveredMenu.getIdentifier() == lavaPool.getId())
{
drawHeatChangerPreviewOverlay(graphics, lavaPool, true);
}
else if (hoveredMenu.getIdentifier() == waterfall.getId())
{
drawHeatChangerPreviewOverlay(graphics, waterfall, false);
}
}
}
return null;
}
@@ -195,26 +215,87 @@ public class FoundryOverlay3D extends Overlay
modelOutlineRenderer.drawOutline(stageObject, config.borderThickness(), _color, config.borderFeather());
}
private void drawHeatingCoolingOverlay(
private void drawHeatChangerPreviewOverlay(
Graphics2D graphics,
GameObject stageObject
GameObject stageObject,
boolean isLava
)
{
if (!config.drawLavaWaterInfoOverlay())
{
return;
}
if (state.heatingCoolingState.isIdle())
{
return;
}
int sign = isLava ? 1 : -1;
int fastVelocity = 27 * sign;
int slowVelocity = 7 * sign;
int fastAccelBonus = 2 * sign;
int slowAccelBonus = 0;
HeatActionSolver.DurationResult fastResult =
HeatActionSolver.solve(
state.getCurrentStage(),
state.getCurrentHeatRange(),
state.getActionsLeftInStage(),
state.getHeatAmount(),
true,
isLava,
config.heatActionPadTicks() * 2
);
final int fastDuration = fastResult.getDuration();
HeatActionSolver.DurationResult slowResult =
HeatActionSolver.solve(
state.getCurrentStage(),
state.getCurrentHeatRange(),
state.getActionsLeftInStage(),
state.getHeatAmount(),
false,
isLava,
config.heatActionPadTicks() * 2
);
final int slowDuration = slowResult.getDuration();
final String fastName = isLava ? "dunks" : "quenches";
final String slowName = isLava ? "heats" : "cools";
String text;
text = String.format("%d %s",
state.heatingCoolingState.getRemainingDuration(),
state.heatingCoolingState.getActionName()
if (config.debugging())
{
text = String.format("%d %s (predicted: %d) or %d %s (predicted: %d) (overshoot: %s goal-in-range: %s)",
fastDuration, fastName, fastResult.getPredictedHeat(), slowDuration, slowName, slowResult.getPredictedHeat(), slowResult.isOvershooting(), fastResult.isGoalInRange());
}
else
{
text = String.format("%d %s or %d %s ",
fastDuration, fastName, slowDuration, slowName);
}
LocalPoint stageLoc = stageObject.getLocalLocation();
stageLoc = new LocalPoint(stageLoc.getX(), stageLoc.getY());
Point pos = Perspective.getCanvasTextLocation(client, graphics, stageLoc, text, 50);
Color color = config.lavaWaterfallColour();
OverlayUtil.renderTextLocation(graphics, pos, text, color);
}
private void drawHeatChangerOverlay(Graphics2D graphics, GameObject stageObject)
{
String text;
if (config.debugging())
{
text = String.format("%d %s (overshoot: %s) [goal-in-range: %s]",
state.heatActionStateMachine.getRemainingDuration(),
state.heatActionStateMachine.getActionname(),
state.heatActionStateMachine.isOverShooting(),
state.heatActionStateMachine.isGoalInRange()
);
}
else
{
text = String.format("%d %s",
state.heatActionStateMachine.getRemainingDuration(),
state.heatActionStateMachine.getActionname()
);
}
LocalPoint stageLoc = stageObject.getLocalLocation();
stageLoc = new LocalPoint(stageLoc.getX(), stageLoc.getY());
@@ -230,11 +311,13 @@ public class FoundryOverlay3D extends Overlay
int change = state.getHeatChangeNeeded();
Shape shape = null;
if (change < 0 || state.heatingCoolingState.isCooling())
boolean isLava = change > 0;
boolean isWaterfall = change < 0;
if (isWaterfall || state.heatActionStateMachine.isCooling())
{
shape = waterfall.getClickbox();
}
else if (change > 0 || state.heatingCoolingState.isHeating())
else if (isLava || state.heatActionStateMachine.isHeating())
{
shape = lavaPool.getClickbox();
}
@@ -254,9 +337,28 @@ public class FoundryOverlay3D extends Overlay
graphics.setColor(new Color(color.getRed(), color.getGreen(), color.getBlue(), 20));
graphics.fill(shape);
}
if (config.drawLavaWaterInfoOverlay())
{
if (state.heatActionStateMachine.isCooling())
{
drawHeatChangerOverlay(graphics, waterfall);
}
else if (isWaterfall)
{
drawHeatChangerPreviewOverlay(graphics, waterfall, false);
}
if (state.heatActionStateMachine.isHeating())
{
drawHeatChangerOverlay(graphics, lavaPool);
}
else if (isLava)
{
drawHeatChangerPreviewOverlay(graphics, lavaPool, true);
}
}
}
static final int CRUCIBLE_CAPACITY = 28;
private void drawCrucibleContent(Graphics2D graphics)
{
@@ -411,6 +513,19 @@ public class FoundryOverlay3D extends Overlay
}
}
private void drawStorage(Graphics2D graphics)
{
Shape shape = storage.getConvexHull();
if (shape != null)
{
Color color = config.generalHighlight();
graphics.setColor(color);
graphics.draw(shape);
graphics.setColor(new Color(color.getRed(), color.getGreen(), color.getBlue(), 20));
graphics.fill(shape);
}
}
private void drawKovacIfHandIn(Graphics2D graphics)
{
Widget handInWidget = client.getWidget(HAND_IN_WIDGET);
@@ -430,6 +545,8 @@ public class FoundryOverlay3D extends Overlay
private void drawActionOverlay(Graphics2D graphics, GameObject gameObject)
{
int actionsLeft = state.getActionsLeftInStage();
int heatLeft = state.getActionsForHeatLevel();
@@ -440,6 +557,10 @@ public class FoundryOverlay3D extends Overlay
LocalPoint textLocation = gameObject.getLocalLocation();
textLocation = new LocalPoint(textLocation.getX(), textLocation.getY());
Point canvasLocation = Perspective.getCanvasTextLocation(client, graphics, textLocation, text, 250);
if (canvasLocation == null)
{
return;
}
OverlayUtil.renderTextLocation(graphics, canvasLocation, text, getHeatColor(actionsLeft, heatLeft));
}
if (config.drawActionLeftOverlay())
@@ -449,6 +570,10 @@ public class FoundryOverlay3D extends Overlay
LocalPoint textLocation = gameObject.getLocalLocation();
textLocation = new LocalPoint(textLocation.getX(), textLocation.getY());
Point canvasLocation = Perspective.getCanvasTextLocation(client, graphics, textLocation, text, 250);
if (canvasLocation == null)
{
return;
}
canvasLocation = new Point(canvasLocation.getX(), canvasLocation.getY() + 10);
OverlayUtil.renderTextLocation(graphics, canvasLocation, text, getHeatColor(actionsLeft, heatLeft));
}

View File

@@ -3,13 +3,16 @@ package com.toofifty.easygiantsfoundry;
//import java.util.ArrayList;
//import java.util.List;
import com.toofifty.easygiantsfoundry.enums.Stage;
import lombok.Value;
/**
* Solves the heating/cooling action and predicts tick duration (index)
* the naming convention is focused on the algorithm rather than in-game terminology for the context.
* <p>
* the dx_n refers to successive derivatives of an ordinary-differential-equations
* https://en.wikipedia.org/wiki/Ordinary_differential_equation
* also known as distance (dx0), speed (dx1), and acceleration (dx2).
* also known as position (dx0), speed (dx1), and acceleration (dx2).
* <p>
* dx0 - players current heat at tick
* dx1 - dx0_current - dx0_last_tick, aka the first derivative
@@ -18,7 +21,17 @@ package com.toofifty.easygiantsfoundry;
* for context, here's what dx1 extracted directly from in-game dunking looks like.
* the purpose of the HeatActionSolver.java is to accurately model this data.
* int[] dx1 = {
* 27,
* 7,
* 8,
* 9,
* 11,
* 13,
* 15,
* 17,
* 19,
* 21,
* 24,
* 27, -- dunk/quench starts here
* 30,
* 33,
* 37,
@@ -77,148 +90,255 @@ subscribe(VarbitChanged.class, ev ->
public class HeatActionSolver
{
/**
* <b>Warning:</b> this method prefers overshooting goal. For example, if goal is 957,
* it will return index that reaches >957.<br>
* This may be desirable if we're aiming to heat over range minimum,
* but undesirable when cooling below range maximum; make sure to -1 the index if so.
*
*
*
* @param goal the desired heat destination
* @param init_dx1 initial speed of heating/cooling. currently 7 for heat/cool, 27 for dunk/quench.
* @param dx2_offset bonus acceleration. currently, 0 for heat/cool, 2 for dunk/quench.
* @return Index here refers to tick. So an index of 10 means the goal can be reached in 10 ticks.
*/
public static int findDx0Index(int goal, int init_dx1, int dx2_offset)
public static final int[] DX_1 = new int[]{
7,
8,
9,
11,
13,
15,
17,
19,
21,
24,
27, // -- dunk/quench starts here
30,
33,
37,
41,
45,
49,
53,
57,
62,
67,
72,
77,
83,
89,
95,
91, // last one will always overshoot 1000
};
public static final int MAX_INDEX = DX_1.length;
public static final int FAST_INDEX = 10;
@Value(staticConstructor = "of")
public static class SolveResult
{
int index;
int dx0;
int dx1;
int dx2;
}
private static SolveResult heatingSolve(int start, int goal, boolean overshoot, int max, boolean isFast)
{
return relativeSolve(goal - start, overshoot, max - start, isFast, -1);
}
private static SolveResult coolingSolve(int start, int goal, boolean overshoot, int min, boolean isFast)
{
return relativeSolve(start - goal, overshoot, start - min, isFast, 1);
}
private static SolveResult relativeSolve(int goal, boolean overshoot, int max, boolean isFast, int decayValue)
{
int index = isFast ? FAST_INDEX : 0;
int dx0 = 0;
int dx1 = init_dx1;
int count_index = 0;
for (int dx2 = 1; dx0 <= goal; dx2++)
{ // Start from 1 up to the count inclusive
int repetitions;
if (dx2 == 1)
boolean decay = false;
while (true) {
if (index > MAX_INDEX)
{
repetitions = 2; // The first number appears twice
break;
}
else if (dx2 % 2 == 0)
if (!overshoot && dx0 + DX_1[index] > goal)
{
repetitions = 6; // Even numbers appear six times
break;
}
else
else if (overshoot && dx0 >= goal)
{
repetitions = 4; // Odd numbers (after 1) appear four times
break;
}
for (int j = 0; j < repetitions && dx0 <= goal; j++)
if (dx0 + DX_1[index] >= max)
{
dx0 += dx1;
dx1 += dx2 + dx2_offset; // Sum the current number 'repetitions' times
count_index += 1;
}
}
return count_index;
break;
}
/**
* We can use the pattern to get the dx2 at a specific index numerically
*
* @param index the index/tick we want to calculate dx2 at
* @return the acceleration of heating/cooling at index/tick
*/
public static int getDx2AtIndex(int index)
if (decay)
{
if (index <= 1) return 1;
dx0 -= decayValue;
}
index -= 2;
// 0 1 2 3 4 5 6 7 8 9
// e,e,e,e,e,e,o,o,o,o
int block = index / 10;
int block_idx = index % 10;
int number = block * 2;
if (block_idx <= 5)
dx0 += DX_1[index];
++index;
decay = !decay;
}
if (isFast)
{
return number + 2;
index -= FAST_INDEX;
}
else
return SolveResult.of(index, dx0, DX_1[index], -1);
}
@Value(staticConstructor = "of")
public static class DurationResult
{
return number + 3;
}
int duration;
boolean goalInRange;
boolean overshooting;
int predictedHeat;
}
/**
* We can use the pattern to get the dx1 at a specific index numerically
*
* @param index the index/tick we want to calculate the speed of heating/cooling
* @param constant the initial speed of heating/cooling.
* @return the speed of heating at index/tick
*/
public static int getDx1AtIndex(int index, int constant)
public static DurationResult solve(
Stage stage,
int[] range,
int actionLeftInStage,
int start,
boolean isFast,
boolean isActionHeating,
int padding)
{
int _dx1 = constant;
for (int i = 0; i < index; ++i)
final boolean isStageHeating = stage.isHeating();
// adding 2.4s/8ticks worth of padding so preform doesn't decay out of range
// average distance from lava+waterfall around 8 ticks
// preform decays 1 heat every 2 ticks
final int min = Math.max(0, Math.min(1000, range[0] + padding));
final int max = Math.max(0, Math.min(1000, range[1] + padding));
final int actionsLeft_DeltaHeat = actionLeftInStage * stage.getHeatChange();
int estimatedDuration = 0;
final boolean goalInRange;
boolean overshoot = false;
SolveResult result = null;
// case actions are all cooling, heating is mirrored version
// goal: in-range // stage: heating
// overshoot goal
// <----------|stop|<---------------- heat
// ------|min|----------goal-------|max|
// stage ---------------->
// goal: out-range // stage: heating
// undershoot min
// ...----------|stop|<--------------------- heat
// -goal---|min|---------------------|max|
// stage ----------------->
// goal: in-range // stage: cooling
// undershoot goal
// <-------------------------|stop|<--------------- heat
// ------|min|----------goal-------|max|
// <---------------- stage
// goal: out-range // stage: cooling
// overshoot max
// <--------------------|stop|<--------------- heat
// --------|min|---------------------|max|----goal
// <---------------- stage
if (isActionHeating)
{
_dx1 += getDx2AtIndex(i);
int goal = min - actionsLeft_DeltaHeat;
goalInRange = goal >= min && goal <= max;
if (isStageHeating)
{
if (start <= max)
{
overshoot = !goalInRange;
if (!goalInRange)
{
goal = min;
}
return _dx1;
result = heatingSolve(start, goal, overshoot, max, isFast);
estimatedDuration = result.index;
}
}
else // cooling stage
{
// actionsLeft_DeltaHeat is negative here
if (start <= max)
{
overshoot = goalInRange;
if (!goalInRange)
{
goal = max;
}
// Methods below are functional, but only used to for debugging & development
result = heatingSolve(start, goal, overshoot, max, isFast);
// public static int getDx0AtIndex(int index, int constant)
// {
// int dx0 = 0;
// int dx1 = getDx1AtIndex(0, constant);
// for (int i = 0; i < index; i++)
// { // Start from 1 up to the count inclusive
// int dx2 = getDx2AtIndex(i);
// dx1 += dx2; // Sum the current number 'repetitions' times
// dx0 += dx1;
// }
// return dx0;
// }
estimatedDuration = result.index;
}
}
}
else // cooling action
{
int goal = max - actionsLeft_DeltaHeat;
goalInRange = goal >= min && goal <= max;
// We iteratively generate dx2 into a list
// public static List<Integer> generateDx2List(int count)
// {
// List<Integer> pattern = new ArrayList<>(); // This will hold our pattern
// for (int n = 1, i = 0; i < count; n++)
// { // Start from 1 up to the count inclusive
// int repetitions;
// if (n == 1)
// {
// repetitions = 2; // The first number appears twice
// } else if (n % 2 == 0)
// {
// repetitions = 6; // Even numbers appear six times
// } else
// {
// repetitions = 4; // Odd numbers (after 1) appear four times
// }
// for (int j = 0; j < repetitions && i < count; j++, i++)
// {
// pattern.add(n); // Append the current number 'repetitions' times
// }
// }
// return pattern;
// }
if (isStageHeating)
{
if (start >= min)
{
overshoot = goalInRange;
if (!goalInRange)
{
goal = min;
}
result = coolingSolve(start, goal, overshoot, min, isFast);
estimatedDuration = result.index;
}
}
else // cooling stage cooling action
{
if (start >= min)
{
overshoot = !goalInRange;
if (!goalInRange)
{
goal = max;
}
result = coolingSolve(start, goal, overshoot, min, isFast);
estimatedDuration = result.index;
}
}
}
int dx0 = result == null ? 0 : result.dx0;
if (!isActionHeating)
{
dx0 *= -1;
}
return DurationResult.of(estimatedDuration, goalInRange, overshoot, start + dx0);
}
// public static int findDx0IndexContinue(int goal, int constant, int init_index)
// {
// int dx0 = getDx0AtIndex(init_index, constant);
// int dx1 = getDx1AtIndex(init_index, constant);
// int count_index = init_index;
// for (; dx0 <= goal; count_index++)
// { // Start from 1 up to the count inclusive
// int dx2 = getDx2AtIndex(count_index);
// dx1 += dx2; // Sum the current number 'repetitions' times
// dx0 += dx1;
// }
// return count_index - init_index;
// }
}

View File

@@ -1,6 +1,5 @@
package com.toofifty.easygiantsfoundry;
import com.toofifty.easygiantsfoundry.enums.Stage;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
@@ -14,46 +13,45 @@ public class HeatActionStateMachine
/**
* Tick counter for heating, -1 means not currently heating.
*/
int HeatingTicks = -1;
int heatingTicks = -1;
/**
* Tick counter for cooling, -1 means not currently cooling.
*/
int CoolingTicks = -1;
int coolingTicks = -1;
/**
* The velocity of the heating/cooling action.
*/
int Velocity;
boolean actionFast;
/**
* The acceleration bonus of the heating/cooling action.
*/
int AccelerationBonus;
boolean actionHeating;
/**
* The starting heat amount of the heating/cooling action.
*/
int StartingHeat;
int startingHeat;
/**
* The estimated tick duration of the heating/cooling action.
*/
int EstimatedDuration;
int estimatedDuration;
/**
* The goal heat amount of the heating/cooling action.
*/
int GoalHeat = 0;
int goalHeat = 0;
// debug
boolean goalInRange;
boolean isOverShooting;
int predictedHeat;
/**
* The last action the player clicked on. Used for ui overlay to display.
* When null, the state-machine will stop() and reset.
*/
String ActionName = null;
String actionname = null;
private EasyGiantsFoundryState State;
private EasyGiantsFoundryConfig Config;
private EasyGiantsFoundryState state;
private EasyGiantsFoundryConfig config;
/**
* Start the state-machine with the given parameters.
@@ -64,26 +62,27 @@ public class HeatActionStateMachine
* @param state the current state of the foundry
* @param config the current configuration of the plugin
* @param startingHeat the starting heat amount
* @see HeatActionStateMachine#setup(int, int, String)
* @see HeatActionStateMachine#setup(boolean, boolean, String)
*/
public void start(EasyGiantsFoundryState state, EasyGiantsFoundryConfig config, int startingHeat)
{
// use Velocity to determine if heating or cooling
if (Velocity > 0)
if (actionHeating)
{
HeatingTicks = 0;
CoolingTicks = -1;
heatingTicks = 0;
coolingTicks = -1;
}
else
{
CoolingTicks = 0;
HeatingTicks = -1;
heatingTicks = -1;
coolingTicks = 0;
}
StartingHeat = startingHeat - Velocity;
State = state;
Config = config;
calculateEstimates();
this.startingHeat = startingHeat;
this.state = state;
this.config = config;
updateEstimates();
}
/**
@@ -95,11 +94,11 @@ public class HeatActionStateMachine
{
if (isHeating())
{
return Math.max(0, EstimatedDuration - HeatingTicks);
return Math.max(0, (estimatedDuration - heatingTicks));
}
else if (isCooling())
{
return Math.max(0, EstimatedDuration - CoolingTicks);
return Math.max(0, (estimatedDuration - coolingTicks));
}
else
{
@@ -111,125 +110,38 @@ public class HeatActionStateMachine
* Core logic. Runs once on {@link HeatActionStateMachine#start} and assumes synchronization with the game.
* Calculate the estimated duration and goal heat amount of the heating/cooling action.
*/
public void calculateEstimates()
public void updateEstimates()
{
// 0: left/min 1: right/max
int[] range = State.getCurrentHeatRange();
int stageMin = range[0];
int stageMax = range[1];
Stage stage = State.getCurrentStage();
int actionsLeft = State.getActionsLeftInStage();
int actionsLeft_DeltaHeat = (actionsLeft+1) * stage.getHeatChange();
if (isHeating())
{
if (stage.isHeating())
{
GoalHeat = Math.max(stageMin, stageMax - actionsLeft_DeltaHeat);
if (StartingHeat < GoalHeat)
{
int duration = HeatActionSolver.findDx0Index(
GoalHeat - StartingHeat,
Velocity, AccelerationBonus
HeatActionSolver.DurationResult result =
HeatActionSolver.solve(
getState().getCurrentStage(),
getState().getCurrentHeatRange(),
getState().getActionsLeftInStage(),
getStartingHeat(),
actionFast,
isHeating(),
config.heatActionPadTicks() * 2
);
GoalHeat += duration / 2;
goalInRange = result.isGoalInRange();
isOverShooting = result.isOvershooting();
EstimatedDuration = HeatActionSolver.findDx0Index(
GoalHeat - StartingHeat,
Velocity, AccelerationBonus
);
}
else // overheating
{
EstimatedDuration = 0;
}
}
else // is cooling
{
// actionsLeft_DeltaHeat is negative here
GoalHeat = Math.min(stageMax, stageMin - actionsLeft_DeltaHeat);
if (StartingHeat < GoalHeat)
{
int duration = HeatActionSolver.findDx0Index(
GoalHeat - StartingHeat,
Velocity, AccelerationBonus
) - 1;
predictedHeat = result.getPredictedHeat();
GoalHeat -= duration / 2;
EstimatedDuration = HeatActionSolver.findDx0Index(
GoalHeat - StartingHeat,
Velocity, AccelerationBonus
) - 1;
}
else // cold enough
{
EstimatedDuration = 0;
}
}
}
else if (isCooling())
{
if (stage.isHeating()) {
GoalHeat = Math.max(stageMin, stageMax - actionsLeft_DeltaHeat);
if (StartingHeat > GoalHeat)
{
int duration = HeatActionSolver.findDx0Index(
StartingHeat - GoalHeat,
Math.abs(Velocity), Math.abs(AccelerationBonus)
) - 1;
GoalHeat += duration / 2;
EstimatedDuration = HeatActionSolver.findDx0Index(
(StartingHeat - GoalHeat),
Math.abs(Velocity), Math.abs(AccelerationBonus)
) - 1;
}
else
{
EstimatedDuration = 0;
}
}
// Heating Stage
else {
GoalHeat = Math.max(stageMax, stageMin + actionsLeft_DeltaHeat);
if (StartingHeat > GoalHeat) // too hot
{
int duration = HeatActionSolver.findDx0Index(
StartingHeat - GoalHeat,
Math.abs(Velocity), Math.abs(AccelerationBonus)
);
GoalHeat -= duration / 2;
EstimatedDuration = HeatActionSolver.findDx0Index(
StartingHeat - GoalHeat,
Math.abs(Velocity), Math.abs(AccelerationBonus)
);
}
else // hot enough
{
EstimatedDuration = 0;
}
}
}
estimatedDuration = result.getDuration();
}
/**
* Helper to remind the neccessary parameters to start the state-machine.
*
* @param velocity the velocity of the heating/cooling action, 7 for slow, 27 for fast.
* @param accelerationBonus the acceleration bonus of the heating/cooling action. Usually 0 for slow, 2 for fast.
* @param actionName the name of the action to display in the ui overlay
*/
public void setup(int velocity, int accelerationBonus, String actionName)
public void setup(boolean isFast, boolean isHeating, String actionName)
{
Velocity = velocity;
AccelerationBonus = accelerationBonus;
ActionName = actionName;
actionFast = isFast;
actionHeating = isHeating;
actionname = actionName;
}
/**
@@ -237,9 +149,9 @@ public class HeatActionStateMachine
*/
public void stop()
{
HeatingTicks = -1;
CoolingTicks = -1;
ActionName = null;
heatingTicks = -1;
coolingTicks = -1;
actionname = null;
}
/**
@@ -249,7 +161,7 @@ public class HeatActionStateMachine
*/
public boolean isHeating()
{
return HeatingTicks >= 0;
return heatingTicks >= 0;
}
/**
@@ -259,7 +171,7 @@ public class HeatActionStateMachine
*/
public boolean isCooling()
{
return CoolingTicks >= 0;
return coolingTicks >= 0;
}
/**
@@ -277,25 +189,30 @@ public class HeatActionStateMachine
*/
public void onTick()
{
if (isIdle()) return;
if (isHeating())
{
HeatingTicks++;
if (HeatingTicks >= EstimatedDuration)
if (heatingTicks >= estimatedDuration)
{
stop();
}
}
if (isCooling())
else
{
CoolingTicks++;
if (CoolingTicks >= EstimatedDuration)
heatingTicks++;
}
}
else if (isCooling())
{
if (coolingTicks >= estimatedDuration)
{
stop();
}
else
{
coolingTicks++;
}
}
// log.info("\nReal Heat: " + State.getHeatAmount()
// + "\nGoal Heat - StartingHeat: " + (GoalHeat - StartingHeat)
// + "\nDuration: " + EstimatedDuration);
}
}

View File

@@ -114,7 +114,7 @@ public class MouldHelper
int height = scrollList.getHeight();
int scrollMax = scrollList.getScrollHeight();
Widget finalBestWidget = bestWidget;
clientThread.invokeLater(() ->
clientThread.invokeAtTickEnd(() ->
{
if (finalBestWidget != null)
{

View File

@@ -1,180 +0,0 @@
package com.toofifty.easygiantsfoundry;
import static org.junit.Assert.*;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
// playground to test HeatSolver
public class HeatSolverTest
{
// @Test
// public void TestHeatSolver_dx2_Iterative()
// {
// final int[] answer =
// {1, 1, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 6, 6};
// List<Integer> produced = HeatActionSolver.generateDx2List(answer.length);
//
// System.err.println("Expected Length: " + answer.length + " Length: " + produced.size());
// // print produced
// for (Integer integer : produced)
// {
// System.err.print(integer + ",");
// }
// System.err.println();
// // compare
// for (int i = 0; i < answer.length; i++)
// {
// assertEquals("Asserting dx2 n=" + i, answer[i], produced.get(i).intValue());
// }
// }
@Test
public void TestHeatSolver_dx2_Numerical()
{
final int[] answer =
{1, 1, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 6, 6};
// test getDx2AtIndex
for (int i = 0; i < answer.length; i++)
{
assertEquals("Asserting dx2 n=" + i, answer[i], HeatActionSolver.getDx2AtIndex(i));
}
}
@Test
public void TestHeatSolver_dx1()
{
final int c = 7;
final int[] answer =
// {1,1,2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6};
{7, 8, 9, 11, 13, 15, 17, 19, 21, 24, 27, 30, 33, 37, 41, 45, 49, 53, 57, 62, 67, 72, 77, 83, 89};
for (int i = 0; i < answer.length; i++)
{
assertEquals("Asserting dx1 n=" + i + " c=" + c + " answer=" + answer[i], answer[i], HeatActionSolver.getDx1AtIndex(i, c));
}
}
@Test
public void TestHeatSolver_dx2_from_groundtruth()
{
// runelite-shell script for retrieving heating/cooling delta.
// copy-paste into developer-tools -> Shell
// ground-truth answer from game
final int[] answer =
{7, 8, 9, 11, 13, 15, 17, 19, 21, 24, 27, 30, 33, 37, 41, 45, 49, 53, 57, 62, 67, 72, 77, 83, 89};
for (int i = 0; i < answer.length - 1; i++)
{
System.err.print(answer[i + 1] - answer[i] + ",");
}
}
@Test
public void TestHeatSolver_Dx0()
{
final int[] answer_dx1 =
{7, 8, 9, 11, 13, 15, 17, 19, 21, 24, 27, 30, 33, 37, 41, 45, 49, 53, 57, 62, 67, 72, 77, 83, 89};
List<Integer> answer_dx0 = new ArrayList<>();
int sum = 0;
for (int i = 0; i < answer_dx1.length; i++)
{
sum += answer_dx1[i];
answer_dx0.add(sum);
}
System.err.println(answer_dx0);
for (int i = 0; i < answer_dx1.length; i++)
{
TestHeatSolver_Dx0_Helper(answer_dx0.get(i), answer_dx0.get(0), i);
}
}
@Test
public void TestHeatSolver_Dx0_Manual()
{
for (int i = 0; i < 50; i++)
{
System.err.println("[" + (350 + i) + "]" + HeatActionSolver.findDx0Index(350 + i, 7, 0));
}
}
@Test
public void TestHeatSolver_Dx0_2()
{
// 7->1,15->2,24->3,35->4,48->5,63->6,80->7,99->8,120->9,144->10,171->11,201->12,234->13,271->14,312->15,357->16,406->17,459->18,516->19,578->20,645->21,717->22,794->23,877->24,966->25
final int[] answer_dx1 =
{7, 8, 9, 11, 13, 15, 17, 19, 21, 24, 27, 30, 33, 37, 41, 45, 49, 53, 57, 62, 67, 72, 77, 83, 89};
List<Integer> answer_dx0 = new ArrayList<>();
int sum = 0;
for (int i = 0; i < answer_dx1.length; i++)
{
sum += answer_dx1[i];
answer_dx0.add(sum);
}
System.err.println(answer_dx0);
// System.err.println(
// HeatSolver.findDx0IndexContinue(406, 7, 0));
// System.err.println(
// HeatSolver.findDx0IndexContinue(406, 7, 10));
// System.err.println(
// HeatSolver.findDx0IndexContinue(406, 7, 17));
//
// System.err.println(
// HeatSolver.findDx0IndexContinue(1000, 7, 0));
System.err.println(
HeatActionSolver.findDx0Index(957, 27, 2));
// System.err.println(
// HeatActionSolver.findDx0Index(1000, 7, 1));
}
public void TestHeatSolver_Dx0_Helper(int dx0, int constant, int answer_index)
{
System.err.print(dx0 + "->" + HeatActionSolver.findDx0Index(dx0, constant, 0) + ",");
// test calcDx0Index
assertEquals("Asserting dx0 for index " + answer_index,
answer_index, HeatActionSolver.findDx0Index(dx0, constant, 0));
}
@Test
public void Calc()
{
int[] dx1 = {
27,
30,
33,
37,
41,
45,
49,
53,
57,
62,
67,
72,
77,
83,
89,
95,
91,
};
List<Integer> dx2 = new ArrayList<>();
for (int i = 0; i < dx1.length - 1; i++)
{
dx2.add(dx1[i+1] - dx1[i]);
}
System.err.println(dx2);
}
}