Repository: runelite/runelite
Pull request #17492 (2024-02-13 18:08:14)

woodcutting: prevent division by zero, wait until 3 logs cut for hourly rate
A division by zero occurs in the calculation of the number of logs/bark cut per hour in the Woodcutting plugin if 0 < `elapsed` < 1ms for `elapsed` the time since the creation of the `WoodcuttingSession`, because `elapsed` is not zero, thus the condition is true, but the calculation uses the value converted to milliseconds, which is zero on the first action (a few thousand nanoseconds).

This results in the following visible bug:
<img width="155" alt="Screenshot 2024-02-13 at 12 32 04" src="https://github.com/runelite/runelite/assets/4353030/193b8931-5371-45b7-9c5b-697f0dfea62e">

This PR fixes this by only updating `logsPerHr`,`barkPerHr`,  if at least one millisecond has passed since the creation of the `WoodcuttingSession`, i.e. on the second action, since the session is created after the first log/bark.

Nightfirecat: Alternatively, this could be fixed by using the same implementation as the xp tracker, which iirc limits per-hour calculations to a minimum of 1 minute duration. See: #2271
lhoenig: > Alternatively, this could be fixed by using the same implementation as the xp tracker, which iirc limits per-hour calculations to a minimum of 1 minute duration. See: #2271

Agreed, another alternative could be to do it like the Fishing plugin, which only sets (displays) the per-hour value after three actions in a session. As I wouldn't expect the logs/hr estimates to be so wildly inaccurate as in the case of the XP tracker, we wouldn't need to make the player wait a whole minute.
lhoenig: I added the three logs cut condition and also left the 1 ms check in, so we under no circumstances divide by zero.

For the bark I am not sure if such a three-barks-gained condition makes sense, as it is gained in variable amounts I think? I don't have first-hand experience with Forestry so cannot really judge.. it seems like a good idea to keep the 1ms check for the bark as well though as I think it might be possible to gain some without first felling a tree in some situations, i.e. at the very start of a woodcutting session.

Patch:
diff --git a/cache/pom.xml b/cache/pom.xml
index 6f5074bd7..13dff7140 100644
--- a/cache/pom.xml
+++ b/cache/pom.xml
@@ -29,7 +29,7 @@
 	<parent>
 		<groupId>net.runelite</groupId>
 		<artifactId>runelite-parent</artifactId>
-		<version>1.10.22-SNAPSHOT</version>
+		<version>1.10.23-SNAPSHOT</version>
 	</parent>
 
 	<artifactId>cache</artifactId>
diff --git a/cache/src/main/java/net/runelite/cache/NpcManager.java b/cache/src/main/java/net/runelite/cache/NpcManager.java
index b59c533b5..c72f29c48 100644
--- a/cache/src/main/java/net/runelite/cache/NpcManager.java
+++ b/cache/src/main/java/net/runelite/cache/NpcManager.java
@@ -59,7 +59,7 @@ public class NpcManager
 		Index index = store.getIndex(IndexType.CONFIGS);
 		Archive archive = index.getArchive(ConfigType.NPC.getId());
 
-		loader.setRev210HeadIcons(archive.getRevision() >= NpcLoader.REV_210_NPC_ARCHIVE_REV);
+		loader.configureForRevision(archive.getRevision());
 
 		byte[] archiveData = storage.loadArchive(archive);
 		ArchiveFiles files = archive.getFiles(archiveData);
diff --git a/cache/src/main/java/net/runelite/cache/ObjectManager.java b/cache/src/main/java/net/runelite/cache/ObjectManager.java
index ba39ae923..b59377314 100644
--- a/cache/src/main/java/net/runelite/cache/ObjectManager.java
+++ b/cache/src/main/java/net/runelite/cache/ObjectManager.java
@@ -59,6 +59,8 @@ public class ObjectManager
 		Index index = store.getIndex(IndexType.CONFIGS);
 		Archive archive = index.getArchive(ConfigType.OBJECT.getId());
 
+		loader.configureForRevision(archive.getRevision());
+
 		byte[] archiveData = storage.loadArchive(archive);
 		ArchiveFiles files = archive.getFiles(archiveData);
 
