Updates to SpeechController.

(Not working for some reason, will debug tomorrow.)
This commit is contained in:
Jeremy Schroeder 2012-12-22 22:44:33 -05:00
parent 1baa5c0f01
commit 2be6437a63
5 changed files with 308 additions and 279 deletions

View File

@ -11,7 +11,7 @@
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<craftbukkit.version>LATEST</craftbukkit.version>
<craftbukkit.version>1.4.5-R1.0</craftbukkit.version>
<citizensapi.version>2.0.6-SNAPSHOT</citizensapi.version>
<vault.version>1.2.19-SNAPSHOT</vault.version>
<powermock.version>1.4.12</powermock.version>

View File

@ -46,7 +46,6 @@ public class Settings {
CHAT_FORMAT("npc.chat.format.no-targets", "[<npc>]: <text>"),
CHAT_FORMAT_TO_TARGET("npc.chat.format.to-target", "[<npc>] -> You: <text>"),
CHAT_FORMAT_TO_BYSTANDERS("npc.chat.prefix.to-bystanders", "[<npc>] -> [<target>]: <text>"),
CHAT_FORMAT_WITH_TARGETS_TO_TARGET("npc.chat.format.with-target-to-target", "[<npc>] -> [<targets>]: <text>"),
CHAT_FORMAT_WITH_TARGETS_TO_BYSTANDERS("npc.chat.format.with-target-to-bystanders", "[<npc>] -> [<target>]: <text>"),
CHAT_RANGE("npc.chat.options.range", 5),
CHAT_BYSTANDERS_HEAR_TARGETED_CHAT("npc.chat.options.bystanders-hear-targeted-chat", true),

View File

@ -1,6 +1,8 @@
package net.citizensnpcs.npc.ai.speech;
import java.util.Collections;
import java.util.List;
import java.util.logging.Level;
import org.bukkit.entity.Entity;
import org.bukkit.entity.LivingEntity;
@ -9,79 +11,102 @@ import net.citizensnpcs.Settings.Setting;
import net.citizensnpcs.api.CitizensAPI;
import net.citizensnpcs.api.ai.speech.Talkable;
import net.citizensnpcs.api.ai.speech.TalkableEntity;
import net.citizensnpcs.api.ai.speech.Tongue;
import net.citizensnpcs.api.ai.speech.SpeechContext;
import net.citizensnpcs.api.ai.speech.VocalChord;
import net.citizensnpcs.api.npc.NPC;
import net.citizensnpcs.util.Messaging;
public class Chat implements VocalChord {
/*
CHAT_FORMAT("npc.chat.format.no-targets", "[<npc>]: <text>"),
CHAT_FORMAT_TO_TARGET("npc.chat.format.to-target", "[<npc>] -> You: <text>"),
CHAT_FORMAT_TO_BYSTANDERS("npc.chat.prefix.to-bystanders", "[<npc>] -> [<target>]: <text>"),
CHAT_FORMAT_WITH_TARGETS_TO_BYSTANDERS("npc.chat.format.with-target-to-bystanders", "[<npc>] -> [<targets>]: <text>"),
CHAT_RANGE("npc.chat.options.range", 5),
CHAT_BYSTANDERS_HEAR_TARGETED_CHAT("npc.chat.options.bystanders-hear-targeted-chat", true),
CHAT_MAX_NUMBER_OF_TARGETS("npc.chat.options.max-number-of-targets-to-show", 2),
CHAT_MULTIPLE_TARGETS_FORMAT("npc.chat.options.multiple-targets-format", "<target>,|<target>|& <target>|& others"),
*/
public final String VOCAL_CHORD_NAME = "chat";
@Override
public void talk(Tongue tongue) {
NPC npc = CitizensAPI.getNPCRegistry().getNPC(tongue.getTalker().getEntity());
@Override
public void talk(SpeechContext context) {
NPC npc = CitizensAPI.getNPCRegistry().getNPC(context.getTalker().getEntity());
// If no recipients, chat to the world with CHAT_FORMAT and CHAT_RANGE settings
if (!tongue.isTargeted()) {
String text = Setting.CHAT_FORMAT.asString().replace("<npc>", npc.getName()).replace("<text>", tongue.getContents());
talkToBystanders(npc, text, tongue);
if (!context.hasRecipients()) {
String text = Setting.CHAT_FORMAT.asString().replace("<npc>", npc.getName()).replace("<text>", context.getMessage());
talkToBystanders(npc, text, context);
return;
}
// Assumed recipients at this point
else if (tongue.getRecipients().size() <= 1) { // One recipient
String text = Setting.CHAT_FORMAT_TO_TARGET.asString().replace("<npc>", npc.getName()).replace("<text>", tongue.getContents());
tongue.getRecipients().get(0).talkTo(tongue, text, this);
else if (context.size() <= 1) { // One recipient
String text = Setting.CHAT_FORMAT_TO_TARGET.asString().replace("<npc>", npc.getName()).replace("<text>", context.getMessage());
String targetName = "";
// For each recipient
for (Talkable entity : context) {
entity.talkTo(context, text, this);
targetName = entity.getName();
}
// Check if bystanders hear targeted chat
if (!Setting.CHAT_BYSTANDERS_HEAR_TARGETED_CHAT.asBoolean()) return;
String bystanderText = Setting.CHAT_FORMAT_TO_BYSTANDERS.asString().replace("<npc>", npc.getName()).replace("<target>", tongue.getRecipients().get(0).getName()).replace("<text>", tongue.getContents());
talkToBystanders(npc, bystanderText, tongue);
// Format message with config setting and send to bystanders
String bystanderText = Setting.CHAT_FORMAT_TO_BYSTANDERS.asString().replace("<npc>", npc.getName()).replace("<target>", targetName).replace("<text>", context.getMessage());
talkToBystanders(npc, bystanderText, context);
return;
}
else { // Multiple recipients
// Set up text
String text = Setting.CHAT_FORMAT_TO_TARGET.asString().replace("<npc>", npc.getName()).replace("<text>", tongue.getContents());
tongue.getRecipients().get(0).talkTo(tongue, text, this);
if (!Setting.CHAT_BYSTANDERS_HEAR_TARGETED_CHAT.asBoolean()) return;
String bystanders = null;
bystanders = bystanders + "";
String bystanderText = Setting.CHAT_FORMAT_WITH_TARGETS_TO_BYSTANDERS.asString().replace("<npc>", npc.getName()).replace("<targets>", tongue.getRecipients().get(0).getName()).replace("<text>", tongue.getContents());
talkToBystanders(npc, bystanderText, tongue);
// TODO: Finish multiple recipients
else { // Multiple recipients
String text = Setting.CHAT_FORMAT_TO_TARGET.asString().replace("<npc>", npc.getName()).replace("<text>", context.getMessage());
List<String> targetNames = Collections.emptyList();
// Talk to each recipient
for (Talkable entity : context) {
entity.talkTo(context, text, this);
targetNames.add(entity.getName());
}
if (!Setting.CHAT_BYSTANDERS_HEAR_TARGETED_CHAT.asBoolean()) return;
String targets = "";
int max = Setting.CHAT_MAX_NUMBER_OF_TARGETS.asInt();
String[] format = Setting.CHAT_FORMAT_WITH_TARGETS_TO_BYSTANDERS.asString().split("\\|");
if (format.length != 4) Messaging.log(Level.WARNING, "npc.chat.format.with-target-to-bystanders invalid!");
if (max == 1) {
targets = format[0].replace("<npc>", targetNames.get(0)) + format[3];
}
else if (max == 2 || targetNames.size() == 2) {
if (targetNames.size() == 2)
targets = format[0].replace("<npc>", targetNames.get(0)) + format[2].replace("<npc>", targetNames.get(1));
else
targets = format[0].replace("<npc>", targetNames.get(0)) + format[1].replace("<npc>", targetNames.get(1)) + format[3];
}
else if (max >= 3) {
targets = format[0].replace("<npc>", targetNames.get(0));
int x = 1;
for (x = 1; x < max - 1; x++) {
if (targetNames.size() - 1 == x) break;
targets = targets + format[1].replace("<npc>", targetNames.get(x));
}
if (targetNames.size() == max)
targets = targets + format[2].replace("<npc>", targetNames.get(x));
else targets = targets + format[3];
}
String bystanderText = Setting.CHAT_FORMAT_WITH_TARGETS_TO_BYSTANDERS.asString().replace("<npc>", npc.getName()).replace("<targets>", targets).replace("<text>", context.getMessage());
talkToBystanders(npc, bystanderText, context);
}
}
private void talkToBystanders(NPC npc, String text, Tongue tongue) {
private void talkToBystanders(NPC npc, String text, SpeechContext context) {
// Get list of nearby entities
List<Entity> bystanderEntities = npc.getBukkitEntity().getNearbyEntities(Setting.CHAT_RANGE.asDouble(), Setting.CHAT_RANGE.asDouble(), Setting.CHAT_RANGE.asDouble());
for (Entity bystander : bystanderEntities)
// Continue if a LivingEntity, which is compatible with TalkableEntity
if (bystander instanceof LivingEntity) {
// Exclude Targets
if (tongue.isTargeted()) {
for (Talkable target : tongue.getRecipients())
// Exclude targeted recipients
if (context.hasRecipients()) {
for (Talkable target : context)
if (target.getEntity() == bystander) continue;
} else
// Found a nearby LivingEntity, make it Talkable and talkNear it
new TalkableEntity((LivingEntity) bystander).talkNear(tongue, text, this);
// Found a nearby LivingEntity, make it Talkable and talkNear it
new TalkableEntity((LivingEntity) bystander).talkNear(context, text, this);
}
}
@Override
public String getName() {
return VOCAL_CHORD_NAME;

View File

@ -1,6 +1,6 @@
package net.citizensnpcs.npc.ai.speech;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
@ -9,7 +9,7 @@ import net.citizensnpcs.api.ai.speech.VocalChord;
public class CitizensSpeechFactory implements SpeechFactory {
Map<String, Class<? extends VocalChord>> registered = Collections.emptyMap();
Map<String, Class<? extends VocalChord>> registered = new HashMap<String, Class <? extends VocalChord>>();
@Override
public void register(Class<? extends VocalChord> clazz, String name) {

View File

@ -1,232 +1,237 @@
package net.citizensnpcs.trait.text;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.TimeUnit;
import net.citizensnpcs.Settings.Setting;
import net.citizensnpcs.api.CitizensAPI;
import net.citizensnpcs.api.event.NPCRightClickEvent;
import net.citizensnpcs.api.exception.NPCLoadException;
import net.citizensnpcs.api.trait.Trait;
import net.citizensnpcs.api.util.DataKey;
import net.citizensnpcs.editor.Editor;
import net.citizensnpcs.trait.Toggleable;
import net.citizensnpcs.util.Messages;
import net.citizensnpcs.util.Messaging;
import net.citizensnpcs.util.Paginator;
import net.citizensnpcs.util.Util;
import org.bukkit.Bukkit;
import org.bukkit.conversations.Conversation;
import org.bukkit.conversations.ConversationAbandonedEvent;
import org.bukkit.conversations.ConversationAbandonedListener;
import org.bukkit.conversations.ConversationFactory;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.plugin.Plugin;
public class Text extends Trait implements Runnable, Toggleable, Listener, ConversationAbandonedListener {
private final Map<String, Date> cooldowns = new HashMap<String, Date>();
private int currentIndex;
private String itemInHandPattern = "default";
private final Plugin plugin;
private boolean randomTalker = Setting.DEFAULT_RANDOM_TALKER.asBoolean();
private double range = Setting.DEFAULT_TALK_CLOSE_RANGE.asDouble();
private boolean realisticLooker = Setting.DEFAULT_REALISTIC_LOOKING.asBoolean();
private boolean talkClose = Setting.DEFAULT_TALK_CLOSE.asBoolean();
private final List<String> text = new ArrayList<String>();
public Text() {
super("text");
this.plugin = CitizensAPI.getPlugin();
}
void add(String string) {
text.add(string);
}
@Override
public void conversationAbandoned(ConversationAbandonedEvent event) {
Bukkit.dispatchCommand((Player) event.getContext().getForWhom(), "npc text");
}
void edit(int index, String newText) {
text.set(index, newText);
}
public Editor getEditor(final Player player) {
final Conversation conversation = new ConversationFactory(plugin)
.addConversationAbandonedListener(this).withLocalEcho(false).withEscapeSequence("/npc text")
.withEscapeSequence("exit").withModality(false).withFirstPrompt(new TextStartPrompt(this))
.buildConversation(player);
return new Editor() {
@Override
public void begin() {
Messaging.sendTr(player, Messages.TEXT_EDITOR_BEGIN);
conversation.begin();
}
@Override
public void end() {
Messaging.sendTr(player, Messages.TEXT_EDITOR_END);
conversation.abandon();
}
};
}
boolean hasIndex(int index) {
return index >= 0 && text.size() > index;
}
@Override
public void load(DataKey key) throws NPCLoadException {
text.clear();
// TODO: legacy, remove later
for (DataKey sub : key.getIntegerSubKeys())
text.add(sub.getString(""));
for (DataKey sub : key.getRelative("text").getIntegerSubKeys())
text.add(sub.getString(""));
if (text.isEmpty())
populateDefaultText();
talkClose = key.getBoolean("talk-close", talkClose);
realisticLooker = key.getBoolean("realistic-looking", realisticLooker);
randomTalker = key.getBoolean("random-talker", randomTalker);
range = key.getDouble("range", range);
itemInHandPattern = key.getString("talkitem", itemInHandPattern);
}
@EventHandler
public void onRightClick(NPCRightClickEvent event) {
if (!event.getNPC().equals(npc))
return;
String localPattern = itemInHandPattern.equals("default") ? Setting.TALK_ITEM.asString()
: itemInHandPattern;
if (Util.matchesItemInHand(event.getClicker(), localPattern) && !shouldTalkClose())
sendText(event.getClicker());
}
private void populateDefaultText() {
text.addAll(Setting.DEFAULT_TEXT.asList());
}
void remove(int index) {
text.remove(index);
}
@Override
public void run() {
if (!talkClose || !npc.isSpawned())
return;
List<Entity> nearby = npc.getBukkitEntity().getNearbyEntities(range, range, range);
for (Entity search : nearby) {
if (!(search instanceof Player))
continue;
Player player = (Player) search;
// If the cooldown is not expired, do not send text
Date cooldown = cooldowns.get(player.getName());
if (cooldown != null) {
if (!new Date().after(cooldown))
return;
cooldowns.remove(player.getName());
}
if (!sendText(player))
return;
// Add a cooldown if the text was successfully sent
Date wait = new Date();
int secondsDelta = RANDOM.nextInt(Setting.TALK_CLOSE_MAXIMUM_COOLDOWN.asInt())
+ Setting.TALK_CLOSE_MINIMUM_COOLDOWN.asInt();
if (secondsDelta <= 0)
return;
long millisecondsDelta = TimeUnit.MILLISECONDS.convert(secondsDelta, TimeUnit.SECONDS);
wait.setTime(wait.getTime() + millisecondsDelta);
cooldowns.put(player.getName(), wait);
}
}
@Override
public void save(DataKey key) {
key.setBoolean("talk-close", talkClose);
key.setBoolean("random-talker", randomTalker);
key.setBoolean("realistic-looking", realisticLooker);
key.setDouble("range", range);
key.setString("talkitem", itemInHandPattern);
// TODO: legacy, remove later
for (int i = 0; i < 100; i++)
key.removeKey(String.valueOf(i));
key.removeKey("text");
for (int i = 0; i < text.size(); i++)
key.setString("text." + String.valueOf(i), text.get(i));
}
boolean sendPage(Player player, int page) {
Paginator paginator = new Paginator().header(npc.getName() + "'s Text Entries");
for (int i = 0; i < text.size(); i++)
paginator.addLine("<a>" + i + " <7>- <e>" + text.get(i));
return paginator.sendPage(player, page);
}
private boolean sendText(Player player) {
if (!player.hasPermission("citizens.admin") && !player.hasPermission("citizens.npc.talk"))
return false;
if (text.size() == 0)
return false;
int index = 0;
if (randomTalker)
index = new Random().nextInt(text.size());
else {
if (currentIndex > text.size() - 1)
currentIndex = 0;
index = currentIndex++;
}
Messaging.sendWithNPC(player, Setting.CHAT_PREFIX.asString() + text.get(index), npc);
return true;
}
void setItemInHandPattern(String pattern) {
itemInHandPattern = pattern;
}
void setRange(double range) {
this.range = range;
}
boolean shouldTalkClose() {
return talkClose;
}
@Override
public boolean toggle() {
return (talkClose = !talkClose);
}
boolean toggleRandomTalker() {
return (randomTalker = !randomTalker);
}
boolean toggleRealisticLooking() {
return (realisticLooker = !realisticLooker);
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("Text{talk-close=" + talkClose + ",text=");
for (String line : text)
builder.append(line + ",");
builder.append("}");
return builder.toString();
}
private static Random RANDOM = Util.getFastRandom();
package net.citizensnpcs.trait.text;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.TimeUnit;
import net.citizensnpcs.Settings.Setting;
import net.citizensnpcs.api.CitizensAPI;
import net.citizensnpcs.api.ai.speech.SpeechContext;
import net.citizensnpcs.api.ai.speech.Talkable;
import net.citizensnpcs.api.ai.speech.TalkableEntity;
import net.citizensnpcs.api.event.NPCRightClickEvent;
import net.citizensnpcs.api.exception.NPCLoadException;
import net.citizensnpcs.api.trait.Trait;
import net.citizensnpcs.api.util.DataKey;
import net.citizensnpcs.editor.Editor;
import net.citizensnpcs.trait.Toggleable;
import net.citizensnpcs.util.Messages;
import net.citizensnpcs.util.Messaging;
import net.citizensnpcs.util.Paginator;
import net.citizensnpcs.util.Util;
import org.bukkit.Bukkit;
import org.bukkit.conversations.Conversation;
import org.bukkit.conversations.ConversationAbandonedEvent;
import org.bukkit.conversations.ConversationAbandonedListener;
import org.bukkit.conversations.ConversationFactory;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.plugin.Plugin;
public class Text extends Trait implements Runnable, Toggleable, Listener, ConversationAbandonedListener {
private final Map<String, Date> cooldowns = new HashMap<String, Date>();
private int currentIndex;
private String itemInHandPattern = "default";
private final Plugin plugin;
private boolean randomTalker = Setting.DEFAULT_RANDOM_TALKER.asBoolean();
private double range = Setting.DEFAULT_TALK_CLOSE_RANGE.asDouble();
private boolean realisticLooker = Setting.DEFAULT_REALISTIC_LOOKING.asBoolean();
private boolean talkClose = Setting.DEFAULT_TALK_CLOSE.asBoolean();
private final List<String> text = new ArrayList<String>();
public Text() {
super("text");
this.plugin = CitizensAPI.getPlugin();
}
void add(String string) {
text.add(string);
}
@Override
public void conversationAbandoned(ConversationAbandonedEvent event) {
Bukkit.dispatchCommand((Player) event.getContext().getForWhom(), "npc text");
}
void edit(int index, String newText) {
text.set(index, newText);
}
public Editor getEditor(final Player player) {
final Conversation conversation = new ConversationFactory(plugin)
.addConversationAbandonedListener(this).withLocalEcho(false).withEscapeSequence("/npc text")
.withEscapeSequence("exit").withModality(false).withFirstPrompt(new TextStartPrompt(this))
.buildConversation(player);
return new Editor() {
@Override
public void begin() {
Messaging.sendTr(player, Messages.TEXT_EDITOR_BEGIN);
conversation.begin();
}
@Override
public void end() {
Messaging.sendTr(player, Messages.TEXT_EDITOR_END);
conversation.abandon();
}
};
}
boolean hasIndex(int index) {
return index >= 0 && text.size() > index;
}
@Override
public void load(DataKey key) throws NPCLoadException {
text.clear();
// TODO: legacy, remove later
for (DataKey sub : key.getIntegerSubKeys())
text.add(sub.getString(""));
for (DataKey sub : key.getRelative("text").getIntegerSubKeys())
text.add(sub.getString(""));
if (text.isEmpty())
populateDefaultText();
talkClose = key.getBoolean("talk-close", talkClose);
realisticLooker = key.getBoolean("realistic-looking", realisticLooker);
randomTalker = key.getBoolean("random-talker", randomTalker);
range = key.getDouble("range", range);
itemInHandPattern = key.getString("talkitem", itemInHandPattern);
}
@EventHandler
public void onRightClick(NPCRightClickEvent event) {
if (!event.getNPC().equals(npc))
return;
String localPattern = itemInHandPattern.equals("default") ? Setting.TALK_ITEM.asString()
: itemInHandPattern;
if (Util.matchesItemInHand(event.getClicker(), localPattern) && !shouldTalkClose())
sendText(event.getClicker());
}
private void populateDefaultText() {
text.addAll(Setting.DEFAULT_TEXT.asList());
}
void remove(int index) {
text.remove(index);
}
@Override
public void run() {
if (!talkClose || !npc.isSpawned())
return;
List<Entity> nearby = npc.getBukkitEntity().getNearbyEntities(range, range, range);
for (Entity search : nearby) {
if (!(search instanceof Player))
continue;
Player player = (Player) search;
// If the cooldown is not expired, do not send text
Date cooldown = cooldowns.get(player.getName());
if (cooldown != null) {
if (!new Date().after(cooldown))
return;
cooldowns.remove(player.getName());
}
if (!sendText(player))
return;
// Add a cooldown if the text was successfully sent
Date wait = new Date();
int secondsDelta = RANDOM.nextInt(Setting.TALK_CLOSE_MAXIMUM_COOLDOWN.asInt())
+ Setting.TALK_CLOSE_MINIMUM_COOLDOWN.asInt();
if (secondsDelta <= 0)
return;
long millisecondsDelta = TimeUnit.MILLISECONDS.convert(secondsDelta, TimeUnit.SECONDS);
wait.setTime(wait.getTime() + millisecondsDelta);
cooldowns.put(player.getName(), wait);
}
}
@Override
public void save(DataKey key) {
key.setBoolean("talk-close", talkClose);
key.setBoolean("random-talker", randomTalker);
key.setBoolean("realistic-looking", realisticLooker);
key.setDouble("range", range);
key.setString("talkitem", itemInHandPattern);
// TODO: legacy, remove later
for (int i = 0; i < 100; i++)
key.removeKey(String.valueOf(i));
key.removeKey("text");
for (int i = 0; i < text.size(); i++)
key.setString("text." + String.valueOf(i), text.get(i));
}
boolean sendPage(Player player, int page) {
Paginator paginator = new Paginator().header(npc.getName() + "'s Text Entries");
for (int i = 0; i < text.size(); i++)
paginator.addLine("<a>" + i + " <7>- <e>" + text.get(i));
return paginator.sendPage(player, page);
}
private boolean sendText(Player player) {
if (!player.hasPermission("citizens.admin") && !player.hasPermission("citizens.npc.talk"))
return false;
if (text.size() == 0)
return false;
int index = 0;
if (randomTalker)
index = new Random().nextInt(text.size());
else {
if (currentIndex > text.size() - 1)
currentIndex = 0;
index = currentIndex++;
}
npc.getDefaultSpeechController().speak(new SpeechContext(text.get(index), new TalkableEntity(player)));
// Messaging.sendWithNPC(player, Setting.CHAT_PREFIX.asString() + text.get(index), npc);
return true;
}
void setItemInHandPattern(String pattern) {
itemInHandPattern = pattern;
}
void setRange(double range) {
this.range = range;
}
boolean shouldTalkClose() {
return talkClose;
}
@Override
public boolean toggle() {
return (talkClose = !talkClose);
}
boolean toggleRandomTalker() {
return (randomTalker = !randomTalker);
}
boolean toggleRealisticLooking() {
return (realisticLooker = !realisticLooker);
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("Text{talk-close=" + talkClose + ",text=");
for (String line : text)
builder.append(line + ",");
builder.append("}");
return builder.toString();
}
private static Random RANDOM = Util.getFastRandom();
}