package tc.oc.commons.bukkit.tablist;
import java.util.HashMap;
import java.util.Map;
import org.bukkit.entity.Player;
import org.bukkit.event.player.PlayerChangedWorldEvent;
import org.bukkit.event.player.PlayerRespawnEvent;
import org.bukkit.scheduler.BukkitTask;
import tc.oc.commons.core.util.DefaultProvider;
import javax.annotation.Nullable;
/**
* A single player's tab list. When this view is enabled, it creates a scoreboard team for
* each slot and creates an entry for each team. The team names are used to order the list.
* The view is always full of entries. When an entry is removed, it is replaced by a blank
* one. The player's list is not disabled, but because it is always full of fake entries,
* the real entries are pushed off the bottom and cannot be seen. The fake team names all
* start with a '\u0001' character, so they will always come before any real teams.
*/
public class TabView {
public static class Factory implements DefaultProvider<Player, TabView> {
@Override
public TabView get(Player key) {
return new TabView(key);
}
}
private static final int WIDTH = 4, HEIGHT = 20;
private final int size, headerSlot, footerSlot;
// The single player seeing this view
private final Player viewer;
protected @Nullable TabManager manager;
// True when any slots/header/footer have been changed but not rendered
private boolean dirtyLayout, dirtyContent, dirtyHeaderFooter;
private final TabEntry[] slots, rendered;
private @Nullable BukkitTask fakeEntityTask;
public TabView(Player viewer) {
this.viewer = viewer;
this.size = WIDTH * HEIGHT;
this.headerSlot = this.size;
this.footerSlot = this.headerSlot + 1;
// Two extra slots for header/footer
this.slots = new TabEntry[this.size + 2];
this.rendered = new TabEntry[this.size + 2];
}
private void assertEnabled() {
if(manager == null) throw new IllegalStateException(getClass().getSimpleName() + " is not enabled");
}
public Player getViewer() {
return viewer;
}
public int getWidth() {
return WIDTH;
}
public int getHeight() {
return HEIGHT;
}
public int getSize() {
return this.size;
}
/**
* Take control of the viewer's player list
*/
public void enable(TabManager manager) {
if(this.manager != null) disable();
this.manager = manager;
this.setup();
this.invalidateLayout();
this.invalidateContent();
this.invalidateHeaderFooter();
}
/**
* Tear down the display and return control the the viewer's player list to bukkit
*/
public void disable() {
if(this.manager != null) {
if(fakeEntityTask != null) {
fakeEntityTask.cancel();
fakeEntityTask = null;
}
this.manager.removeView(this);
this.tearDown();
for(int i = 0; i < slots.length; i++) {
if(slots[i] != null) {
slots[i].removeFromView(this);
slots[i] = null;
}
}
this.manager = null;
}
}
private void invalidateManager() {
if(this.manager != null) this.manager.invalidate();
}
protected void invalidateLayout() {
if(!this.dirtyLayout) {
this.dirtyLayout = true;
this.invalidateManager();
}
}
protected void invalidateContent() {
if(!this.dirtyContent) {
this.dirtyContent = true;
this.invalidateManager();
}
}
protected void invalidateLayoutAndContent() {
if(!dirtyLayout || !dirtyContent) {
dirtyLayout = dirtyContent = true;
invalidateManager();
}
}
protected void invalidateContent(TabEntry entry) {
int slot = getSlot(entry);
if(slot >= this.size) {
this.invalidateHeaderFooter();
} else if(slot >= 0) {
this.invalidateContent();
}
}
protected void invalidateHeaderFooter() {
if(!this.dirtyHeaderFooter) {
this.dirtyHeaderFooter = true;
this.invalidateManager();
}
}
protected boolean isLayoutDirty() {
return this.dirtyLayout;
}
protected int getSlot(TabEntry entry) {
for(int i = 0; i < slots.length; i++) {
if(entry == slots[i]) return i;
}
return -1;
}
private void setSlot(int slot, @Nullable TabEntry entry) {
assertEnabled();
if(entry == null) {
entry = this.manager.getBlankEntry(slot);
}
TabEntry oldEntry = this.slots[slot];
if(oldEntry != entry) {
oldEntry.removeFromView(this);
int oldIndex = getSlot(entry);
if(oldIndex != -1) {
TabEntry blankEntry = this.manager.getBlankEntry(oldIndex);
this.slots[oldIndex] = blankEntry;
blankEntry.addToView(this);
} else {
entry.addToView(this);
}
this.slots[slot] = entry;
if(slot < this.size) {
this.invalidateLayoutAndContent();
} else {
this.invalidateHeaderFooter();
}
}
}
public void setSlot(int x, int y, @Nullable TabEntry entry) {
this.setSlot(this.slotIndex(x, y), entry);
}
public void setHeader(@Nullable TabEntry entry) {
this.setSlot(this.headerSlot, entry);
}
public void setFooter(@Nullable TabEntry entry) {
this.setSlot(this.footerSlot, entry);
}
private int slotIndex(int x, int y) {
return x * HEIGHT + y;
}
public void render() {
if(this.manager == null) return;
TabRender render = new TabRender(this);
this.renderLayout(render);
this.renderContent(render);
this.markSlotsClean();
this.renderHeaderFooter(render, false);
render.finish();
}
public void renderLayout(TabRender render) {
if(this.manager == null) return;
if(this.dirtyLayout) {
this.dirtyLayout = false;
// First search for entries that have been added, removed, or moved
Map<TabEntry, Integer> removals = new HashMap<>();
Map<TabEntry, Integer> additions = new HashMap<>();
for(int index = 0; index < this.size; index++) {
TabEntry oldEntry = this.rendered[index];
TabEntry newEntry = this.rendered[index] = this.slots[index];
if(oldEntry != newEntry) {
// There is a different entry in this slot
Integer oldIndex = removals.remove(newEntry);
if(oldIndex == null) {
// We have not seen the new entry yet, so assume it's being added
additions.put(newEntry, index);
} else {
// We already saw the new entry removed from another slot, so it's actually being moved
render.changeSlot(newEntry, oldIndex, index);
}
Integer newIndex = additions.remove(oldEntry);
if(newIndex == null) {
// We have not seen the old entry yet, so assume it's being removed
removals.put(oldEntry, index);
} else {
// We already saw the old entry added to another slot, so it's actually being moved
render.changeSlot(oldEntry, index, newIndex);
}
}
}
// Build the removal packet
for(Map.Entry<TabEntry, Integer> removal : removals.entrySet()) {
render.removeEntry(removal.getKey(), removal.getValue());
}
// Build the addition packet (this also adds to the update packet)
for(Map.Entry<TabEntry, Integer> addition : additions.entrySet()) {
render.addEntry(addition.getKey(), addition.getValue());
}
}
}
public void renderContent(TabRender render) {
if(this.manager == null) return;
if(this.dirtyContent) {
this.dirtyContent = false;
// Build the update packet from entries with new content that are not being added or removed
for(int i = 0; i < this.size; i++) {
if(this.slots[i].isDirty(this)) {
render.updateEntry(this.slots[i], i);
}
}
}
}
public void markSlotsClean() {
for(TabEntry entry : slots) {
entry.markClean(this);
}
}
public void renderHeaderFooter(TabRender render, boolean force) {
if(this.manager == null) return;
if(force || this.dirtyHeaderFooter) {
this.dirtyHeaderFooter = false;
render.setHeaderFooter(this.rendered[this.headerSlot] = this.slots[this.headerSlot],
this.rendered[this.footerSlot] = this.slots[this.footerSlot]);
this.slots[this.headerSlot].markClean(this);
this.slots[this.footerSlot].markClean(this);
}
}
private void setup() {
assertEnabled();
for(int slot = 0; slot < this.slots.length; slot++) {
this.slots[slot] = this.manager.getBlankEntry(slot);
this.slots[slot].addToView(this);
}
TabRender render = new TabRender(this);
for(int index = 0; index < this.size; index++) {
render.createSlot(this.rendered[index] = this.slots[index], index);
}
this.renderHeaderFooter(render, true);
render.finish();
}
private void tearDown() {
if(this.manager == null) return;
TabRender render = new TabRender(this);
render.setHeaderFooter(this.manager.getBlankEntry(this.headerSlot),
this.manager.getBlankEntry(this.footerSlot));
for(int index = 0; index < this.size; index++) {
render.destroySlot(this.rendered[index], index);
this.rendered[index] = null;
}
render.finish();
}
protected void refreshEntry(TabEntry entry) {
if(this.manager == null) return;
TabRender render = new TabRender(this);
int slot = getSlot(entry);
if(slot < this.size) {
render.refreshEntry(entry, slot);
} else {
this.renderHeaderFooter(render, true);
}
render.finish();
}
protected void updateFakeEntity(TabEntry entry) {
if(this.manager == null) return;
TabRender render = new TabRender(this);
render.updateFakeEntity(entry, false);
render.finish();
}
private void respawnFakeEntities() {
if(this.manager == null || fakeEntityTask != null) return;
fakeEntityTask = this.viewer.getServer().getScheduler().runTask(this.manager.getPlugin(), () -> {
fakeEntityTask = null;
TabRender render = new TabRender(TabView.this);
for(TabEntry entry : TabView.this.rendered) {
render.updateFakeEntity(entry, true);
}
render.finish();
});
}
protected void onRespawn(PlayerRespawnEvent event) {
if(viewer.equals(event.getPlayer())) this.respawnFakeEntities();
}
protected void onWorldChange(PlayerChangedWorldEvent event) {
if(viewer.equals(event.getPlayer())) this.respawnFakeEntities();
}
}