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
24 changes: 23 additions & 1 deletion src/main/java/net/tfminecraft/rpcharacters/RPCharacters.java
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
import net.tfminecraft.rpcharacters.loaders.PartyLoader;
import net.tfminecraft.rpcharacters.managers.CommandManager;
import net.tfminecraft.rpcharacters.managers.CreationManager;
import net.tfminecraft.rpcharacters.paidchange.PaidChangeListener;
import net.tfminecraft.rpcharacters.managers.ClueDisturbanceListener;
import net.tfminecraft.rpcharacters.managers.MagnifyingGlassListener;
import net.tfminecraft.rpcharacters.managers.PlaceClueManager;
Expand Down Expand Up @@ -291,6 +292,7 @@ public void onDisable() {
PvpStrikeService.shutdown();
LastSolidTracker.get().shutdown();
GraveManager.get().saveAll();
PaidChangeListener.settleAll();
save();
}

Expand All @@ -310,6 +312,7 @@ public void registerListeners() {
getServer().getPluginManager().registerEvents(playerManager, this);
getServer().getPluginManager().registerEvents(new net.tfminecraft.rpcharacters.mmocore.MmoCorePlayerReady(), this);
getServer().getPluginManager().registerEvents(creationManager, this);
getServer().getPluginManager().registerEvents(new PaidChangeListener(), this);
getServer().getPluginManager().registerEvents(clueInputManager, this);
getServer().getPluginManager().registerEvents(placeClueManager, this);
getServer().getPluginManager().registerEvents(magnifyingGlassListener, this);
Expand Down Expand Up @@ -532,6 +535,7 @@ public void reload() {

private boolean reloadWithFocusStatus() {
loadConfigs();
refreshStageRevisionsOnline();
boolean focusReloaded = reloadFocusConfig();
if (!focusReloaded) {
getLogger().warning("Focus did not reload; see the preceding focus error. "
Expand All @@ -544,7 +548,25 @@ private boolean reloadWithFocusStatus() {
return focusReloaded;
}

public void reloadConfigs(CommandSender sender) {
/** A raised stage revision opens a fresh lock window for online characters straight away. */
private void refreshStageRevisionsOnline() {
for (Player p : Bukkit.getOnlinePlayers()) {
net.tfminecraft.rpcharacters.objects.PlayerData pd = PlayerManager.get(p);
if (pd == null) {
continue;
}
boolean changed = false;
for (net.tfminecraft.rpcharacters.objects.RPCharacter c : pd.getCharacters()) {
changed |= net.tfminecraft.rpcharacters.creation.StageRevisions.refresh(
c, net.tfminecraft.rpcharacters.loaders.StageLoader.oList);
}
if (changed) {
playerManager.savePlayer(p);
}
}
}

public void reloadConfigs(CommandSender sender) {
String name = sender != null ? sender.getName() : "unknown";
getLogger().info("Config reload requested by " + name);
if (sender != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@

import net.tfminecraft.rpcharacters.objects.RPCharacter;

import net.tfminecraft.rpcharacters.paidchange.PaidChangeService;
import net.tfminecraft.rpcharacters.paidchange.PendingPaidChange;

import net.tfminecraft.rpcharacters.objects.attributes.AttributeData;
import net.tfminecraft.rpcharacters.objects.trait.Trait;
import net.tfminecraft.rpcharacters.objects.trait.TraitEffectResolver;
Expand Down Expand Up @@ -408,6 +411,15 @@ public RPCharacter getCharacter() {

}

public Player getPlayer() {
return p;
}

/** Denars held while a paid stage is open; kept on the character so they are saved. */
public PendingPaidChange getPendingPaidChange() {
return character == null ? null : character.getPendingPaidChange();
}



public void openSummary() {
Expand Down Expand Up @@ -490,9 +502,19 @@ public void jumpToStageForEdit(String stageId) {

}

if (!StageEditLock.canEdit(p, template, character)) {
PaidChangeService.settle(this);

if (character.getPendingPaidChange() != null) {

// A refund that failed stays held. Any edit now would look like the paid change was used.

RPTexts.send(p, RPTexts.ERROR + "Your last refund hasn't gone through yet. Try again after you rejoin.");

return;

}

RPTexts.send(p, RPTexts.ERROR + "That choice is locked and can no longer be edited.");
if (!StageEditLock.canEdit(p, template, character) && !PaidChangeService.payToOpen(p, this, template)) {

return;

Expand All @@ -504,6 +526,8 @@ public void jumpToStageForEdit(String stageId) {

RPTexts.send(p, RPTexts.ERROR + "Could not open editor for that choice.");

PaidChangeService.settle(this);

return;

}
Expand Down Expand Up @@ -542,6 +566,8 @@ public void jumpToStageForEdit(String stageId) {

RPTexts.send(p, RPTexts.ERROR + "That choice cannot be edited from the summary.");

PaidChangeService.settle(this);

}

}
Expand All @@ -562,6 +588,8 @@ public void returnToSummary() {

editStage = null;

PaidChangeService.settle(this);

if (isEditing()) {

persistEdits();
Expand Down Expand Up @@ -827,6 +855,8 @@ public void finish() {

character.update();

StageRevisions.stampCurrent(character, StageLoader.oList);

pd.addCharacter(character);

net.tfminecraft.rpcharacters.lifecycle.CharacterLifecycle.fireCreated(p, pd.getUniqueId(), character);
Expand Down Expand Up @@ -857,6 +887,8 @@ public void finish() {

public void closeEditSession() {

PaidChangeService.settle(this);

persistEdits();

CreationManager.activeCreators.remove(p);
Expand Down Expand Up @@ -1063,6 +1095,8 @@ public void cancel() {

if (isEditing()) {

PaidChangeService.settle(this);

CreationManager.activeCreators.remove(p);

p.closeInventory();
Expand Down
13 changes: 13 additions & 0 deletions src/main/java/net/tfminecraft/rpcharacters/creation/Stage.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ public class Stage {

private long lockTimeMs = -1L;

/** Raised by hand after a large change; gives every character a fresh lock window. */
private int revision;

/** Inclusive minimum account-age hours, or null if unset. */
private Integer requireAccountAgeHoursMin;
/** Exclusive maximum account-age hours, or null if unset. */
Expand All @@ -35,6 +38,14 @@ public class Stage {
/** Creation client: both (default), web-only, or game-only. */
private String platform = "both";

public int getRevision() {
return revision;
}

public void setRevision(int revision) {
this.revision = Math.max(0, revision);
}

public long getLockTimeMs() {
return lockTimeMs;
}
Expand Down Expand Up @@ -114,6 +125,7 @@ protected void copyBaseFields(Stage source) {
setDependency(null);
}
setLockTimeMs(source.getLockTimeMs());
setRevision(source.getRevision());
setRequireAccountAgeHoursMin(source.getRequireAccountAgeHoursMin());
setRequireAccountAgeHoursMax(source.getRequireAccountAgeHoursMax());
setPlatform(source.getPlatform());
Expand Down Expand Up @@ -172,6 +184,7 @@ public static Stage create(String id, ConfigurationSection config) {
s.setDependency(new Dependency(config.getConfigurationSection("dependency")));
}
s.setLockTimeMs(DurationParser.parseLockTimeMs(config.getString("lock-time", "-1")));
s.setRevision(config.getInt("revision", 0));
if (config.contains("require-account-age-hours-min")) {
s.setRequireAccountAgeHoursMin(config.getInt("require-account-age-hours-min"));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public static boolean canEdit(Stage stage, RPCharacter character) {
if (lockMs < 0) {
return true;
}
return character.getAgeSeconds() * 1000L < lockMs;
return StageRevisions.secondsIntoWindow(stage, character) * 1000L < lockMs;
}

public static long lockRemainingMs(Stage stage, RPCharacter character) {
Expand All @@ -39,7 +39,7 @@ public static long lockRemainingMs(Stage stage, RPCharacter character) {
if (lockMs < 0) {
return 0L;
}
long remaining = lockMs - (character.getAgeSeconds() * 1000L);
long remaining = lockMs - (StageRevisions.secondsIntoWindow(stage, character) * 1000L);
return Math.max(0L, remaining);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package net.tfminecraft.rpcharacters.creation;

import java.time.Instant;
import java.util.Collection;
import java.util.Locale;

import net.tfminecraft.rpcharacters.objects.RPCharacter;

/**
* Staff raise a stage's {@code revision} in stages.yml after a large change to it. Each character
* then gets a fresh lock window on that stage, starting when it next loads, and its paid-change
* prices start over.
*/
public final class StageRevisions {

private StageRevisions() {}

/** Starts a fresh window on every stage whose revision passed the character's. True if any did. */
public static boolean refresh(RPCharacter character, Collection<? extends Stage> stages, long nowEpochSeconds) {
if (character == null || stages == null) {
return false;
}
boolean changed = false;
for (Stage stage : stages) {
if (stage == null || stage.getId() == null) {
continue;
}
String id = key(stage);
if (stage.getRevision() > character.getStageRevision(id)) {
character.setStageRevision(id, stage.getRevision(), nowEpochSeconds);
character.setPaidChangeCount(id, 0);
changed = true;
}
}
return changed;
}

public static boolean refresh(RPCharacter character, Collection<? extends Stage> stages) {
return refresh(character, stages, Instant.now().getEpochSecond());
}

/** New characters start on the current revisions, with the window counted from creation. */
public static void stampCurrent(RPCharacter character, Collection<? extends Stage> stages) {
if (character == null || stages == null) {
return;
}
for (Stage stage : stages) {
if (stage != null && stage.getId() != null && stage.getRevision() > 0) {
character.setStageRevision(key(stage), stage.getRevision(), 0L);
}
}
}

/** Seconds since this stage's lock window opened: creation, or the latest revision reset. */
public static long secondsIntoWindow(Stage stage, RPCharacter character, long nowEpochSeconds) {
int createdAt = character.getCreatedAtEpochSeconds();
if (createdAt <= 0) {
return 0L;
}
long start = Math.max(createdAt, character.getStageRevisionSince(key(stage)));
return Math.max(0L, nowEpochSeconds - start);
}

/** Stage ids are stored lower case, the same key paid-change counts use. */
public static String key(Stage stage) {
return stage.getId() == null ? null : stage.getId().toLowerCase(Locale.ROOT);
}

public static long secondsIntoWindow(Stage stage, RPCharacter character) {
return secondsIntoWindow(stage, character, Instant.now().getEpochSecond());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package net.tfminecraft.rpcharacters.database;

import java.math.BigDecimal;
import java.util.Map;
import java.util.UUID;
import java.util.logging.Logger;

import org.json.simple.JSONObject;

import net.tfminecraft.rpcharacters.objects.RPCharacter;
import net.tfminecraft.rpcharacters.paidchange.DenarWallet.Account;
import net.tfminecraft.rpcharacters.paidchange.PendingPaidChange;

/**
* Per-stage change state in the character file: paid change counts under "paid-changes", the
* stage revision each lock window was opened for under "stage-revisions" (both keyed by stage id),
* and denars held for an open paid stage under "paid-change-pending".
*/
public final class CharacterStageChangeFields {
private CharacterStageChangeFields() {}

public static void load(RPCharacter character, JSONObject characterJson) {
if (character == null || characterJson == null) {
return;
}
if (characterJson.get("paid-changes") instanceof Map<?, ?> counts) {
for (Map.Entry<?, ?> entry : counts.entrySet()) {
if (entry.getKey() instanceof String stageId && entry.getValue() instanceof Number count) {
character.setPaidChangeCount(stageId, count.intValue());
}
}
}
if (characterJson.get("paid-change-pending") instanceof Map<?, ?> pending) {
character.setPendingPaidChange(readPending(pending));
}
if (characterJson.get("stage-revisions") instanceof Map<?, ?> revisions) {
for (Map.Entry<?, ?> entry : revisions.entrySet()) {
if (entry.getKey() instanceof String stageId && entry.getValue() instanceof Map<?, ?> mark
&& mark.get("revision") instanceof Number revision) {
long since = mark.get("since") instanceof Number n ? n.longValue() : 0L;
character.setStageRevision(stageId, revision.intValue(), since);
}
}
}
}

@SuppressWarnings("unchecked")
public static void save(Map<String, Object> defaults, RPCharacter character) {
if (!character.getPaidChangeCounts().isEmpty()) {
JSONObject counts = new JSONObject();
counts.putAll(character.getPaidChangeCounts());
defaults.put("paid-changes", counts);
}
if (!character.getStageRevisions().isEmpty()) {
JSONObject revisions = new JSONObject();
for (Map.Entry<String, Integer> entry : character.getStageRevisions().entrySet()) {
JSONObject mark = new JSONObject();
mark.put("revision", entry.getValue());
long since = character.getStageRevisionSince(entry.getKey());
if (since > 0L) {
mark.put("since", since);
}
revisions.put(entry.getKey(), mark);
}
defaults.put("stage-revisions", revisions);
}
PendingPaidChange pending = character.getPendingPaidChange();
if (pending != null) {
JSONObject held = new JSONObject();
held.put("stage", pending.stageId());
held.put("label", pending.label());
held.put("payer", pending.payerId().toString());
if (pending.account() != null) {
held.put("account", pending.account().name());
}
held.put("amount", pending.amount().toPlainString());
held.put("before", pending.before());
defaults.put("paid-change-pending", held);
}
}

private static PendingPaidChange readPending(Map<?, ?> held) {
try {
Account account = held.get("account") instanceof String name ? Account.valueOf(name) : null;
BigDecimal amount = new BigDecimal(String.valueOf(held.get("amount")));
return new PendingPaidChange((String) held.get("stage"), (String) held.get("label"),
UUID.fromString((String) held.get("payer")), account, amount, (String) held.get("before"));
} catch (RuntimeException e) {
Logger.getLogger("RPCharacters").warning("[RPCharacters] Unreadable paid-change-pending entry " + held
+ "; refund it by hand if denars were taken.");
return null;
}
}
}
Loading
Loading