diff --git a/cache/src/main/java/net/runelite/cache/definitions/NpcDefinition.java b/cache/src/main/java/net/runelite/cache/definitions/NpcDefinition.java
index c700a64f4..ccf9e680b 100644
--- a/cache/src/main/java/net/runelite/cache/definitions/NpcDefinition.java
+++ b/cache/src/main/java/net/runelite/cache/definitions/NpcDefinition.java
@@ -71,7 +71,8 @@ public class NpcDefinition
 	public int varpIndex = -1;
 	public boolean isInteractable = true;
 	public boolean rotationFlag = true;
-	public boolean isPet;
+	public boolean isFollower;
+	public boolean lowPriorityFollowerOps;
 	public Map<Integer, Object> params;
 	public int category;
 }
diff --git a/cache/src/main/java/net/runelite/cache/definitions/ObjectDefinition.java b/cache/src/main/java/net/runelite/cache/definitions/ObjectDefinition.java
index 44324aa88..ff573015f 100644
--- a/cache/src/main/java/net/runelite/cache/definitions/ObjectDefinition.java
+++ b/cache/src/main/java/net/runelite/cache/definitions/ObjectDefinition.java
@@ -45,6 +45,7 @@ public class ObjectDefinition
 	private int sizeY = 1;
 	private int ambientSoundDistance = 0;
 	private int[] ambientSoundIds;
+	private int ambientSoundRetain;
 	private int offsetX = 0;
 	private boolean mergeNormals = false;
 	private int wallOrDoor = -1;
diff --git a/cache/src/main/java/net/runelite/cache/definitions/SequenceDefinition.java b/cache/src/main/java/net/runelite/cache/definitions/SequenceDefinition.java
index ff97ae6c6..726b71c68 100644
--- a/cache/src/main/java/net/runelite/cache/definitions/SequenceDefinition.java
+++ b/cache/src/main/java/net/runelite/cache/definitions/SequenceDefinition.java
@@ -26,6 +26,7 @@ package net.runelite.cache.definitions;
 
 import java.util.Map;
 import lombok.Data;
+import lombok.Value;
 
 @Data
 public class SequenceDefinition
@@ -34,7 +35,7 @@ public class SequenceDefinition
 	public int[] frameIDs; // top 16 bits are FrameDefinition ids
 	public int[] chatFrameIds;
 	public int[] frameLenghts;
-	public int[] frameSounds;
+	public Sound[] frameSounds;
 	public int frameStep = -1;
 	public int[] interleaveLeave;
 	public boolean stretches = false;
@@ -46,8 +47,17 @@ public class SequenceDefinition
 	public int priority = -1;
 	public int replyMode = 2;
 	public int animMayaID = -1;
-	public Map<Integer, Integer> animMayaFrameSounds;
+	public Map<Integer, Sound> animMayaFrameSounds;
 	public int animMayaStart;
 	public int animMayaEnd;
 	public boolean[] animMayaMasks;
+
+	@Value
+	public static class Sound
+	{
+		public int id;
+		public int loops;
+		public int location;
+		public int retain;
+	}
 }
diff --git a/cache/src/main/java/net/runelite/cache/definitions/loaders/NpcLoader.java b/cache/src/main/java/net/runelite/cache/definitions/loaders/NpcLoader.java
index 62016b852..8bf513b4e 100644
--- a/cache/src/main/java/net/runelite/cache/definitions/loaders/NpcLoader.java
+++ b/cache/src/main/java/net/runelite/cache/definitions/loaders/NpcLoader.java
@@ -41,6 +41,12 @@ public class NpcLoader
 	private int defaultHeadIconArchive = -1;
 	private boolean rev210HeadIcons = true;
 
+	public NpcLoader configureForRevision(int rev)
+	{
+		this.rev210HeadIcons = rev >= NpcLoader.REV_210_NPC_ARCHIVE_REV;
+		return this;
+	}
+
 	public NpcDefinition load(int id, byte[] b)
 	{
 		NpcDefinition def = new NpcDefinition(id);
@@ -259,7 +265,9 @@ public class NpcLoader
 		}
 		else if (opcode == 111)
 		{
-			def.isPet = true;
+			// removed in 220
+			def.isFollower = true;
+			def.lowPriorityFollowerOps = true;
 		}
 		else if (opcode == 114)
 		{
@@ -317,6 +325,14 @@ public class NpcLoader
 
 			def.configs[length + 1] = var;
 		}
