Signs.java

AKL City ยท akl-city.git

Branches > main > 02babc9 > files / src / main / java / dev / grahp / aklcity / Signs.java

package dev.grahp.aklcity;

import org.bukkit.event.Listener;
import org.bukkit.event.EventHandler;
import org.bukkit.event.block.*;

import org.bukkit.plugin.java.JavaPlugin;

import net.kyori.adventure.text.*;
import net.kyori.adventure.text.format.NamedTextColor;

import org.bukkit.*;
import org.bukkit.block.Block;
import org.bukkit.block.Sign;
import org.bukkit.block.sign.Side;

import java.util.*;
import java.util.regex.*;

import dev.grahp.option.Optional;
import static dev.grahp.aklcity.utils.Paper.*;

// TODO is there a better interface than signs?
// Perhaps with text entities?
// Maybe they could even spawn their own signs that you can edit?

// Need a disk persistent mapping of addresses to maps of other things

// Keeps track of stuff on signs
// - How will the residents know what beds to go into?
class Signs implements Listener {
  public static record Address(Long number, String street) { }
  public static record Building(String city, Address address, Long houses, Long jobs) { }

  // TODO so this needs to be persisted to the db.
  private static class BuildingRegistry {
    private final Map<Location, Building> buildings = new HashMap<>();
    private final Map<String, Long> houses = new HashMap<>();
    private final Map<String, Long> jobs = new HashMap<>();

    @Override
    public String toString() {
      return "BuildingRegistry[buildings=" + buildings + ", houses=" + houses + ", jobs=" + jobs + "]";
    }

    public void clear() {
      buildings.clear();
      houses.clear();
      jobs.clear();
    }

    public void put(Location location, Building building) {
      var old = buildings.put(location, building);

      if (old != null) {
        var oldCity = old.city();

        houses.put(oldCity, houses.getOrDefault(oldCity, 0L) - old.houses());
        jobs.put(oldCity, jobs.getOrDefault(oldCity, 0L) - old.jobs());
      }

      var city = building.city();
      houses.put(city, houses.getOrDefault(city, 0L) + building.houses());
      jobs.put(city,   jobs.getOrDefault(city, 0L)   + building.jobs());
      // debug("houses", houses);
      // debug("jobs", jobs);
    }

    public void remove(Location l) {
      var b = buildings.remove(l);
      if (b != null) {
        var city = b.city();
        houses.put(city, houses.getOrDefault(city, 0L) - b.houses());
        jobs.put(city,   jobs.getOrDefault(city, 0L)   - b.jobs());
        // debug("houses", houses);
        // debug("jobs", jobs);
      }
    }
  }

  private static final BuildingRegistry buildings = new BuildingRegistry();

  private static final Pattern POP_PATTERN = Pattern.compile("^T:\\s*(.+)$"); // T: Pine

  private static final Pattern CITY_PATTERN = Pattern.compile("^P:\\s*(.+)$"); // P: Burg
  private static final Pattern ADDRESS_PATTERN = Pattern.compile("^(\\d+)\\s+(.+)$"); // 322 Test
  private static final Pattern UNITS_PATTERN = Pattern.compile("^(\\d+)\\s*/\\s*(\\d+)$"); // 6/7

  private Location blockLocation(Sign block) {
    return block.getLocation().toBlockLocation();
  }

  private Sign sign(BlockEvent e) {
    return (Sign) e.getBlock().getState();
  }

  private boolean isPopulationSign(Sign sign) {
    var lines = sign.getSide(Side.FRONT).getLines();
    return POP_PATTERN.matcher(lines[0]).matches();
  }

  private Optional<Building> getSignBuilding(Sign sign) {
    var lines = sign.getSide(Side.FRONT).getLines();
    var addressLine = lines[1];
    var cityMatcher = CITY_PATTERN.matcher(lines[0]);
    var unitsMatcher = UNITS_PATTERN.matcher(lines[2]);
    var addressMatcher = ADDRESS_PATTERN.matcher(addressLine);
    if (cityMatcher.matches() && unitsMatcher.matches() && addressMatcher.matches()) {
      var city = cityMatcher.group(1);

      var houses = Long.parseLong(unitsMatcher.group(1));
      var jobs = Long.parseLong(unitsMatcher.group(2));

      var buildingNumber = Long.parseLong(addressMatcher.group(1));
      var street = addressMatcher.group(2);

      var address = new Address(buildingNumber, street);
      var building = new Building(city, address, houses, jobs);

      return new Optional(building);
    } else {
      return new Optional();
    }
  }

  // TODO color the sign text red or gray or something once the sign has actually been confirmed
  // That way it's obvious the sign is working as intended
  // They would store that color though and thus load in inproperly
  // Perhaps display a particle or something above working signs
  // Particles could be laggy?
  @EventHandler
  public void onEditSign(SignChangeEvent e) {
    var sign = sign(e);
    // TODO building is the OLD building, not the new one!
    var building = getSignBuilding(sign);
    if (building.present) {
      var b = building.val();
      buildings.put(blockLocation(sign), b);
      var address = b.address();
      e.getPlayer().sendMessage(Component.text("Saved Building '" + address.number().toString() + " " + address.street() + "'", NamedTextColor.GREEN));
    }
  }

  // On breaking a block, send a message that the player broke a building
  // Could it be possible to undo the sign break?
  // Shift to break a sign?
  // Or break it twice
  @EventHandler
  public void onBreakSign(BlockBreakEvent e) {
    var sign = sign(e);
    debug(sign);
    var building = getSignBuilding(sign);
    debug(building);
    if (building.present) {
      // Require a shift or cancel the event?
      var b = building.val();
      buildings.remove(blockLocation(sign));
      var address = b.address();
      e.getPlayer().sendMessage(Component.text("Deleted Building '" + address.number().toString() + " " + address.street() + "'", NamedTextColor.RED));
    }
  }

  @EventHandler
  public void onPlaceSign(BlockPlaceEvent e) {
    var sign = sign(e);
    var building = getSignBuilding(sign);
    if (building.present) {
      // Require a shift or cancel the event?
      var b = building.val();
      buildings.put(blockLocation(sign), b);
      var address = b.address();
      e.getPlayer().sendMessage(Component.text("Added Building '" + address.number().toString() + " " + address.street() + "'", NamedTextColor.GREEN));
    }
  }

  // On block place, add the sign as well
  // On loading of the world, add all signs to the set
  // On plugin reload, get all the signs in all loaded chunks again

  public void scanWorldSigns() {
    buildings.clear();

    var populationSigns = new HashSet<Sign>();
    for (var world : Bukkit.getWorlds()) {
      for (var chunk : world.getLoadedChunks()) {
        // TODO are signs still tile entities? After 26.2 or something.
        for (var block : chunk.getTileEntities()) {
          if (block instanceof Sign sign) {

            var building = getSignBuilding(sign);
            if (building.present) {
              buildings.put(blockLocation(sign), building.val());
            } else if (isPopulationSign(sign)) {
              populationSigns.add(sign);
            }
          }
        }
      }
    }
    // for (var sign : populationSigns) {
    //   var front = sign.getSide(Side.FRONT);
    //   POP_PATTERN.matcher(front.line(0))
    //   var jobs = Long.parseLong(unitsMatcher.group(2));
    //   front.line(1, Component.text("Houses: " + ))
    // }
    // debug(buildings);
  }

  public Signs(JavaPlugin plugin) {
    Bukkit.getScheduler().runTaskTimer(
        plugin,
        this::scanWorldSigns,
        0L,
        100L
        );
  }

}