-
Notifications
You must be signed in to change notification settings - Fork 0
refactor: own per-character focus in RPCharacters #27
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
9e1ed6d
refactor: own character focus in RPCharacters
ryanbarlow97 95c9b3c
fix: continue character reload when focus stands down
ryanbarlow97 0871cca
refactor: manage focus directly without migration support
ryanbarlow97 a616f0f
fix: limit focus access to the active character
ryanbarlow97 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
24 changes: 24 additions & 0 deletions
24
src/main/java/net/tfminecraft/rpcharacters/focus/FocusAttributes.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| package net.tfminecraft.rpcharacters.focus; | ||
|
|
||
| import org.bukkit.entity.Player; | ||
|
|
||
| import net.Indyuce.mmocore.api.player.PlayerData; | ||
|
|
||
| final class FocusAttributes { | ||
|
|
||
| private FocusAttributes() {} | ||
|
|
||
| static double getTotal(Player player, String mmocoreId) { | ||
| if (player == null || mmocoreId == null || mmocoreId.isBlank()) { | ||
| return 0; | ||
| } | ||
| if (player.getServer().getPluginManager().getPlugin("MMOCore") == null) { | ||
| return 0; | ||
| } | ||
| try { | ||
| return PlayerData.get(player).getAttributes().getInstance(mmocoreId).getTotal(); | ||
| } catch (Exception ex) { | ||
| return 0; | ||
| } | ||
| } | ||
| } |
69 changes: 69 additions & 0 deletions
69
src/main/java/net/tfminecraft/rpcharacters/focus/FocusCommand.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| package net.tfminecraft.rpcharacters.focus; | ||
|
|
||
| import java.util.List; | ||
| import java.util.Locale; | ||
|
|
||
| import org.bukkit.Bukkit; | ||
| import org.bukkit.command.Command; | ||
| import org.bukkit.command.CommandExecutor; | ||
| import org.bukkit.command.CommandSender; | ||
| import org.bukkit.command.TabCompleter; | ||
| import org.bukkit.entity.Player; | ||
|
|
||
| import net.tfminecraft.rpcharacters.RPCharacters; | ||
|
|
||
| public final class FocusCommand implements CommandExecutor, TabCompleter { | ||
| public static final String PERMISSION = "rpchar.focus.admin"; | ||
| private final RPCharacters plugin; | ||
|
|
||
| public FocusCommand(RPCharacters plugin) { | ||
| this.plugin = plugin; | ||
| } | ||
|
|
||
| @Override | ||
| public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { | ||
| if (!sender.hasPermission(PERMISSION)) { | ||
| sender.sendMessage("You do not have permission to use this command."); | ||
| return true; | ||
| } | ||
| if (args.length == 1 && args[0].equalsIgnoreCase("reload")) { | ||
| boolean loaded = plugin.reloadFocusConfig(); | ||
| sender.sendMessage(loaded ? "Focus configuration reloaded." | ||
| : "Focus configuration did not reload. Check console for the configuration error."); | ||
| return true; | ||
| } | ||
| if (args.length == 2 && args[0].equalsIgnoreCase("restore")) { | ||
| Player target = Bukkit.getPlayerExact(args[1]); | ||
| if (target == null) { | ||
| sender.sendMessage("Player not found: " + args[1]); | ||
| return true; | ||
| } | ||
| FocusService focus = RPCharacters.getFocusService(); | ||
| if (focus == null || !focus.restore(target)) { | ||
| sender.sendMessage("Could not restore focus for " + target.getName() | ||
| + ". Check their active character and the focus service in console."); | ||
| return true; | ||
| } | ||
| sender.sendMessage("Restored focus for " + target.getName() + " (" | ||
| + focus.getPoints(target) + "/" + focus.getMax() + ")."); | ||
| return true; | ||
| } | ||
| sender.sendMessage("Usage: /focus restore <player> | /focus reload"); | ||
| return true; | ||
| } | ||
|
|
||
| @Override | ||
| public List<String> onTabComplete(CommandSender sender, Command command, String alias, String[] args) { | ||
| if (!sender.hasPermission(PERMISSION)) return List.of(); | ||
| if (args.length == 1) { | ||
| return List.of("restore", "reload").stream() | ||
| .filter(value -> value.startsWith(args[0].toLowerCase(Locale.ROOT))).toList(); | ||
| } | ||
| if (args.length == 2 && args[0].equalsIgnoreCase("restore")) { | ||
| String prefix = args[1].toLowerCase(Locale.ROOT); | ||
| return Bukkit.getOnlinePlayers().stream().map(Player::getName) | ||
| .filter(name -> name.toLowerCase(Locale.ROOT).startsWith(prefix)).sorted().toList(); | ||
| } | ||
| return List.of(); | ||
| } | ||
| } |
25 changes: 25 additions & 0 deletions
25
src/main/java/net/tfminecraft/rpcharacters/focus/FocusConfig.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| package net.tfminecraft.rpcharacters.focus; | ||
|
|
||
| import java.util.ArrayList; | ||
| import java.util.List; | ||
|
|
||
| public final class FocusConfig { | ||
|
|
||
| public static int max = 150; | ||
| public static int basePerHour = 10; | ||
| public static long regenIntervalTicks = 72000L; | ||
| public static boolean offlineRegen = true; | ||
| public static final List<RegenBonus> regenBonuses = new ArrayList<>(); | ||
|
|
||
| private FocusConfig() {} | ||
|
|
||
| public static final class RegenBonus { | ||
| public final String mmocoreId; | ||
| public final double extraPerHourPerPoint; | ||
|
|
||
| public RegenBonus(String mmocoreId, double extraPerHourPerPoint) { | ||
| this.mmocoreId = mmocoreId; | ||
| this.extraPerHourPerPoint = extraPerHourPerPoint; | ||
| } | ||
| } | ||
| } |
46 changes: 46 additions & 0 deletions
46
src/main/java/net/tfminecraft/rpcharacters/focus/FocusConfigLoader.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| package net.tfminecraft.rpcharacters.focus; | ||
|
|
||
| import java.io.File; | ||
| import java.io.IOException; | ||
| import java.util.Map; | ||
|
|
||
| import org.bukkit.configuration.InvalidConfigurationException; | ||
| import org.bukkit.configuration.file.FileConfiguration; | ||
| import org.bukkit.configuration.file.YamlConfiguration; | ||
|
|
||
| import net.tfminecraft.rpcharacters.RPCharacters; | ||
|
|
||
| public final class FocusConfigLoader { | ||
|
|
||
| private FocusConfigLoader() {} | ||
|
|
||
| public static boolean load(File file) { | ||
| return load(file, RPCharacters.plugin.getLogger()); | ||
| } | ||
|
|
||
| static boolean load(File file, java.util.logging.Logger logger) { | ||
| FileConfiguration config = new YamlConfiguration(); | ||
| try { | ||
| config.load(file); | ||
| } catch (IOException | InvalidConfigurationException ex) { | ||
| logger.severe("[RPCharacters] Failed to load focus.yml: " + ex.getMessage()); | ||
| return false; | ||
| } | ||
| FocusConfig.max = Math.max(1, config.getInt("max", FocusConfig.max)); | ||
| FocusConfig.basePerHour = config.getInt("base_per_hour", FocusConfig.basePerHour); | ||
| FocusConfig.regenIntervalTicks = Math.max(1L, config.getLong("regen_interval_ticks", FocusConfig.regenIntervalTicks)); | ||
| FocusConfig.offlineRegen = config.getBoolean("offline_regen", FocusConfig.offlineRegen); | ||
| FocusConfig.regenBonuses.clear(); | ||
| for (Map<?, ?> map : config.getMapList("regen_bonuses")) { | ||
| String id = map.get("mmocore_id") != null ? String.valueOf(map.get("mmocore_id")) : ""; | ||
| double extra = 0; | ||
| if (map.get("extra_per_hour_per_point") instanceof Number number) { | ||
| extra = number.doubleValue(); | ||
| } | ||
| if (!id.isBlank()) { | ||
| FocusConfig.regenBonuses.add(new FocusConfig.RegenBonus(id, extra)); | ||
| } | ||
| } | ||
| return true; | ||
| } | ||
| } |
95 changes: 95 additions & 0 deletions
95
src/main/java/net/tfminecraft/rpcharacters/focus/FocusData.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,95 @@ | ||
| package net.tfminecraft.rpcharacters.focus; | ||
|
|
||
| public final class FocusData { | ||
|
|
||
| private String characterId; | ||
| private String ownerUuid; | ||
| private int points; | ||
| private long lastRegenMs; | ||
|
|
||
| public FocusData() {} | ||
|
|
||
| public static FocusData createNew(String characterId, String ownerUuid) { | ||
| FocusData data = new FocusData(); | ||
| data.characterId = characterId; | ||
| data.ownerUuid = ownerUuid; | ||
| data.points = FocusConfig.max; | ||
| data.lastRegenMs = System.currentTimeMillis(); | ||
| return data; | ||
| } | ||
|
|
||
| public String getCharacterId() { | ||
| return characterId; | ||
| } | ||
|
|
||
| public void setCharacterId(String characterId) { | ||
| this.characterId = characterId; | ||
| } | ||
|
|
||
| public String getOwnerUuid() { | ||
| return ownerUuid; | ||
| } | ||
|
|
||
| public void setOwnerUuid(String ownerUuid) { | ||
| this.ownerUuid = ownerUuid; | ||
| } | ||
|
|
||
| public int getPoints() { | ||
| return points; | ||
| } | ||
|
|
||
| public void setPoints(int points) { | ||
| this.points = clamp(points); | ||
| } | ||
|
|
||
| public long getLastRegenMs() { | ||
| return lastRegenMs; | ||
| } | ||
|
|
||
| public void setLastRegenMs(long lastRegenMs) { | ||
| this.lastRegenMs = lastRegenMs; | ||
| } | ||
|
|
||
| public int applyRegenForElapsed(double hourlyRate, long intervalMs, long nowMs) { | ||
| if (intervalMs <= 0 || hourlyRate <= 0) { | ||
| return 0; | ||
| } | ||
| if (points >= FocusConfig.max) { | ||
| lastRegenMs = nowMs; | ||
| return 0; | ||
| } | ||
| long elapsed = nowMs - lastRegenMs; | ||
| if (elapsed < intervalMs) { | ||
| return 0; | ||
| } | ||
| long intervals = elapsed / intervalMs; | ||
| double msPerHour = 3_600_000.0; | ||
| int perInterval = Math.max(1, (int) Math.round(hourlyRate * intervalMs / msPerHour)); | ||
| int newPoints = Math.min(points + (int) intervals * perInterval, FocusConfig.max); | ||
| int added = newPoints - points; | ||
| points = newPoints; | ||
| lastRegenMs += intervals * intervalMs; | ||
| return added; | ||
| } | ||
|
|
||
| public boolean trySpend(int amount) { | ||
| if (amount <= 0) { | ||
| return true; | ||
| } | ||
| if (points < amount) { | ||
| return false; | ||
| } | ||
| points -= amount; | ||
| return true; | ||
| } | ||
|
|
||
| public void grant(int amount) { | ||
| if (amount > 0) { | ||
| points = clamp(points + amount); | ||
| } | ||
| } | ||
|
|
||
| private static int clamp(int value) { | ||
| return Math.min(Math.max(0, value), FocusConfig.max); | ||
| } | ||
| } | ||
34 changes: 34 additions & 0 deletions
34
src/main/java/net/tfminecraft/rpcharacters/focus/FocusListener.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| package net.tfminecraft.rpcharacters.focus; | ||
|
|
||
| import org.bukkit.entity.Player; | ||
| import org.bukkit.event.EventHandler; | ||
| import org.bukkit.event.Listener; | ||
| import org.bukkit.event.player.PlayerQuitEvent; | ||
|
|
||
| import net.tfminecraft.rpcharacters.lifecycle.CharacterActivatedEvent; | ||
|
|
||
| public final class FocusListener implements Listener { | ||
|
|
||
| private final FocusService service; | ||
|
|
||
| public FocusListener(FocusService service) { | ||
| this.service = service; | ||
| } | ||
|
|
||
| @EventHandler | ||
| public void onCharacterActivated(CharacterActivatedEvent event) { | ||
| Player owner = event.getOwner(); | ||
| if (owner == null || event.getCharacter() == null) { | ||
| return; | ||
| } | ||
| if (event.getPrevious() != null) { | ||
| service.savePrevious(owner, event.getPrevious()); | ||
| } | ||
| service.activate(owner, event.getCharacter()); | ||
| } | ||
|
|
||
| @EventHandler | ||
| public void onQuit(PlayerQuitEvent event) { | ||
| service.deactivate(event.getPlayer()); | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
Repository: TF-Minecraft/RPCharacters
Length of output: 1002
🏁 Script executed:
Repository: TF-Minecraft/RPCharacters
Length of output: 25656
🏁 Script executed:
Repository: TF-Minecraft/RPCharacters
Length of output: 28578
🏁 Script executed:
Repository: TF-Minecraft/RPCharacters
Length of output: 7552
Accrue fractional points per complete interval and prevent interval-count overflow.
base_per_hourandextra_per_hour_per_pointform thehourlyRate. A shortregen_interval_ticksmust not force one point per interval. With 100 ticks, the current code awards 720 points per hour for a rate of 10. Theintcast can also overflow before multiplication.Do not replace the interval calculation with
floor(elapsed / msPerPoint). That counts an incomplete configured interval.FocusDataTestexpects 20 points after two complete one-hour intervals, although the elapsed time includes an additional partial interval.Add a persisted fractional accumulator to
FocusData. For each complete interval, addhourlyRate * intervalMs / 3_600_000to the accumulator, award its whole-point part, retain its fraction, and advancelastRegenMsby the complete intervals processed.FocusStoreserializesFocusDatawith Gson, so the accumulator will persist with the record.🐛 Suggested fix
private int points; private long lastRegenMs; + private double regenRemainder; @@ if (points >= FocusConfig.max) { lastRegenMs = nowMs; + regenRemainder = 0.0; return 0; } @@ long intervals = elapsed / intervalMs; double msPerHour = 3_600_000.0; - int perInterval = Math.max(1, (int) Math.round(hourlyRate * intervalMs / msPerHour)); - int newPoints = Math.min(points + (int) intervals * perInterval, FocusConfig.max); - int added = newPoints - points; - points = newPoints; + double perInterval = hourlyRate * intervalMs / msPerHour; + double accumulated = regenRemainder + intervals * perInterval; + double wholePoints = Math.floor(accumulated); lastRegenMs += intervals * intervalMs; + + if (wholePoints <= 0) { + regenRemainder = accumulated; + return 0; + } + + int room = FocusConfig.max - points; + int added = (int) Math.min(wholePoints, room); + points += added; + if (added < wholePoints) { + lastRegenMs = nowMs; + regenRemainder = 0.0; + } else { + regenRemainder = accumulated - added; + } return added;Add tests for short intervals, fractional hourly rates, persistence of the fractional remainder, and large elapsed values.
🤖 Prompt for AI Agents