+		else if (opcode == 122)
+		{
+			def.isFollower = true;
+		}
+		else if (opcode == 123)
+		{
+			def.lowPriorityFollowerOps = true;
+		}
 		else if (opcode == 249)
 		{
 			length = stream.readUnsignedByte();
diff --git a/cache/src/main/java/net/runelite/cache/definitions/loaders/ObjectLoader.java b/cache/src/main/java/net/runelite/cache/definitions/loaders/ObjectLoader.java
index eeef4ff49..774dca55e 100644
--- a/cache/src/main/java/net/runelite/cache/definitions/loaders/ObjectLoader.java
+++ b/cache/src/main/java/net/runelite/cache/definitions/loaders/ObjectLoader.java
@@ -26,14 +26,26 @@ package net.runelite.cache.definitions.loaders;
 
 import java.util.HashMap;
 import java.util.Map;
+import lombok.Data;
+import lombok.experimental.Accessors;
+import lombok.extern.slf4j.Slf4j;
 import net.runelite.cache.definitions.ObjectDefinition;
 import net.runelite.cache.io.InputStream;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
+@Accessors(chain = true)
+@Data
+@Slf4j
 public class ObjectLoader
 {
-	private static final Logger logger = LoggerFactory.getLogger(ObjectLoader.class);
+	public static final int REV_220_OBJ_ARCHIVE_REV = 1673;
+
+	private boolean rev220SoundData = true;
+
+	public ObjectLoader configureForRevision(int rev)
+	{
+		this.rev220SoundData = rev >= REV_220_OBJ_ARCHIVE_REV;
+		return this;
+	}
 
 	public ObjectDefinition load(int id, byte[] b)
 	{
@@ -286,12 +298,20 @@ public class ObjectLoader
 		{
 			def.setAmbientSoundId(is.readUnsignedShort());
 			def.setAmbientSoundDistance(is.readUnsignedByte());
+			if (rev220SoundData)
+			{
+				def.setAmbientSoundRetain(is.readUnsignedByte());
+			}
 		}
 		else if (opcode == 79)
 		{
 			def.setAmbientSoundChangeTicksMin(is.readUnsignedShort());
 			def.setAmbientSoundChangeTicksMax(is.readUnsignedShort());
 			def.setAmbientSoundDistance(is.readUnsignedByte());
+			if (rev220SoundData)
+			{
+				def.setAmbientSoundRetain(is.readUnsignedByte());
+			}
 			int length = is.readUnsignedByte();
 			int[] ambientSoundIds = new int[length];
 
@@ -381,7 +401,7 @@ public class ObjectLoader
 		}
 		else
 		{
-			logger.warn("Unrecognized opcode {}", opcode);
+			log.warn("Unrecognized opcode {}", opcode);
 		}
 	}
 
diff --git a/cache/src/main/java/net/runelite/cache/definitions/loaders/SequenceLoader.java b/cache/src/main/java/net/runelite/cache/definitions/loaders/SequenceLoader.java
index 41e218b4d..201a7f670 100644
--- a/cache/src/main/java/net/runelite/cache/definitions/loaders/SequenceLoader.java
+++ b/cache/src/main/java/net/runelite/cache/definitions/loaders/SequenceLoader.java
@@ -25,11 +25,27 @@
 package net.runelite.cache.definitions.loaders;
 
 import java.util.HashMap;
+import lombok.Data;
+import lombok.experimental.Accessors;
+import lombok.extern.slf4j.Slf4j;
 import net.runelite.cache.definitions.SequenceDefinition;
 import net.runelite.cache.io.InputStream;
 
+@Accessors(chain = true)
+@Data
+@Slf4j
 public class SequenceLoader
 {
+	public static final int REV_220_SEQ_ARCHIVE_REV = 1141;
+
+	private boolean rev220FrameSounds = true;
+
+	public SequenceLoader configureForRevision(int rev)
+	{
+		this.rev220FrameSounds = rev > REV_220_SEQ_ARCHIVE_REV;
+		return this;
+	}
+
 	public SequenceDefinition load(int id, byte[] b)
 	{
 		SequenceDefinition def = new SequenceDefinition(id);
@@ -141,11 +157,11 @@ public class SequenceLoader
 		else if (opcode == 13)
 		{
 			var3 = stream.readUnsignedByte();
-			def.frameSounds = new int[var3];
+			def.frameSounds = new SequenceDefinition.Sound[var3];
 
 			for (var4 = 0; var4 < var3; ++var4)
 			{
-				def.frameSounds[var4] = stream.read24BitInt();
+				def.frameSounds[var4] = this.readFrameSound(stream);
 			}
 		}
 		else if (opcode == 14)
@@ -159,9 +175,8 @@ public class SequenceLoader
 
 			for (var4 = 0; var4 < var3; ++var4)
 			{
-				int var5 = stream.readUnsignedShort();
-				int var6 = stream.read24BitInt();
-				def.animMayaFrameSounds.put(var5, var6);
+				int frame = stream.readUnsignedShort();
+				def.animMayaFrameSounds.put(frame, this.readFrameSound(stream));
 			}
 		}
 		else if (opcode == 16)
@@ -180,5 +195,41 @@ public class SequenceLoader
 				def.animMayaMasks[stream.readUnsignedByte()] = true;
 			}
 		}
+		else
+		{
+			log.warn("Unrecognized opcode {}", opcode);
+		}
+	}
+
+	private SequenceDefinition.Sound readFrameSound(InputStream stream)
+	{
+		int id;
+		int loops;
+		int location;
+		int retain;
+		if (!rev220FrameSounds)
+		{
+			int bits = stream.read24BitInt();
+			location = bits & 15;
+			id = bits >> 8;
+			loops = bits >> 4 & 7;
+			retain = 0;
+		}
+		else
+		{
+			id = stream.readUnsignedShort();
+			loops = stream.readUnsignedByte();
+			location = stream.readUnsignedByte();
+			retain = stream.readUnsignedByte();
+		}
+
+		if (id >= 1 && loops >= 1 && location >= 0 && retain >= 0)
+		{
+			return new SequenceDefinition.Sound(id, loops, location, retain);
+		}
+		else
+		{
+			return null;
+		}
 	}
 }
