diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d8069adf7..340587455 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -32,7 +32,7 @@ jobs: timeout-minutes: 20 steps: - name: Checkout project sources - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: ref: ${{github.event.pull_request.head.sha || github.sha}} @@ -45,13 +45,13 @@ jobs: run: echo "version=$(grep version gradle.properties | cut -d"=" -f2 | xargs)" >> $GITHUB_OUTPUT - name: Setup Java - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: temurin java-version: 25 - name: Setup Gradle - uses: gradle/actions/setup-gradle@v4 + uses: gradle/actions/setup-gradle@v6 - name: Run build with Gradle Wrapper run: ./gradlew "-Pversion=${{steps.version.outputs.version}}-${{steps.hash.outputs.sha_short}}" core:build @@ -59,7 +59,7 @@ jobs: - name: Upload artifact if: github.event_name == 'push' || contains(github.event.pull_request.labels.*.name, 'Build PR Jar') id: artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: MagicSpells-${{steps.version.outputs.version}}-${{steps.hash.outputs.sha_short}} if-no-files-found: error diff --git a/.github/workflows/pr_comment.yml b/.github/workflows/pr_comment.yml index 812afdaf8..07f49e37e 100644 --- a/.github/workflows/pr_comment.yml +++ b/.github/workflows/pr_comment.yml @@ -12,7 +12,7 @@ jobs: github.event.workflow_run.event == 'pull_request' runs-on: ubuntu-latest steps: - - uses: actions/github-script@v7 + - uses: actions/github-script@v9 with: script: | const label = "Build PR Jar"; diff --git a/core/src/main/java/com/nisovin/magicspells/Spell.java b/core/src/main/java/com/nisovin/magicspells/Spell.java index f7db15374..f2f7d414b 100644 --- a/core/src/main/java/com/nisovin/magicspells/Spell.java +++ b/core/src/main/java/com/nisovin/magicspells/Spell.java @@ -2,6 +2,7 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.MustBeInvokedByOverriders; import de.slikey.effectlib.Effect; @@ -684,6 +685,7 @@ protected void initializeModifiers() { /** * This method is called immediately after all spells have been loaded. */ + @MustBeInvokedByOverriders protected void initialize() { // Process shared cooldowns List rawSharedCooldowns = config.getList(internalKey + "shared-cooldowns", null); @@ -1236,7 +1238,7 @@ public void sendMessages(LivingEntity caster, String[] args) { public void sendMessages(SpellData data, String... replacements) { sendMessage(strCastSelf, data.caster(), data, replacements); sendMessage(strCastTarget, data.target(), data, replacements); - sendMessageNear(strCastOthers, data, broadcastRange.get(data), replacements); + sendMessageNear(strCastOthers, data, replacements); } protected boolean preCastTimeCheck(LivingEntity livingEntity, String[] args) { @@ -2296,8 +2298,7 @@ protected void sendMessage(String message, LivingEntity recipient, SpellData dat */ @Deprecated protected void sendMessageNear(LivingEntity livingEntity, String message) { - SpellData data = new SpellData(livingEntity); - sendMessageNear(message, data, broadcastRange.get(data)); + sendMessageNear(message, new SpellData(livingEntity)); } /** @@ -2325,6 +2326,17 @@ protected void sendMessageNear(LivingEntity livingEntity, Player ignore, String sendMessageNear(message, new SpellData(livingEntity, ignore, 1f, args), range, replacements); } + /** + * Sends a message to all players near the specified player, within the default broadcast range. + * + * @param message the message to send + * @param data the associated spell data + * @param replacements replacements to be done on message + */ + protected void sendMessageNear(String message, SpellData data, String... replacements) { + sendMessageNear(message, data, broadcastRange.get(data), replacements); + } + /** * Sends a message to all players near the specified player, within the specified broadcast range. * diff --git a/core/src/main/java/com/nisovin/magicspells/spells/instant/TimeSpell.java b/core/src/main/java/com/nisovin/magicspells/spells/instant/TimeSpell.java index 9f142d43c..596b004a0 100644 --- a/core/src/main/java/com/nisovin/magicspells/spells/instant/TimeSpell.java +++ b/core/src/main/java/com/nisovin/magicspells/spells/instant/TimeSpell.java @@ -1,44 +1,74 @@ package com.nisovin.magicspells.spells.instant; import org.bukkit.World; +import org.bukkit.GameRules; import org.bukkit.entity.Player; +import net.kyori.adventure.util.TriState; + import com.nisovin.magicspells.util.SpellData; import com.nisovin.magicspells.util.CastResult; import com.nisovin.magicspells.util.MagicConfig; import com.nisovin.magicspells.spells.InstantSpell; import com.nisovin.magicspells.util.config.ConfigData; +import com.nisovin.magicspells.spells.TargetedEntitySpell; import com.nisovin.magicspells.spells.TargetedLocationSpell; -public class TimeSpell extends InstantSpell implements TargetedLocationSpell { +public class TimeSpell extends InstantSpell implements TargetedEntitySpell, TargetedLocationSpell { + + private final ConfigData time; + + private final ConfigData addTime; + private final ConfigData setPlayerTime; - private final ConfigData timeToSet; + private final ConfigData advance; private String strAnnounce; - + public TimeSpell(MagicConfig config, String spellName) { super(config, spellName); - - timeToSet = getConfigDataInt("time-to-set", 0); - strAnnounce = getConfigString("str-announce", "The sun suddenly appears in the sky."); + + time = getConfigDataInt("time", getConfigDataInt("time-to-set", 0)); + + addTime = getConfigDataBoolean("add-time", false); + setPlayerTime = getConfigDataBoolean("set-player-time", false); + + advance = getConfigDataEnum("advance", TriState.class, TriState.NOT_SET); + + strAnnounce = getConfigString("str-announce", ""); } @Override public CastResult cast(SpellData data) { - setTime(data.caster().getWorld(), data); - return new CastResult(PostCastAction.HANDLE_NORMALLY, data); + return setTime(data.caster().getWorld(), data.target(data.caster())); + } + + @Override + public CastResult castAtEntity(SpellData data) { + return setTime(data.target().getWorld(), data); } @Override public CastResult castAtLocation(SpellData data) { - setTime(data.location().getWorld(), data); - return new CastResult(PostCastAction.HANDLE_NORMALLY, data); + return setTime(data.location().getWorld(), data); } - private void setTime(World world, SpellData data) { - world.setTime(timeToSet.get(data)); - for (Player p : world.getPlayers()) sendMessage(strAnnounce, p, data); + private CastResult setTime(World world, SpellData data) { + long time = this.time.get(data); + if (addTime.get(data)) time += world.getTime(); + + if (setPlayerTime.get(data)) { + if (!(data.target() instanceof Player player)) return noTarget(data); + player.setPlayerTime(time, true); // Reset with "time: 0". + } else world.setTime(time); + + Boolean advance = this.advance.get(data).toBoolean(); + if (advance != null) world.setGameRule(GameRules.ADVANCE_TIME, advance); + + sendMessageNear(strAnnounce, data); playSpellEffects(data); + + return new CastResult(PostCastAction.HANDLE_NORMALLY, data); } public String getStrAnnounce() { diff --git a/core/src/main/java/com/nisovin/magicspells/spells/instant/WeatherSpell.java b/core/src/main/java/com/nisovin/magicspells/spells/instant/WeatherSpell.java new file mode 100644 index 000000000..4b5735bec --- /dev/null +++ b/core/src/main/java/com/nisovin/magicspells/spells/instant/WeatherSpell.java @@ -0,0 +1,100 @@ +package com.nisovin.magicspells.spells.instant; + +import org.bukkit.World; +import org.bukkit.GameRules; +import org.bukkit.WeatherType; +import org.bukkit.entity.Player; + +import net.kyori.adventure.util.TriState; + +import com.nisovin.magicspells.util.SpellData; +import com.nisovin.magicspells.util.CastResult; +import com.nisovin.magicspells.util.MagicConfig; +import com.nisovin.magicspells.spells.InstantSpell; +import com.nisovin.magicspells.util.config.ConfigData; +import com.nisovin.magicspells.spells.TargetedEntitySpell; +import com.nisovin.magicspells.spells.TargetedLocationSpell; + +public class WeatherSpell extends InstantSpell implements TargetedEntitySpell, TargetedLocationSpell { + + private final ConfigData rain; + private final ConfigData thunder; + private final ConfigData advance; + + private final ConfigData durationClear; + private final ConfigData durationWeather; + private final ConfigData durationThunder; + + private final ConfigData playerWeather; + + public WeatherSpell(MagicConfig config, String spellName) { + super(config, spellName); + + rain = getConfigDataEnum("rain", TriState.class, TriState.NOT_SET); + thunder = getConfigDataEnum("thunder", TriState.class, TriState.NOT_SET); + advance = getConfigDataEnum("advance", TriState.class, TriState.NOT_SET); + + durationClear = getConfigDataInt("duration-clear", -1); + durationWeather = getConfigDataInt("duration-weather", -1); + durationThunder = getConfigDataInt("duration-thunder", -1); + + playerWeather = getConfigDataEnum("player-weather", PlayerWeather.class, null); + } + + @Override + public CastResult cast(SpellData data) { + return weather(data.caster().getWorld(), data.target(data.caster())); + } + + @Override + public CastResult castAtEntity(SpellData data) { + return weather(data.target().getWorld(), data); + } + + @Override + public CastResult castAtLocation(SpellData data) { + return weather(data.location().getWorld(), data); + } + + private CastResult weather(World world, SpellData data) { + PlayerWeather playerWeather = this.playerWeather.get(data); + + if (playerWeather == null) { + Boolean rain = this.rain.get(data).toBoolean(); + if (rain != null) world.setStorm(rain); + + Boolean thunder = this.thunder.get(data).toBoolean(); + if (thunder != null) world.setThundering(thunder); + + Boolean advance = this.advance.get(data).toBoolean(); + if (advance != null) world.setGameRule(GameRules.ADVANCE_WEATHER, advance); + + int durationWeather = this.durationWeather.get(data); + if (durationWeather >= 0) world.setWeatherDuration(durationWeather); + + int durationThunder = this.durationThunder.get(data); + if (durationThunder >= 0) world.setThunderDuration(durationThunder); + + int durationClear = this.durationClear.get(data); + if (durationClear >= 0) world.setClearWeatherDuration(durationClear); + } else { + if (!(data.target() instanceof Player player)) return noTarget(data); + + switch (playerWeather) { + case CLEAR -> player.setPlayerWeather(WeatherType.CLEAR); + case DOWNFALL -> player.setPlayerWeather(WeatherType.DOWNFALL); + case RESET -> player.resetPlayerWeather(); + } + } + + playSpellEffects(data); + return new CastResult(PostCastAction.HANDLE_NORMALLY, data); + } + + private enum PlayerWeather { + CLEAR, + DOWNFALL, + RESET, + } + +} diff --git a/core/src/main/java/com/nisovin/magicspells/spells/targeted/PasteSpell.java b/core/src/main/java/com/nisovin/magicspells/spells/targeted/PasteSpell.java index 47b5965d6..119518541 100644 --- a/core/src/main/java/com/nisovin/magicspells/spells/targeted/PasteSpell.java +++ b/core/src/main/java/com/nisovin/magicspells/spells/targeted/PasteSpell.java @@ -6,7 +6,10 @@ import java.io.IOException; import java.io.FileInputStream; +import org.bukkit.World; +import org.bukkit.Material; import org.bukkit.Location; +import org.bukkit.block.Block; import com.sk89q.worldedit.WorldEdit; import com.sk89q.worldedit.EditSession; @@ -30,7 +33,7 @@ public class PasteSpell extends TargetedSpell implements TargetedLocationSpell { - private final List sessions; + private final List sessions = new ArrayList<>(); private Clipboard clipboard; @@ -40,8 +43,11 @@ public class PasteSpell extends TargetedSpell implements TargetedLocationSpell { private final ConfigData undoDelay; private final ConfigData pasteAir; + private final ConfigData pasteStructureVoid; + private final ConfigData removePaste; private final ConfigData pasteAtCaster; + private final ConfigData preventOverwrite; public PasteSpell(MagicConfig config, String spellName) { super(config, spellName); @@ -56,10 +62,11 @@ public PasteSpell(MagicConfig config, String spellName) { undoDelay = getConfigDataInt("undo-delay", 0); pasteAir = getConfigDataBoolean("paste-air", false); + pasteStructureVoid = getConfigDataBoolean("paste-structure-void", false); + removePaste = getConfigDataBoolean("remove-paste", true); pasteAtCaster = getConfigDataBoolean("paste-at-caster", false); - - sessions = new ArrayList<>(); + preventOverwrite = getConfigDataBoolean("prevent-overwrite", false); } @Override @@ -110,11 +117,35 @@ public CastResult castAtLocation(SpellData data) { target.add(0, yOffset.get(data), 0); data = data.location(target); - try (EditSession editSession = WorldEdit.getInstance().newEditSession(BukkitAdapter.adapt(target.getWorld()))) { + World world = target.getWorld(); + BlockVector3 pasteTo = BukkitAdapter.asBlockVector(target); + + boolean ignoreAir = !pasteAir.get(data); + boolean ignoreStructureVoid = !pasteStructureVoid.get(data); + + if (preventOverwrite.get(data)) { + BlockVector3 offset = pasteTo.subtract(clipboard.getOrigin()); + + for (BlockVector3 pos : clipboard.getRegion()) { + BlockVector3 worldPos = pos.add(offset); + Block origin = world.getBlockAt(worldPos.x(), worldPos.y(), worldPos.z()); + if (origin.getType().isAir()) continue; + + Material place = BukkitAdapter.adapt(clipboard.getFullBlock(pos).getBlockType()); + + if (ignoreAir && place.isAir()) continue; + if (ignoreStructureVoid && place == Material.STRUCTURE_VOID) continue; + + return noTarget(data); + } + } + + try (EditSession editSession = WorldEdit.getInstance().newEditSession(BukkitAdapter.adapt(world))) { Operation operation = new ClipboardHolder(clipboard) .createPaste(editSession) - .to(BlockVector3.at(target.getX(), target.getY(), target.getZ())) - .ignoreAirBlocks(!pasteAir.get(data)) + .to(pasteTo) + .ignoreAirBlocks(ignoreAir) + .ignoreStructureVoidBlocks(ignoreStructureVoid) .build(); Operations.complete(operation);