Various improvements to the interface
More time units are supported, which makes long temporary durations more readable Makes right-clicking a sign display a full description, and requires sneaking while clicking to perform the transaction Makes signs only be destroyed when sneaking Improves sign protection a bit, but improvements are required Prevents a creative player breaking a sign from also interacting with it
This commit is contained in:
parent
bec45ae968
commit
23c023a027
@ -33,6 +33,9 @@ import java.util.Queue;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* The main permissionsigns class
|
||||
*/
|
||||
public final class PermissionSigns extends JavaPlugin {
|
||||
|
||||
private static final Queue<SignCreationRequest> signCreationRequests = new PriorityQueue<>();
|
||||
@ -131,6 +134,7 @@ public final class PermissionSigns extends JavaPlugin {
|
||||
PluginDescriptionFile pluginDescriptionFile = this.getDescription();
|
||||
pluginVersion = pluginDescriptionFile.getVersion();
|
||||
//TODO: Allow for custom language files. Perhaps just look for strings.yml in the folder
|
||||
//TODO: Display a notice in the console if a new version is available
|
||||
|
||||
FileConfiguration config = this.getConfig();
|
||||
config.options().copyDefaults(true);
|
||||
|
@ -148,11 +148,34 @@ public class PermissionSign {
|
||||
*
|
||||
* @return <p>The string used for displaying this sign's duration</p>
|
||||
*/
|
||||
private String getDurationString() {
|
||||
public String getDurationString() {
|
||||
if (duration == 0) {
|
||||
return Translator.getTranslatedMessage(TranslatableMessage.SIGN_PERMANENT);
|
||||
} else {
|
||||
return duration + " " + Translator.getTranslatedMessage(TranslatableMessage.SIGN_TIME_UNIT);
|
||||
double minute = 60;
|
||||
double hour = minute * 60;
|
||||
double day = hour * 24;
|
||||
if (duration / day >= 1) {
|
||||
double days = round(duration / day);
|
||||
if (days == 1) {
|
||||
return (int) days + " " + Translator.getTranslatedMessage(TranslatableMessage.UNIT_DAY);
|
||||
} else {
|
||||
return days + " " + Translator.getTranslatedMessage(TranslatableMessage.UNIT_DAYS);
|
||||
}
|
||||
} else if (duration / hour >= 1) {
|
||||
double hours = round(duration / hour);
|
||||
if (hours == 1) {
|
||||
return (int) hours + " " + Translator.getTranslatedMessage(TranslatableMessage.UNIT_HOUR);
|
||||
} else {
|
||||
return hours + " " + Translator.getTranslatedMessage(TranslatableMessage.UNIT_HOURS);
|
||||
}
|
||||
} else {
|
||||
if (duration == 1) {
|
||||
return duration + " " + Translator.getTranslatedMessage(TranslatableMessage.UNIT_SECOND);
|
||||
} else {
|
||||
return duration + " " + Translator.getTranslatedMessage(TranslatableMessage.UNIT_SECONDS);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -161,7 +184,7 @@ public class PermissionSign {
|
||||
*
|
||||
* @return <p>The string used for displaying this sign's cost</p>
|
||||
*/
|
||||
private String getCostString() {
|
||||
public String getCostString() {
|
||||
if (cost == 0) {
|
||||
return Translator.getTranslatedMessage(TranslatableMessage.SIGN_COST_FREE);
|
||||
} else {
|
||||
@ -170,4 +193,14 @@ public class PermissionSign {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rounds a number to its last two digits
|
||||
*
|
||||
* @param number <p>The number to round</p>
|
||||
* @return <p>The rounded number</p>
|
||||
*/
|
||||
private double round(double number) {
|
||||
return Math.round(number * 100.0) / 100.0;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -16,9 +16,44 @@ public enum TranslatableMessage {
|
||||
SIGN_PREFIX,
|
||||
|
||||
/**
|
||||
* The text to display as a permission sign's duration time unit
|
||||
* The text to display for 1 second
|
||||
*/
|
||||
SIGN_TIME_UNIT,
|
||||
UNIT_SECOND,
|
||||
|
||||
/**
|
||||
* The text to display for a number of seconds
|
||||
*/
|
||||
UNIT_SECONDS,
|
||||
|
||||
/**
|
||||
* The text to display for 1 minute
|
||||
*/
|
||||
UNIT_MINUTE,
|
||||
|
||||
/**
|
||||
* The text to display for a number of minutes
|
||||
*/
|
||||
UNIT_MINUTES,
|
||||
|
||||
/**
|
||||
* The text to display for 1 hour
|
||||
*/
|
||||
UNIT_HOUR,
|
||||
|
||||
/**
|
||||
* The text to display for a number of hours
|
||||
*/
|
||||
UNIT_HOURS,
|
||||
|
||||
/**
|
||||
* The text to display for 1 day
|
||||
*/
|
||||
UNIT_DAY,
|
||||
|
||||
/**
|
||||
* The text to display for a number of days
|
||||
*/
|
||||
UNIT_DAYS,
|
||||
|
||||
/**
|
||||
* The text to display on a permission sign that is entirely free
|
||||
@ -50,6 +85,11 @@ public enum TranslatableMessage {
|
||||
*/
|
||||
PERMISSION_SIGN_DESTROY_DENY,
|
||||
|
||||
/**
|
||||
* The message to display to a non-sneaking player trying to destroy a permissions sign
|
||||
*/
|
||||
PERMISSION_SIGN_DESTROY_SNEAK,
|
||||
|
||||
/**
|
||||
* The error message to display if the user has not provided necessary information to create a permission sign
|
||||
*/
|
||||
@ -113,6 +153,11 @@ public enum TranslatableMessage {
|
||||
/**
|
||||
* The message to display when a reload action has been successfully performed
|
||||
*/
|
||||
RELOAD_SUCCESSFUL
|
||||
RELOAD_SUCCESSFUL,
|
||||
|
||||
/**
|
||||
* The text describing all properties of a permission sign
|
||||
*/
|
||||
SIGN_INFO
|
||||
|
||||
}
|
||||
|
@ -6,7 +6,6 @@ import org.bukkit.configuration.file.YamlConfiguration;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.logging.Level;
|
||||
@ -57,7 +56,7 @@ public final class Translator {
|
||||
BufferedReader reader;
|
||||
try {
|
||||
reader = FileHelper.getBufferedReaderForInternalFile("/strings.yml");
|
||||
} catch (FileNotFoundException | UnsupportedEncodingException e) {
|
||||
} catch (FileNotFoundException e) {
|
||||
PermissionSigns.getInstance().getLogger().log(Level.SEVERE, "Unable to load translated messages");
|
||||
return null;
|
||||
}
|
||||
|
@ -6,12 +6,19 @@ import net.knarcraft.permissionsigns.manager.SignManager;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.Tag;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.BlockFace;
|
||||
import org.bukkit.block.Sign;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.block.BlockBreakEvent;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A listener for relevant block events such as signs being broken
|
||||
*/
|
||||
public class BlockListener implements Listener {
|
||||
|
||||
@EventHandler
|
||||
@ -23,11 +30,60 @@ public class BlockListener implements Listener {
|
||||
return;
|
||||
}
|
||||
|
||||
//if (block instanceof FallingBlock) {
|
||||
//TODO: Search recursively upwards (and downwards if pointed dripstone) until a non-falling block is found.
|
||||
// If upwards, check the non-falling block, but not downwards. If player does not have the create permission,
|
||||
// just stop and cancel once a permission sign is found. If it has, all affected signs need to be
|
||||
// de-registered. Must account for stacks of sand and signs on top of each-other. So if a normal sign is
|
||||
// found, the recursion upward must also happen.
|
||||
//}
|
||||
|
||||
Material material = block.getBlockData().getMaterial();
|
||||
if (!Tag.SIGNS.isTagged(material) && !Tag.WALL_SIGNS.isTagged(material)) {
|
||||
return;
|
||||
if (Tag.SIGNS.isTagged(material) || Tag.WALL_SIGNS.isTagged(material)) {
|
||||
checkIfBlockIsPermissionSign(block, player, event);
|
||||
if (event.isCancelled()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Block relativeBlock = block.getRelative(BlockFace.UP);
|
||||
if (Tag.SIGNS.isTagged(relativeBlock.getBlockData().getMaterial())) {
|
||||
checkIfBlockIsPermissionSign(relativeBlock, player, event);
|
||||
if (event.isCancelled()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (block.getBlockData().getMaterial() == Material.POINTED_DRIPSTONE) {
|
||||
relativeBlock = block.getRelative(BlockFace.DOWN);
|
||||
if (Tag.WALL_SIGNS.isTagged(relativeBlock.getBlockData().getMaterial())) {
|
||||
checkIfBlockIsPermissionSign(relativeBlock, player, event);
|
||||
if (event.isCancelled()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (BlockFace blockFace : getRelevantBlockFaces()) {
|
||||
relativeBlock = block.getRelative(blockFace);
|
||||
if (Tag.WALL_SIGNS.isTagged(relativeBlock.getBlockData().getMaterial())) {
|
||||
checkIfBlockIsPermissionSign(relativeBlock, player, event);
|
||||
if (event.isCancelled()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//TODO: Need to protect against other things that might damage the sign, such as explosions
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given block is a permission sign, and perform necessary tasks if it is
|
||||
*
|
||||
* @param block <p>The block to check</p>
|
||||
* @param player <p>The player that caused the event</p>
|
||||
* @param event <p>The triggered block break event</p>
|
||||
*/
|
||||
private void checkIfBlockIsPermissionSign(Block block, Player player, BlockBreakEvent event) {
|
||||
Sign sign = (Sign) block.getState();
|
||||
boolean registered = SignManager.getSign(sign.getLocation()) != null;
|
||||
if (!registered) {
|
||||
@ -37,12 +93,28 @@ public class BlockListener implements Listener {
|
||||
event.setCancelled(true);
|
||||
player.sendMessage(StringFormatter.getTranslatedErrorMessage(TranslatableMessage.PERMISSION_SIGN_DESTROY_DENY));
|
||||
} else {
|
||||
if (!player.isSneaking()) {
|
||||
event.setCancelled(true);
|
||||
player.sendMessage(StringFormatter.getTranslatedErrorMessage(TranslatableMessage.PERMISSION_SIGN_DESTROY_SNEAK));
|
||||
return;
|
||||
}
|
||||
SignManager.removeSign(sign.getLocation());
|
||||
player.sendMessage(StringFormatter.getTranslatedInfoMessage(TranslatableMessage.PERMISSION_SIGN_REMOVED));
|
||||
}
|
||||
}
|
||||
|
||||
//TODO: Need to protect against other things that might damage the sign, such as explosions
|
||||
//TODO: Only allow the sign to be broken if shift-clicking?
|
||||
/**
|
||||
* Gets the relevant block faces for checking around a block
|
||||
*
|
||||
* @return <p>The relevant block faces for checking around a block</p>
|
||||
*/
|
||||
private List<BlockFace> getRelevantBlockFaces() {
|
||||
List<BlockFace> relevantBlockFaces = new ArrayList<>();
|
||||
relevantBlockFaces.add(BlockFace.WEST);
|
||||
relevantBlockFaces.add(BlockFace.EAST);
|
||||
relevantBlockFaces.add(BlockFace.NORTH);
|
||||
relevantBlockFaces.add(BlockFace.SOUTH);
|
||||
return relevantBlockFaces;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -9,6 +9,8 @@ import net.knarcraft.permissionsigns.formatting.Translator;
|
||||
import net.knarcraft.permissionsigns.manager.EconomyManager;
|
||||
import net.knarcraft.permissionsigns.manager.PermissionManager;
|
||||
import net.knarcraft.permissionsigns.manager.SignManager;
|
||||
import net.md_5.bungee.api.ChatColor;
|
||||
import org.bukkit.GameMode;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.Tag;
|
||||
import org.bukkit.block.Block;
|
||||
@ -16,9 +18,11 @@ import org.bukkit.block.Sign;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.block.Action;
|
||||
import org.bukkit.event.player.PlayerInteractEvent;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.StringJoiner;
|
||||
|
||||
/**
|
||||
@ -46,7 +50,7 @@ public class SignListener implements Listener {
|
||||
}
|
||||
|
||||
Sign sign = (Sign) block.getState();
|
||||
handleSignClick(sign, player);
|
||||
handleSignClick(sign, player, event.getAction());
|
||||
}
|
||||
|
||||
/**
|
||||
@ -54,19 +58,26 @@ public class SignListener implements Listener {
|
||||
*
|
||||
* @param sign <p>The clicked sign</p>
|
||||
* @param player <p>The player that clicked the sign</p>
|
||||
* @param action <p>The hand the player used to interact with the sign</p>
|
||||
*/
|
||||
private void handleSignClick(Sign sign, Player player) {
|
||||
private void handleSignClick(Sign sign, Player player, Action action) {
|
||||
//Check if the sign is a registered permission sign
|
||||
PermissionSign permissionSign = SignManager.getSign(sign.getLocation());
|
||||
if (permissionSign != null) {
|
||||
if (player.getGameMode() == GameMode.CREATIVE && action == Action.LEFT_CLICK_BLOCK) {
|
||||
return;
|
||||
}
|
||||
if (!player.hasPermission("permissionsigns.use")) {
|
||||
player.sendMessage(StringFormatter.getTranslatedErrorMessage(TranslatableMessage.INTERACT_PERMISSION_MISSING));
|
||||
return;
|
||||
}
|
||||
handlePermissionSignInteract(permissionSign, player);
|
||||
if (player.isSneaking()) {
|
||||
handlePermissionSignInteract(permissionSign, player);
|
||||
} else {
|
||||
player.sendMessage(getSignInfoText(permissionSign));
|
||||
}
|
||||
return;
|
||||
}
|
||||
//TODO: Display information about granted permissions when shift-clicking?
|
||||
|
||||
//Check if the player has a creation request that can be fulfilled
|
||||
SignCreationRequest request = PermissionSigns.getSignCreationRequest(player.getUniqueId());
|
||||
@ -75,6 +86,31 @@ public class SignListener implements Listener {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets information about a sign
|
||||
*
|
||||
* @param sign <p>The sign to get information about</p>
|
||||
* @return <p>The information to display</p>
|
||||
*/
|
||||
private String getSignInfoText(PermissionSign sign) {
|
||||
String rawSignInfo = Translator.getTranslatedMessage(TranslatableMessage.SIGN_INFO);
|
||||
StringBuilder permissionString = new StringBuilder();
|
||||
List<String> permissionNodes = sign.getPermissionNodes();
|
||||
if (permissionNodes.size() == 1) {
|
||||
permissionString = new StringBuilder(permissionNodes.get(0));
|
||||
} else {
|
||||
for (String permissionNode : permissionNodes) {
|
||||
permissionString.append("\n" + "&f| &7- ").append(permissionNode);
|
||||
}
|
||||
permissionString.append("\n");
|
||||
}
|
||||
|
||||
String replacedString = StringFormatter.replacePlaceholders(rawSignInfo, new String[]{"{Name}", "{Permissions}",
|
||||
"{Cost}", "{Duration}"}, new String[]{sign.getName(), permissionString.toString(), sign.getCostString(),
|
||||
sign.getDurationString()});
|
||||
return ChatColor.translateAlternateColorCodes('&', replacedString);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the interaction with a permission sign
|
||||
*
|
||||
@ -137,9 +173,8 @@ public class SignListener implements Listener {
|
||||
String grantedPermissions = permissionsJoiner.toString();
|
||||
if (permissionSign.getDuration() > 0) {
|
||||
String successMessage = StringFormatter.getTranslatedInfoMessage(TranslatableMessage.PERMISSIONS_GRANTED);
|
||||
String timeUnit = Translator.getTranslatedMessage(TranslatableMessage.SIGN_TIME_UNIT);
|
||||
player.sendMessage(StringFormatter.replacePlaceholders(successMessage, new String[]{"{permissions}", "{time}"},
|
||||
new String[]{grantedPermissions, permissionSign.getDuration() + " " + timeUnit}));
|
||||
new String[]{grantedPermissions, permissionSign.getDurationString()}));
|
||||
} else {
|
||||
player.sendMessage(StringFormatter.replacePlaceholder(StringFormatter.getTranslatedInfoMessage(
|
||||
TranslatableMessage.PERMISSIONS_GRANTED_PERMANENTLY), "{permissions}", grantedPermissions));
|
||||
|
@ -4,7 +4,6 @@ import java.io.BufferedReader;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
@ -18,7 +17,7 @@ public class FileHelper {
|
||||
* @return <p>A buffered read for reading the file</p>
|
||||
* @throws FileNotFoundException <p>If unable to get an input stream for the given file</p>
|
||||
*/
|
||||
public static BufferedReader getBufferedReaderForInternalFile(String file) throws FileNotFoundException, UnsupportedEncodingException {
|
||||
public static BufferedReader getBufferedReaderForInternalFile(String file) throws FileNotFoundException {
|
||||
InputStream inputStream = FileHelper.class.getResourceAsStream(file);
|
||||
if (inputStream == null) {
|
||||
throw new FileNotFoundException("Unable to read the given file");
|
||||
|
@ -2,13 +2,21 @@ en:
|
||||
PREFIX: "[PermissionSigns]"
|
||||
SIGN_PREFIX: "&4[PermSign]"
|
||||
MISSING_CREATION_INFO: "&7You must specify a sign name and a comma-separated list of permissions to create a permission sign"
|
||||
SIGN_TIME_UNIT: "seconds"
|
||||
UNIT_SECOND: "second"
|
||||
UNIT_SECONDS: "seconds"
|
||||
UNIT_MINUTE: "minute"
|
||||
UNIT_MINUTES: "minutes"
|
||||
UNIT_HOUR: "hour"
|
||||
UNIT_HOURS: "hours"
|
||||
UNIT_DAY: "day"
|
||||
UNIT_DAYS: "days"
|
||||
SIGN_PERMANENT: "Permanent"
|
||||
SIGN_COST_FREE: "Free"
|
||||
COST_INVALID_NUMBER: "&7The given cost is not a valid number"
|
||||
DURATION_INVALID_NUMBER: "&7The given duration is not a valid number"
|
||||
COMMAND_PLAYER_ONLY: "&7This command is only available to players"
|
||||
PERMISSION_SIGN_DESTROY_DENY: "&7You do not have permissions to delete permissions signs"
|
||||
PERMISSION_SIGN_DESTROY_SNEAK: "&7You must be sneaking to remove a permissions sign"
|
||||
PERMISSION_SIGN_REMOVED: "&7Permissions sign removed"
|
||||
COMMAND_PERMISSION_DENIED: "&7You do not have necessary permissions to perform this command"
|
||||
CREATION_REQUEST_CREATED: "&7Permission Sign request created. Right-click an empty sign within 60 seconds to create the new permission sign"
|
||||
@ -21,17 +29,26 @@ en:
|
||||
INTERACT_PERMISSION_MISSING: "&7You do not have the necessary permissions required to use permission signs"
|
||||
CREATION_REQUEST_CANCELLED: "&7Your last permission sign creation request has been cancelled"
|
||||
RELOAD_SUCCESSFUL: "&7The plugin has been successfully reloaded"
|
||||
SIGN_INFO: "&f---- &4Permission Sign&f ----\n&f| &bName: &7{Name}\n&f| &bPermission(s): &7{Permissions}\n&f| &bCost: &7{Cost}\n&f| &bDuration: &7{Duration}\n&f| &7Right-click the sign while sneaking to buy the permission(s)\n&f-------------------------"
|
||||
nb-no:
|
||||
PREFIX: "[Tilgangsrettighetsskilt]"
|
||||
SIGN_PREFIX: "&4[PermSign]"
|
||||
MISSING_CREATION_INFO: "&7Du må spesifisere et skiltnavn og en komma-separert liste av tilgangsrettigheter for å opprette et tilgangsrettighetsskilt"
|
||||
SIGN_TIME_UNIT: "sekunder"
|
||||
UNIT_SECOND: "sekund"
|
||||
UNIT_SECONDS: "sekunder"
|
||||
UNIT_MINUTE: "minutt"
|
||||
UNIT_MINUTES: "minutter"
|
||||
UNIT_HOUR: "time"
|
||||
UNIT_HOURS: "timer"
|
||||
UNIT_DAY: "dag"
|
||||
UNIT_DAYS: "dager"
|
||||
SIGN_PERMANENT: "Permanent"
|
||||
SIGN_COST_FREE: "Gratis"
|
||||
COST_INVALID_NUMBER: "&7Den gitte kostnaden er ikke et gyldig nummer"
|
||||
DURATION_INVALID_NUMBER: "&7Den gitte varigheten er ikke et gyldig nummer"
|
||||
COMMAND_PLAYER_ONLY: "&7Denne kommandoen kan bare brukes av spillere"
|
||||
PERMISSION_SIGN_DESTROY_DENY: "&7Du har ikke tillatelse til å ødelegge tilgangsrettighetsskilter"
|
||||
PERMISSION_SIGN_DESTROY_SNEAK: "&7Du må snike for å fjerne et tilgangsrettighetsskilt"
|
||||
PERMISSION_SIGN_REMOVED: "&7Tilgangsrettighetsskilt fjernet"
|
||||
COMMAND_PERMISSION_DENIED: "&7Du har ikke de nødvendige tilgangsrettighetene for å kjøre denne kommandoen"
|
||||
CREATION_REQUEST_CREATED: "&7Tilgangsrettighetsskiltsforespørsel opprettet. Høyre-klikk på et tomt skilt innen 60 sekunder for å opprette det nye tilgangsrettighetsskiltet"
|
||||
@ -43,4 +60,5 @@ nb-no:
|
||||
PERMISSION_WORLD_INVALID: "&7Verdenen \"{world}\" er ikke et gyldig verdensnavn på denne serveren!"
|
||||
INTERACT_PERMISSION_MISSING: "&7Du har ikke de nødvendige tilgangsrettighetene for å bruke tilgangsrettighetsskilt"
|
||||
CREATION_REQUEST_CANCELLED: "&7Din siste tilgangsrettighetsskiltsforespørsel har blitt kansellert"
|
||||
RELOAD_SUCCESSFUL: "&7Plugin-modulen har blitt lastet inn på nytt"
|
||||
RELOAD_SUCCESSFUL: "&7Plugin-modulen har blitt lastet inn på nytt"
|
||||
SIGN_INFO: "&f---- &4Tilgangsrettighetsskilt&f ----\n&f| &bNavn: &7{Name}\n&f| &bTilgangsrettighet(er): &7{Permissions}\n&f| &bKostnad: &7{Cost}\n&f| &bVarighet: &7{Duration}\n&f| &7Høyreklikk skiltet mens du sniker for å kjøpe tilgangsrettighet(en/ene)\n&f-------------------------"
|
Loading…
Reference in New Issue
Block a user