diff --git a/pom.xml b/pom.xml
index a2406b5b5..00a0fe95c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -28,7 +28,7 @@
 
 	<groupId>net.runelite</groupId>
 	<artifactId>runelite-parent</artifactId>
-	<version>1.10.22-SNAPSHOT</version>
+	<version>1.10.23-SNAPSHOT</version>
 	<packaging>pom</packaging>
 
 	<name>RuneLite</name>
@@ -47,7 +47,7 @@
 		<checkstyle.skip>true</checkstyle.skip>
 		<glslang.path></glslang.path>
 
-		<rs.version>219</rs.version>
+		<rs.version>220</rs.version>
 	</properties>
 
 	<licenses>
diff --git a/runelite-api/pom.xml b/runelite-api/pom.xml
index eb3abd1d1..c3e8d3d95 100644
--- a/runelite-api/pom.xml
+++ b/runelite-api/pom.xml
@@ -29,7 +29,7 @@
 	<parent>
 		<groupId>net.runelite</groupId>
 		<artifactId>runelite-parent</artifactId>
-		<version>1.10.22-SNAPSHOT</version>
+		<version>1.10.23-SNAPSHOT</version>
 	</parent>
 
 	<artifactId>runelite-api</artifactId>
diff --git a/runelite-api/src/main/java/net/runelite/api/Quest.java b/runelite-api/src/main/java/net/runelite/api/Quest.java
index db3fa4b77..dab17fa1a 100644
--- a/runelite-api/src/main/java/net/runelite/api/Quest.java
+++ b/runelite-api/src/main/java/net/runelite/api/Quest.java
@@ -216,6 +216,7 @@ public enum Quest
 	HIS_FAITHFUL_SERVANTS(3250, "His Faithful Servants"),
 	THE_PATH_OF_GLOUPHRIE(3425, "The Path of Glouphrie"),
 	CHILDREN_OF_THE_SUN(3450, "Children of the Sun"),
