mirror of
https://github.com/DRE2N/DungeonsXL.git
synced 2024-11-24 19:45:43 +01:00
Add basic Parties group adapter
This commit is contained in:
parent
74ede6d392
commit
9727137675
@ -14,10 +14,16 @@
|
||||
*/
|
||||
package de.erethon.dungeonsxl.api.player;
|
||||
|
||||
import de.erethon.dungeonsxl.api.DungeonsAPI;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
/**
|
||||
* Implement and register in order to track a group.
|
||||
* <p>
|
||||
* See implementation classes in de.erethon.dungeonsxl.player.groupadapter for reference.
|
||||
*
|
||||
* @param <T> the external group object
|
||||
* @author Daniel Saukel
|
||||
@ -44,12 +50,50 @@ public abstract class GroupAdapter<T> {
|
||||
ONLINE
|
||||
}
|
||||
|
||||
public class ExternalGroupData<T> {
|
||||
|
||||
private T eGroup;
|
||||
private boolean createdByDXL;
|
||||
|
||||
public ExternalGroupData(T eGroup, boolean createdByDXL) {
|
||||
this.eGroup = eGroup;
|
||||
this.createdByDXL = createdByDXL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the wrapped external group object.
|
||||
*
|
||||
* @return the wrapped external group object
|
||||
*/
|
||||
public T get() {
|
||||
return eGroup;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if the external group was created by DungeonsXL.
|
||||
* <p>
|
||||
* Groups may be created by DungeonsXL, for example through a command, a group sign or automatically if a dungeon is entered.
|
||||
* The integration implementation should give dungeon groups equivalent groups from the external group plugin.
|
||||
* External groups created to mirror dungeon groups should be removed when their dungeon group is deleted, but those created intentionally should not.
|
||||
*
|
||||
* @return if the external group was created by DungeonsXL.
|
||||
*/
|
||||
public boolean isCreatedByDXL() {
|
||||
return createdByDXL;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
protected DungeonsAPI dxl;
|
||||
private Philosophy philosophy;
|
||||
protected Map<PlayerGroup, ExternalGroupData<T>> groups = new HashMap<>();
|
||||
|
||||
/**
|
||||
* @param dxl the DungeonsAPI instance
|
||||
* @param philosophy the player handling philosophy
|
||||
*/
|
||||
protected GroupAdapter(Philosophy philosophy) {
|
||||
protected GroupAdapter(DungeonsAPI dxl, Philosophy philosophy) {
|
||||
this.dxl = dxl;
|
||||
this.philosophy = philosophy;
|
||||
}
|
||||
|
||||
@ -68,11 +112,10 @@ public abstract class GroupAdapter<T> {
|
||||
* @param eGroup the external group
|
||||
* @return a dungeon group {@link #areCorresponding(PlayerGroup, Object) corresponding} with the external group
|
||||
*/
|
||||
public abstract PlayerGroup createPlayerGroup(T eGroup);
|
||||
public abstract PlayerGroup createDungeonGroup(T eGroup);
|
||||
|
||||
/**
|
||||
* Creates an external group {@link #areCorresponding(PlayerGroup, Object) corresponding} with the dungeon
|
||||
* group.
|
||||
* Creates an external group {@link #areCorresponding(PlayerGroup, Object) corresponding} with the dungeon group.
|
||||
*
|
||||
* @param dGroup the dungeon group
|
||||
* @return an external group {@link #areCorresponding(PlayerGroup, Object) corresponding} with the dungeon group
|
||||
@ -80,24 +123,30 @@ public abstract class GroupAdapter<T> {
|
||||
public abstract T createExternalGroup(PlayerGroup dGroup);
|
||||
|
||||
/**
|
||||
* Returns the dungeon group {@link #areCorresponding(PlayerGroup, Object) corresponding} with the external
|
||||
* group or null of none exists.
|
||||
* Returns the dungeon group {@link #areCorresponding(PlayerGroup, Object) corresponding} with the external group or null of none exists.
|
||||
*
|
||||
* @param eGroup the external group
|
||||
* @return the dungeon group {@link #areCorresponding(PlayerGroup, Object) corresponding} with the external
|
||||
* group
|
||||
* @return the dungeon group {@link #areCorresponding(PlayerGroup, Object) corresponding} with the external group
|
||||
*/
|
||||
public abstract PlayerGroup getPlayerGroup(T eGroup);
|
||||
public PlayerGroup getDungeonGroup(T eGroup) {
|
||||
for (Entry<PlayerGroup, ExternalGroupData<T>> entry : groups.entrySet()) {
|
||||
if (entry.getValue().get().equals(eGroup)) {
|
||||
return entry.getKey();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the external group {@link #areCorresponding(PlayerGroup, Object) corresponding} with the dungeon
|
||||
* group.
|
||||
* Returns the external group {@link #areCorresponding(PlayerGroup, Object) corresponding} with the dungeon group.
|
||||
*
|
||||
* @param dGroup the dungeon group
|
||||
* @return the external group {@link #areCorresponding(PlayerGroup, Object) corresponding} with the dungeon
|
||||
* group
|
||||
* @return the external group {@link #areCorresponding(PlayerGroup, Object) corresponding} with the dungeon group
|
||||
*/
|
||||
public abstract T getExternalGroup(PlayerGroup dGroup);
|
||||
public T getExternalGroup(PlayerGroup dGroup) {
|
||||
ExternalGroupData<T> data = groups.get(dGroup);
|
||||
return data != null ? data.get() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the dungeon group that mirrors the external group.
|
||||
@ -107,10 +156,10 @@ public abstract class GroupAdapter<T> {
|
||||
* @param eGroup the dungeon group
|
||||
* @return the dungeon group that mirrors the dungeon group
|
||||
*/
|
||||
public PlayerGroup getOrCreatePlayerGroup(T eGroup) {
|
||||
PlayerGroup dGroup = getPlayerGroup(eGroup);
|
||||
public PlayerGroup getOrCreateDungeonGroup(T eGroup) {
|
||||
PlayerGroup dGroup = getDungeonGroup(eGroup);
|
||||
if (dGroup == null) {
|
||||
dGroup = createPlayerGroup(eGroup);
|
||||
dGroup = createDungeonGroup(eGroup);
|
||||
}
|
||||
return dGroup;
|
||||
}
|
||||
@ -131,6 +180,14 @@ public abstract class GroupAdapter<T> {
|
||||
return eGroup;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the external group of the given group member.
|
||||
*
|
||||
* @param member the group member
|
||||
* @return the external group of the given group member
|
||||
*/
|
||||
public abstract T getExternalGroup(Player member);
|
||||
|
||||
/**
|
||||
* Checks if two groups are corresponding.
|
||||
* <p>
|
||||
@ -142,7 +199,30 @@ public abstract class GroupAdapter<T> {
|
||||
* @param eGroup the external group
|
||||
* @return if the two groups are corresponding
|
||||
*/
|
||||
public abstract boolean areCorresponding(PlayerGroup dGroup, T eGroup);
|
||||
public boolean areCorresponding(PlayerGroup dGroup, T eGroup) {
|
||||
if (dGroup == null || eGroup == null) {
|
||||
return false;
|
||||
}
|
||||
ExternalGroupData<T> data = groups.get(dGroup);
|
||||
return data != null && eGroup.equals(data.get());
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the external group corresponding with the given dungeon group.
|
||||
*
|
||||
* @param dGroup the dungeon group corresponding with the external one to delete
|
||||
* @return if the deletion was successful
|
||||
*/
|
||||
public abstract boolean deleteCorrespondingGroup(PlayerGroup dGroup);
|
||||
|
||||
/**
|
||||
* Checks if the two groups have the same members.
|
||||
*
|
||||
* @param dGroup the dungeon group
|
||||
* @param eGroup the external group
|
||||
* @return if the two groups have the same members
|
||||
*/
|
||||
public abstract boolean areSimilar(PlayerGroup dGroup, T eGroup);
|
||||
|
||||
/**
|
||||
* Ensures that the player is in {@link #areCorresponding(PlayerGroup, Object) corresponding} groups.
|
||||
@ -151,13 +231,28 @@ public abstract class GroupAdapter<T> {
|
||||
* If no dungeon group exists, it is created automatically. Switching dungeon groups forces the player to leave their dungeon.
|
||||
* <p>
|
||||
* If the player is in a dungeon group but not in an external group, the player is added to the corresponding external group if it exists.
|
||||
* If no corresponding external group exists, a new one is only created if the {@link #getPhilosophy() philosophy} is either
|
||||
* {@link Philosophy#RUNTIME} or {@link Philosophy#ONLINE}.
|
||||
* If no corresponding external group exists, a new one is created.
|
||||
*
|
||||
* @param player the player
|
||||
*/
|
||||
public void syncPlayer(Player player) {
|
||||
throw new UnsupportedOperationException("TODO");
|
||||
T eGroup = getExternalGroup(player);
|
||||
PlayerGroup dGroup = dxl.getPlayerGroup(player);
|
||||
|
||||
if (eGroup != null && !areCorresponding(dGroup, eGroup)) {
|
||||
if (areSimilar(dGroup, eGroup)) {
|
||||
// The groups are not yet marked as corresponding because one of them is still being created.
|
||||
return;
|
||||
}
|
||||
if (dGroup != null) {
|
||||
dGroup.removePlayer(player, false);
|
||||
return;
|
||||
}
|
||||
dGroup = createDungeonGroup(eGroup);
|
||||
|
||||
} else if (eGroup == null && dGroup != null) {
|
||||
createExternalGroup(dGroup);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -89,6 +89,11 @@
|
||||
<version>2.10.2</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.alessiodp.parties</groupId>
|
||||
<artifactId>parties-api</artifactId>
|
||||
<version>2.6.14</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<repositories>
|
||||
<repository>
|
||||
@ -107,5 +112,9 @@
|
||||
<id>placeholderapi</id>
|
||||
<url>http://repo.extendedclip.com/content/repositories/placeholderapi/</url>
|
||||
</repository>
|
||||
<repository>
|
||||
<id>codemc-repo</id>
|
||||
<url>https://repo.codemc.org/repository/maven-public/</url>
|
||||
</repository>
|
||||
</repositories>
|
||||
</project>
|
||||
|
@ -68,6 +68,7 @@ import de.erethon.dungeonsxl.player.DGroup;
|
||||
import de.erethon.dungeonsxl.player.DPermission;
|
||||
import de.erethon.dungeonsxl.player.DPlayerListener;
|
||||
import de.erethon.dungeonsxl.player.SecureModeTask;
|
||||
import de.erethon.dungeonsxl.player.groupadapter.*;
|
||||
import de.erethon.dungeonsxl.requirement.*;
|
||||
import de.erethon.dungeonsxl.reward.*;
|
||||
import de.erethon.dungeonsxl.sign.DSignListener;
|
||||
@ -137,6 +138,7 @@ public class DungeonsXL extends DREPlugin implements DungeonsAPI {
|
||||
private Registry<String, GameRule> gameRuleRegistry = new GameRuleRegistry();
|
||||
private Registry<String, ExternalMobProvider> externalMobProviderRegistry = new Registry<>();
|
||||
private Registry<String, PlayerGroup> playerGroupCache = new Registry<>();
|
||||
private Collection<GroupAdapter> groupAdapters = new ArrayList<>();
|
||||
|
||||
@Deprecated
|
||||
private class SignRegistry extends Registry<String, Class<? extends DungeonSign>> {
|
||||
@ -213,6 +215,9 @@ public class DungeonsXL extends DREPlugin implements DungeonsAPI {
|
||||
if (manager.isPluginEnabled("PlaceholderAPI")) {
|
||||
new PlaceholderUtil(this, "dxl").register();
|
||||
}
|
||||
if (manager.isPluginEnabled("Parties")) {
|
||||
registerGroupAdapter(new PartiesAdapter(this));
|
||||
}
|
||||
VignetteAPI.init(this);
|
||||
}
|
||||
|
||||
@ -530,7 +535,16 @@ public class DungeonsXL extends DREPlugin implements DungeonsAPI {
|
||||
|
||||
@Override
|
||||
public void registerGroupAdapter(GroupAdapter groupAdapter) {
|
||||
throw new UnsupportedOperationException("Not supported yet.");
|
||||
groupAdapters.add(groupAdapter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a collection of the loadedGroupAdapters
|
||||
*
|
||||
* @return a collection of GroupAdapters
|
||||
*/
|
||||
public Collection<GroupAdapter> getGroupAdapters() {
|
||||
return groupAdapters;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -107,6 +107,8 @@ public class DGroup implements PlayerGroup {
|
||||
floorCount = 0;
|
||||
|
||||
id = counter++;
|
||||
|
||||
plugin.getGroupAdapters().forEach(a -> a.syncPlayer(player));
|
||||
}
|
||||
|
||||
public DGroup(DungeonsXL plugin, Player player, Dungeon dungeon) {
|
||||
@ -143,6 +145,8 @@ public class DGroup implements PlayerGroup {
|
||||
floorCount = 0;
|
||||
|
||||
id = counter++;
|
||||
|
||||
plugin.getGroupAdapters().forEach(a -> a.syncPlayer(captain));
|
||||
}
|
||||
|
||||
// Getters and setters
|
||||
@ -216,6 +220,8 @@ public class DGroup implements PlayerGroup {
|
||||
|
||||
players.add(player.getUniqueId());
|
||||
}
|
||||
|
||||
plugin.getGroupAdapters().forEach(a -> a.syncPlayer(player));
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -238,8 +244,11 @@ public class DGroup implements PlayerGroup {
|
||||
|
||||
if (!event.isCancelled()) {
|
||||
delete();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
plugin.getGroupAdapters().forEach(a -> a.syncPlayer(player));
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -662,6 +671,8 @@ public class DGroup implements PlayerGroup {
|
||||
}
|
||||
|
||||
plugin.getGlobalProtectionCache().updateGroupSigns(this);
|
||||
|
||||
plugin.getGroupAdapters().forEach(a -> a.deleteCorrespondingGroup(this));
|
||||
}
|
||||
|
||||
public boolean startGame(Game game) {
|
||||
|
@ -0,0 +1,193 @@
|
||||
/*
|
||||
* Copyright (C) 2012-2020 Frank Baumann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package de.erethon.dungeonsxl.player.groupadapter;
|
||||
|
||||
import com.alessiodp.parties.api.Parties;
|
||||
import com.alessiodp.parties.api.events.bukkit.party.BukkitPartiesPartyPostCreateEvent;
|
||||
import com.alessiodp.parties.api.events.bukkit.party.BukkitPartiesPartyPreDeleteEvent;
|
||||
import com.alessiodp.parties.api.events.bukkit.party.BukkitPartiesPartyRenameEvent;
|
||||
import com.alessiodp.parties.api.events.bukkit.player.BukkitPartiesPlayerPostJoinEvent;
|
||||
import com.alessiodp.parties.api.events.bukkit.player.BukkitPartiesPlayerPostLeaveEvent;
|
||||
import com.alessiodp.parties.api.interfaces.PartiesAPI;
|
||||
import com.alessiodp.parties.api.interfaces.Party;
|
||||
import com.alessiodp.parties.api.interfaces.PartyPlayer;
|
||||
import de.erethon.commons.chat.MessageUtil;
|
||||
import de.erethon.dungeonsxl.api.DungeonsAPI;
|
||||
import de.erethon.dungeonsxl.api.player.GroupAdapter;
|
||||
import de.erethon.dungeonsxl.api.player.PlayerGroup;
|
||||
import de.erethon.dungeonsxl.config.DMessage;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.UUID;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.scheduler.BukkitRunnable;
|
||||
|
||||
/**
|
||||
* This class may be used as a reference for implementations of the GroupAdapter API.
|
||||
*
|
||||
* @author Daniel Saukel
|
||||
*/
|
||||
public class PartiesAdapter extends GroupAdapter<Party> implements Listener {
|
||||
|
||||
private PartiesAPI partiesAPI;
|
||||
|
||||
public PartiesAdapter(DungeonsAPI api) {
|
||||
super(api, Philosophy.PERSISTENT);
|
||||
Bukkit.getPluginManager().registerEvents(this, api);
|
||||
partiesAPI = Parties.getApi();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Party createExternalGroup(PlayerGroup dGroup) {
|
||||
if (!partiesAPI.createParty(dGroup.getRawName(), partiesAPI.getPartyPlayer(dGroup.getLeader().getUniqueId()))) {
|
||||
return null;
|
||||
}
|
||||
Party eGroup = partiesAPI.getParty(dGroup.getRawName());
|
||||
groups.put(dGroup, new ExternalGroupData<>(eGroup, true));
|
||||
return eGroup;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlayerGroup createDungeonGroup(Party eGroup) {
|
||||
PlayerGroup dGroup = dxl.createGroup(Bukkit.getPlayer(eGroup.getLeader()), eGroup.getName());
|
||||
eGroup.getMembers().forEach(uuid -> dGroup.addPlayer(Bukkit.getPlayer(uuid), false));
|
||||
groups.put(dGroup, new ExternalGroupData<>(eGroup, false));
|
||||
return dGroup;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Party getExternalGroup(Player member) {
|
||||
PartyPlayer pPlayer = getPartyPlayer(member);
|
||||
if (pPlayer == null) {
|
||||
return null;
|
||||
}
|
||||
return partiesAPI.getParty(pPlayer.getPartyName());
|
||||
}
|
||||
|
||||
public boolean addExternalGroupMember(Party eGroup, Player member) {
|
||||
return eGroup.addMember(getPartyPlayer(member));
|
||||
}
|
||||
|
||||
public boolean removeExternalGroupMember(Player member) {
|
||||
PartyPlayer pPlayer = getPartyPlayer(member);
|
||||
if (pPlayer == null) {
|
||||
return false;
|
||||
}
|
||||
Party eGroup = partiesAPI.getParty(pPlayer.getPartyName());
|
||||
if (eGroup == null) {
|
||||
return false;
|
||||
}
|
||||
eGroup.removeMember(pPlayer);
|
||||
if (eGroup.getMembers().isEmpty()) {
|
||||
eGroup.delete();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean deleteCorrespondingGroup(PlayerGroup dGroup) {
|
||||
ExternalGroupData<Party> data = groups.get(dGroup);
|
||||
if (data == null || !data.isCreatedByDXL()) {
|
||||
return false;
|
||||
}
|
||||
data.get().delete();
|
||||
groups.remove(dGroup);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areSimilar(PlayerGroup dGroup, Party eGroup) {
|
||||
if (dGroup == null || eGroup == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Collection<UUID> members = new ArrayList<>(dGroup.getMembers().getUniqueIds());
|
||||
for (UUID member : eGroup.getMembers()) {
|
||||
if (!members.contains(member)) {
|
||||
return false;
|
||||
}
|
||||
members.remove(member);
|
||||
}
|
||||
return members.isEmpty();
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onCreation(BukkitPartiesPartyPostCreateEvent event) {
|
||||
// Event is called asynchronously
|
||||
new BukkitRunnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
createDungeonGroup(event.getParty());
|
||||
}
|
||||
}.runTask(dxl);
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onDeletion(BukkitPartiesPartyPreDeleteEvent event) {
|
||||
PlayerGroup dGroup = getDungeonGroup(event.getParty());
|
||||
if (dGroup != null) {
|
||||
groups.remove(dGroup); // This avoids circular deleting of groups
|
||||
dGroup.delete();
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onRename(BukkitPartiesPartyRenameEvent event) {
|
||||
PlayerGroup dGroup = getDungeonGroup(event.getParty());
|
||||
if (dGroup != null) {
|
||||
dGroup.delete();
|
||||
}
|
||||
if (dxl.getPlayerGroupCache().get(event.getNewPartyName()) != null) {
|
||||
MessageUtil.sendMessage(getPlayer(event.getPartyPlayer()), DMessage.ERROR_NAME_IN_USE.getMessage(event.getNewPartyName()));
|
||||
event.setCancelled(true);
|
||||
return;
|
||||
}
|
||||
dGroup.setName(event.getNewPartyName());
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onJoin(BukkitPartiesPlayerPostJoinEvent event) {
|
||||
new BukkitRunnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
syncPlayer(getPlayer(event.getPartyPlayer()));
|
||||
}
|
||||
}.runTask(dxl);
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onLeave(BukkitPartiesPlayerPostLeaveEvent event) {
|
||||
new BukkitRunnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
syncPlayer(getPlayer(event.getPartyPlayer()));
|
||||
}
|
||||
}.runTask(dxl);
|
||||
}
|
||||
|
||||
private Player getPlayer(PartyPlayer player) {
|
||||
return Bukkit.getPlayer(player.getPlayerUUID());
|
||||
}
|
||||
|
||||
private PartyPlayer getPartyPlayer(Player player) {
|
||||
return partiesAPI.getPartyPlayer(player.getUniqueId());
|
||||
}
|
||||
|
||||
}
|
@ -4,7 +4,7 @@ version: ${project.parent.version}${buildNo}
|
||||
authors: [Frank Baumann, Milan Albrecht, Tobias Schmitz, Daniel Saukel]
|
||||
description: ${project.parent.description}
|
||||
website: ${project.parent.url}
|
||||
softdepend: [CommandsXL, ItemsXL, Vault, Citizens, CustomMobs, InsaneMobs, MythicMobs, HolographicDisplays, LWC, PlaceholderAPI]
|
||||
softdepend: [CommandsXL, ItemsXL, Vault, Citizens, CustomMobs, InsaneMobs, MythicMobs, HolographicDisplays, LWC, PlaceholderAPI, Parties]
|
||||
commands:
|
||||
dungeonsxl:
|
||||
description: Reference command for DungeonsXL.
|
||||
|
Loading…
Reference in New Issue
Block a user