+	BARBARIAN_TRAINING(3451, "Barbarian Training"),
 	;
 
 	@Getter
diff --git a/runelite-client/pom.xml b/runelite-client/pom.xml
index 15cbb4d45..6e93907c6 100644
--- a/runelite-client/pom.xml
+++ b/runelite-client/pom.xml
@@ -29,7 +29,7 @@
 	<parent>
 		<groupId>net.runelite</groupId>
 		<artifactId>runelite-parent</artifactId>
-		<version>1.10.22-SNAPSHOT</version>
+		<version>1.10.23-SNAPSHOT</version>
 	</parent>
 
 	<artifactId>client</artifactId>
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/woodcutting/WoodcuttingSession.java b/runelite-client/src/main/java/net/runelite/client/plugins/woodcutting/WoodcuttingSession.java
index 62c12efd8..bab395c0f 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/woodcutting/WoodcuttingSession.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/woodcutting/WoodcuttingSession.java
@@ -58,9 +58,10 @@ class WoodcuttingSession
 		++logsCut;
 
 		Duration elapsed = Duration.between(start, Instant.now());
-		if (!elapsed.isZero())
+		long elapsedMs = elapsed.toMillis();
+		if (logsCut >= 3 && elapsedMs > 0)
 		{
-			logsPerHr = (int) ((double) logsCut * Duration.ofHours(1).toMillis() / elapsed.toMillis());
+			logsPerHr = (int) ((double) logsCut * Duration.ofHours(1).toMillis() / elapsedMs);
 		}
 	}
 
@@ -69,9 +70,10 @@ class WoodcuttingSession
 		bark += num;
 
 		Duration elapsed = Duration.between(start, Instant.now());
-		if (!elapsed.isZero())
+		long elapsedMs = elapsed.toMillis();
+		if (elapsedMs > 0)
 		{
-			barkPerHr = (int) ((double) bark * Duration.ofHours(1).toMillis() / elapsed.toMillis());
+			barkPerHr = (int) ((double) bark * Duration.ofHours(1).toMillis() / elapsedMs);
 		}
 	}
 }
diff --git a/runelite-jshell/pom.xml b/runelite-jshell/pom.xml
index ede76af18..1e4c14552 100644
--- a/runelite-jshell/pom.xml
+++ b/runelite-jshell/pom.xml
@@ -30,7 +30,7 @@
     <parent>
         <groupId>net.runelite</groupId>
         <artifactId>runelite-parent</artifactId>
-        <version>1.10.22-SNAPSHOT</version>
+        <version>1.10.23-SNAPSHOT</version>
     </parent>
 
     <artifactId>jshell</artifactId>
diff --git a/runelite-maven-plugin/pom.xml b/runelite-maven-plugin/pom.xml
index ffee39af7..16d96eca0 100644
--- a/runelite-maven-plugin/pom.xml
+++ b/runelite-maven-plugin/pom.xml
@@ -29,7 +29,7 @@
 	<parent>
 		<groupId>net.runelite</groupId>
 		<artifactId>runelite-parent</artifactId>
-		<version>1.10.22-SNAPSHOT</version>
+		<version>1.10.23-SNAPSHOT</version>
 	</parent>
 
 	<artifactId>runelite-maven-plugin</artifactId>


Test patch:
diff --git a/cache/src/test/java/net/runelite/cache/SequenceDumper.java b/cache/src/test/java/net/runelite/cache/SequenceDumper.java
index b03bed7f0..e575530bb 100644
--- a/cache/src/test/java/net/runelite/cache/SequenceDumper.java
+++ b/cache/src/test/java/net/runelite/cache/SequenceDumper.java
@@ -77,6 +77,7 @@ public class SequenceDumper
 			for (FSFile file : files.getFiles())
 			{
 				SequenceLoader loader = new SequenceLoader();
+				loader.configureForRevision(archive.getRevision());
 				SequenceDefinition seq = loader.load(file.getFileId(), file.getContents());
 
 				Files.asCharSink(new File(outDir, file.getFileId() + ".json"), Charset.defaultCharset()).write(gson.toJson(seq));