diff options
| -rw-r--r-- | .gitignore | 7 | ||||
| -rw-r--r-- | pantheon-bugs.pcapng | bin | 0 -> 843512 bytes | |||
| -rw-r--r-- | pantheon-combat.pcapng | bin | 0 -> 20223452 bytes | |||
| -rw-r--r-- | pantheon-harvesting.pcapng | bin | 0 -> 1341940 bytes | |||
| -rw-r--r-- | pantheon-login.pcapng | bin | 0 -> 7685016 bytes | |||
| -rw-r--r-- | pantheon-long-extended.pcapng | bin | 0 -> 142722232 bytes | |||
| -rw-r--r-- | pantheon-long.pcapng | bin | 0 -> 24115228 bytes | |||
| -rw-r--r-- | pom.xml | 117 | ||||
| -rwxr-xr-x | run.sh | 3 | ||||
| -rw-r--r-- | src/main/java/org/challman/pantheon_parser/App.java | 398 | ||||
| -rw-r--r-- | src/main/java/org/challman/pantheon_parser/Entity.java | 10 | ||||
| -rw-r--r-- | src/main/java/org/challman/pantheon_parser/EntityMap.java | 355 | ||||
| -rw-r--r-- | src/main/java/org/challman/pantheon_parser/algorithm/AhoCorasick.java | 164 | ||||
| -rw-r--r-- | src/main/java/org/challman/pantheon_parser/algorithm/BooyerMoore.java | 194 | ||||
| -rw-r--r-- | src/test/java/org/challman/pantheon_parser/AppTest.java | 19 |
15 files changed, 1267 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9842b38 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +.classpath +.idea/* +.mvn/ +.project +.settings/* +target/* +*~ diff --git a/pantheon-bugs.pcapng b/pantheon-bugs.pcapng Binary files differnew file mode 100644 index 0000000..062b2a8 --- /dev/null +++ b/pantheon-bugs.pcapng diff --git a/pantheon-combat.pcapng b/pantheon-combat.pcapng Binary files differnew file mode 100644 index 0000000..5ba3dbf --- /dev/null +++ b/pantheon-combat.pcapng diff --git a/pantheon-harvesting.pcapng b/pantheon-harvesting.pcapng Binary files differnew file mode 100644 index 0000000..fddf838 --- /dev/null +++ b/pantheon-harvesting.pcapng diff --git a/pantheon-login.pcapng b/pantheon-login.pcapng Binary files differnew file mode 100644 index 0000000..75311aa --- /dev/null +++ b/pantheon-login.pcapng diff --git a/pantheon-long-extended.pcapng b/pantheon-long-extended.pcapng Binary files differnew file mode 100644 index 0000000..627a75f --- /dev/null +++ b/pantheon-long-extended.pcapng diff --git a/pantheon-long.pcapng b/pantheon-long.pcapng Binary files differnew file mode 100644 index 0000000..fe09a70 --- /dev/null +++ b/pantheon-long.pcapng @@ -0,0 +1,117 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + + <groupId>org.challman.pantheon_parser</groupId> + <artifactId>pantheon-parser</artifactId> + <version>1.0-SNAPSHOT</version> + + <name>pantheon-parser</name> + <!-- FIXME change it to the project's website --> + <url>http://www.example.com</url> + + <properties> + <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> + <maven.compiler.release>22</maven.compiler.release> + </properties> + + <dependencyManagement> + <dependencies> + <dependency> + <groupId>org.junit</groupId> + <artifactId>junit-bom</artifactId> + <version>5.11.0</version> + <type>pom</type> + <scope>import</scope> + </dependency> + </dependencies> + </dependencyManagement> + + <dependencies> + <dependency> + <groupId>org.junit.jupiter</groupId> + <artifactId>junit-jupiter-api</artifactId> + <scope>test</scope> + </dependency> + <!-- Optionally: parameterized tests support --> + <dependency> + <groupId>org.junit.jupiter</groupId> + <artifactId>junit-jupiter-params</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>com.slytechs.jnet.jnetpcap</groupId> + <artifactId>jnetpcap-wrapper</artifactId> + <version>2.3.1</version> + </dependency> + </dependencies> + + <build> + <pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) --> + <plugins> + <!-- clean lifecycle, see https://maven.apache.org/ref/current/maven-core/lifecycles.html#clean_Lifecycle --> + <plugin> + <artifactId>maven-clean-plugin</artifactId> + <version>3.4.0</version> + </plugin> + <!-- default lifecycle, jar packaging: see https://maven.apache.org/ref/current/maven-core/default-bindings.html#Plugin_bindings_for_jar_packaging --> + <plugin> + <artifactId>maven-resources-plugin</artifactId> + <version>3.3.1</version> + </plugin> + <plugin> + <artifactId>maven-compiler-plugin</artifactId> + <version>3.13.0</version> + </plugin> + <plugin> + <artifactId>maven-surefire-plugin</artifactId> + <version>3.3.0</version> + </plugin> + <plugin> + <artifactId>maven-jar-plugin</artifactId> + <version>3.4.2</version> + </plugin> + <plugin> + <artifactId>maven-install-plugin</artifactId> + <version>3.1.2</version> + </plugin> + <plugin> + <artifactId>maven-deploy-plugin</artifactId> + <version>3.1.2</version> + </plugin> + <!-- site lifecycle, see https://maven.apache.org/ref/current/maven-core/lifecycles.html#site_Lifecycle --> + <plugin> + <artifactId>maven-site-plugin</artifactId> + <version>3.12.1</version> + </plugin> + <plugin> + <artifactId>maven-project-info-reports-plugin</artifactId> + <version>3.6.1</version> + </plugin> + </plugins> + </pluginManagement> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-shade-plugin</artifactId> + <version>3.4.1</version> + <executions> + <execution> + <phase>package</phase> + <goals> + <goal>shade</goal> + </goals> + <configuration> + <transformers> + <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"> + <mainClass>org.challman.pantheon_parser.App</mainClass> + </transformer> + </transformers> + </configuration> + </execution> + </executions> + </plugin> + </plugins> + </build> +</project> @@ -0,0 +1,3 @@ +#!/bin/bash + +mvn clean compile && mvn exec:java -Dexec.mainClass="org.challman.pantheon_parser.App" diff --git a/src/main/java/org/challman/pantheon_parser/App.java b/src/main/java/org/challman/pantheon_parser/App.java new file mode 100644 index 0000000..4d6999f --- /dev/null +++ b/src/main/java/org/challman/pantheon_parser/App.java @@ -0,0 +1,398 @@ +package org.challman.pantheon_parser; + +import org.challman.pantheon_parser.algorithm.BooyerMoore.MatchResult; +import org.challman.pantheon_parser.algorithm.BooyerMoore; +import org.jnetpcap.BpFilter; +import org.jnetpcap.Pcap; +import org.jnetpcap.PcapException; +import org.jnetpcap.PcapHeader; +import org.jnetpcap.PcapIf; + +import javax.swing.*; +import java.awt.*; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.charset.StandardCharsets; +import java.sql.Time; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.BiConsumer; + +public class App { + private static HashMap<String, String> entityDict = new HashMap<>(); + private static EntityMap entityMap; + private static Entity player = new Entity(); + + private static void debugDuplicateCoordinates() { + System.out.println("=== DEBUG: Checking for duplicate coordinates ==="); + + // Get all entities from entityMap (you'll need to add a method to EntityMap) + Collection<Entity> allEntities = entityMap.getAllEntities(); // You'll need to implement this + + if (allEntities == null || allEntities.isEmpty()) { + System.out.println("No entities found"); + return; + } + + // Group entities by coordinates + Map<String, List<Entity>> coordMap = new HashMap<>(); + + for (Entity entity : allEntities) { + String coordKey = String.format("%.2f,%.2f,%.2f", entity.x, entity.y, entity.z); + coordMap.computeIfAbsent(coordKey, k -> new ArrayList<>()).add(entity); + } + + // Find and report duplicates + boolean foundDuplicates = false; + for (Map.Entry<String, List<Entity>> entry : coordMap.entrySet()) { + if (entry.getValue().size() > 1) { + foundDuplicates = true; + System.out.println("DUPLICATE COORDINATES: " + entry.getKey()); + for (Entity entity : entry.getValue()) { + System.out.println(" - " + entity.name + " (ID: " + entity.id + ") " + + "lastUpdated: " + entity.lastUpdated); + } + } + } + + if (!foundDuplicates) { + System.out.println("No duplicate coordinates found"); + } + + // Also check for entities with same ID but different objects + debugSameIdDifferentObjects(); + + System.out.println("Total entities: " + allEntities.size()); + System.out.println("Unique coordinates: " + coordMap.size()); + System.out.println("=== END DEBUG ==="); + } + + private static void debugSameIdDifferentObjects() { + System.out.println("=== DEBUG: Checking for same ID, different objects ==="); + + Collection<Entity> allEntities = entityMap.getAllEntities(); + Map<String, Set<Integer>> idMap = new HashMap<>(); + + for (Entity entity : allEntities) { + String idHash = entity.id + "|" + System.identityHashCode(entity); + idMap.computeIfAbsent(entity.id, k -> new HashSet<>()).add(System.identityHashCode(entity)); + } + + boolean foundDuplicates = false; + for (Map.Entry<String, Set<Integer>> entry : idMap.entrySet()) { + if (entry.getValue().size() > 1) { + foundDuplicates = true; + System.out.println("DUPLICATE ENTITY ID WITH DIFFERENT OBJECTS: " + entry.getKey()); + System.out.println(" Object hashes: " + entry.getValue()); + + // Show details of each duplicate + for (Entity entity : allEntities) { + if (entity.id.equals(entry.getKey())) { + System.out.println(" - " + entity.name + " @ " + + String.format("(%.2f,%.2f,%.2f)", entity.x, entity.y, entity.z) + + " lastUpdated: " + entity.lastUpdated + + " objHash: " + System.identityHashCode(entity)); + } + } + } + } + + if (!foundDuplicates) { + System.out.println("No duplicate entity objects found"); + } + System.out.println("=== END DEBUG ==="); + } + + private static float bytesToHeading(byte[] headingBytes) { + // Convert 2 bytes to 16-bit unsigned integer + int headingInt = ((headingBytes[1] & 0xFF) << 8) | (headingBytes[0] & 0xFF); + // Convert to degrees (0-360) + return (headingInt / 65535.0f) * 360.0f; + } + + private static void entityLocation(byte[] packet, Integer endIndex) { + byte[] entity_id = Arrays.copyOfRange(packet, endIndex + 1,endIndex + 6); + byte[] bytes_x = Arrays.copyOfRange(packet, endIndex + 21,endIndex + 25); + byte[] bytes_y = Arrays.copyOfRange(packet, endIndex + 26,endIndex + 30); + byte[] bytes_z = Arrays.copyOfRange(packet, endIndex + 31,endIndex + 35); + + float value_x = ByteBuffer.wrap(bytes_x).order(ByteOrder.LITTLE_ENDIAN).getFloat(); + float value_y = ByteBuffer.wrap(bytes_y).order(ByteOrder.LITTLE_ENDIAN).getFloat(); + float value_z = ByteBuffer.wrap(bytes_z).order(ByteOrder.LITTLE_ENDIAN).getFloat(); + + String entityIdHex = java.util.HexFormat.of().withUpperCase().formatHex(entity_id); + String name = entityDict.getOrDefault(entityIdHex, entityIdHex); + + Entity entity = entityMap.getEntity(entityIdHex); + if (entity == null) { + entity = new Entity(); + entity.id = entityIdHex; + entity.name = name; + } + + if (!name.equals(entityIdHex)) { + entity.name = name; + } + + entity.x = value_x; + entity.y = value_y; + entity.z = value_z; + entity.lastUpdated = System.currentTimeMillis(); + + entityMap.updateEntity(entity, player); + + String output = String.format( + "entity location - entity id: %s x: %s %.2f y: %s %.2f z: %s %.2f", + name, + java.util.HexFormat.of().withUpperCase().formatHex(bytes_x), value_x, + java.util.HexFormat.of().withUpperCase().formatHex(bytes_y), value_y, + java.util.HexFormat.of().withUpperCase().formatHex(bytes_z), value_z + ); +// System.out.println(output); + } + + private static void playerLocation(byte[] packet, Integer endIndex) { + byte[] entity_id = Arrays.copyOfRange(packet, endIndex + 1,endIndex + 6); + byte[] bytes_heading = Arrays.copyOfRange(packet, endIndex + 10,endIndex + 12); + byte[] bytes_x = Arrays.copyOfRange(packet, endIndex + 13,endIndex + 17); + byte[] bytes_y = Arrays.copyOfRange(packet, endIndex + 18,endIndex + 22); + byte[] bytes_z = Arrays.copyOfRange(packet, endIndex + 23,endIndex + 27); + + + float value_x = ByteBuffer.wrap(bytes_x).order(ByteOrder.LITTLE_ENDIAN).getFloat(); + float value_y = ByteBuffer.wrap(bytes_y).order(ByteOrder.LITTLE_ENDIAN).getFloat(); + float value_z = ByteBuffer.wrap(bytes_z).order(ByteOrder.LITTLE_ENDIAN).getFloat(); + float heading = bytesToHeading(bytes_heading); + + player.id = java.util.HexFormat.of().withUpperCase().formatHex(entity_id); + player.name = java.util.HexFormat.of().withUpperCase().formatHex(entity_id); + player.x = value_x; + player.y = value_y; + player.z = value_z; + player.heading = heading; + + String output = String.format( + "player location - entity id: %s x: %s %.2f y: %s %.2f z: %s %.2f", + java.util.HexFormat.of().withUpperCase().formatHex(entity_id), + java.util.HexFormat.of().withUpperCase().formatHex(bytes_x), value_x, + java.util.HexFormat.of().withUpperCase().formatHex(bytes_y), value_y, + java.util.HexFormat.of().withUpperCase().formatHex(bytes_z), value_z + ); +// System.out.println(output); + } + + private static void entitySpawn(byte[] packet, Integer endIndex) { + if (packet[endIndex + 7] == (byte) 0x01 || packet[endIndex + 7] == (byte) 0x04) { + return; + } + + Integer offset = 57; + + Integer entityType = packet[endIndex + 7] & 0xFF; + byte[] entityId = Arrays.copyOfRange(packet, endIndex + 1, endIndex + 6); + Integer nameLength = (packet[endIndex + offset] & 0xFF) + ((packet[endIndex + offset + 1] & 0xFF) << 8); + byte[] nameBytes = Arrays.copyOfRange(packet, endIndex + offset + 2, endIndex + offset + 1 + nameLength); + String entityName = new String(nameBytes, StandardCharsets.US_ASCII); + String entityIdHex = java.util.HexFormat.of().withUpperCase().formatHex(entityId); + + MatchResult result = subPacket("1c xx xx 00 02 00", packet, endIndex); + + if (result == null) { + byte[] last = Arrays.copyOfRange(packet, endIndex, packet.length); + result = new MatchResult(last, null, 0, last.length); + } + + byte[] subPacket = Arrays.copyOfRange(result.packet, 0, result.endIndex); + + BooyerMoore matcher = new BooyerMoore(); + matcher.addPattern("15 00 00 xx xx fb"); + + List<MatchResult> matches = matcher.search(subPacket); + + if (!matches.isEmpty()) { + MatchResult locationResult = matches.getFirst(); + Entity entity = entityMap.getEntity(entityIdHex); + if (entity == null) { + entity = new Entity(); + entity.name = entityName; + entity.id = entityIdHex; + } + + entity.lastUpdated = -1; + entity.type = entityType; + + byte[] bytes_heading = Arrays.copyOfRange(locationResult.packet, locationResult.endIndex - 2,locationResult.endIndex); + byte[] bytes_x = Arrays.copyOfRange(locationResult.packet, locationResult.endIndex + 1,locationResult.endIndex + 5); + byte[] bytes_y = Arrays.copyOfRange(locationResult.packet, locationResult.endIndex + 6,locationResult.endIndex + 10); + byte[] bytes_z = Arrays.copyOfRange(locationResult.packet, locationResult.endIndex + 11,locationResult.endIndex + 15); + + float value_x = ByteBuffer.wrap(bytes_x).order(ByteOrder.LITTLE_ENDIAN).getFloat(); + float value_y = ByteBuffer.wrap(bytes_y).order(ByteOrder.LITTLE_ENDIAN).getFloat(); + float value_z = ByteBuffer.wrap(bytes_z).order(ByteOrder.LITTLE_ENDIAN).getFloat(); + float heading = bytesToHeading(bytes_heading); + + entity.x = value_x; + entity.y = value_y; + entity.z = value_z; + entity.heading = heading; + + entityMap.updateEntity(entity, player); + +// System.out.println("Spawn location: x - " + entity.x + " y: " + entity.y + " z: " + entity.z); + } + + entityDict.put(java.util.HexFormat.of().withUpperCase().formatHex(entityId), entityName); + +// System.out.println("Entity Name: " + entityName + " Entity ID: " + entityIdHex); + } + + private static void chatMessage(byte[] packet, Integer endIndex) { + if ((packet[endIndex + 1] & 0xFF) > 0x09) { + return; + } + + Integer offset = 5; + Integer playerNameLength = (packet[endIndex + offset] & 0xFF) + ((packet[endIndex + offset + 1] & 0xFF) << 8); + byte[] playerNameBytes = Arrays.copyOfRange(packet, endIndex + offset + 2, endIndex + offset + 2 + playerNameLength); + String playerName = new String(playerNameBytes, StandardCharsets.US_ASCII); + + offset = endIndex + offset + 1 + playerNameLength; + Integer playerMessageLength = (packet[offset] & 0xFF) + ((packet[offset + 1] & 0xFF) << 8); + byte[] playerMessageBytes = Arrays.copyOfRange(packet, offset + 2, offset + 1 + playerMessageLength); + String playerMessage = new String(playerMessageBytes, StandardCharsets.US_ASCII); + +// System.out.println(playerName + " says: " + playerMessage); + } + + private static MatchResult subPacket(String pattern, byte[] packet, Integer start) { + BooyerMoore matcher = new BooyerMoore(); + matcher.addPattern(pattern); + + packet = Arrays.copyOfRange(packet, start, packet.length); + + List<MatchResult> matches = matcher.search(packet); + + if (!matches.isEmpty()) { + return matches.getFirst(); + } + + return null; + } + + public static void main(String[] args) { + SwingUtilities.invokeLater(() -> { + entityMap = new EntityMap(); + + JFrame frame = new JFrame("Minimap"); + frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + frame.setUndecorated(true); + frame.setAlwaysOnTop(true); + frame.setResizable(false); + frame.add(entityMap); + frame.pack(); + frame.setLocation(50, 50); + + // Make it square + Dimension size = entityMap.getPreferredSize(); + frame.setSize(size.width, size.height); + + frame.setVisible(true); + }); + + HashMap<String, BiConsumer<byte[], Integer>> dict = new HashMap<>(); + + BooyerMoore matcher = new BooyerMoore(); +// matcher.addPattern("00 00 d9 eb 00"); // pet mob + matcher.addPattern("00 75 5c 01"); // pet +// matcher.addPattern("1c xx xx 00 02 00"); +// matcher.addPattern("1a e7 00"); +// matcher.addPattern("1a e1 00"); +// matcher.addPattern("00 ff 00 00 00"); +// dict.put("1c fe fe 00 02 00", App::entitySpawn); +// dict.put("1c fe fe 00 02 00", App::entitySpawn); + +// dict.put("1a e7 00", App::entityLocation); +// dict.put("1a e1 00", App::playerLocation); +// dict.put("00 ff 00 00 00", App::chatMessage); + + try { + List<PcapIf> devices = Pcap.findAllDevs(); + + if (devices.isEmpty()) { + System.out.println("No devices found"); + return; + } + + System.out.println("Available devices:"); + for (int i = 0; i < devices.size(); i++) { + PcapIf device = devices.get(i); + System.out.println(i + ": " + device.name()); + System.out.println(" Description: " + device.description()); + System.out.println(); + } + + // Use the first device + String deviceName = devices.get(3).name(); + System.out.println("Using device: " + deviceName); + + try (Pcap pcap = Pcap.openOffline("pantheon-combat.pcapng")){ +// try (Pcap pcap = Pcap.create(deviceName)) { +// pcap.setImmediateMode(true); +// pcap.setBufferSize(64 * 1024); +// pcap.activate(); + BpFilter filter = pcap.compile("host 20.22.207.201", true, 0); + pcap.setFilter(filter); + + System.out.println("opened"); + pcap.loop(-1, (String user, PcapHeader header, byte[] packet) -> { + packet = Arrays.copyOfRange(packet, 42, packet.length); + List<MatchResult> matches = matcher.search(packet); + +// debugDuplicateCoordinates(); + + if (!matches.isEmpty()) { + System.out.println("Packet captured at: " + header.timestamp()); + System.out.println("Packet length: " + header.captureLength()); + long timestampLong = (long) header.timestamp(); + if (timestampLong == 1764204961927760L) { + for (int i = 0; i < packet.length; i++) { + System.out.printf("%02X ", packet[i]); + if ((i + 1) % 16 == 0) System.out.println(); // New line every 16 bytes + } + System.out.println("\n"); + } + } + + for (MatchResult match : matches) { + BiConsumer<byte[], Integer> handler = dict.get(match.toString()); + System.out.println(match.toString()); + if (handler != null) { + handler.accept(packet, match.endIndex); + } + } + + if (!matches.isEmpty()) { + for (int i = 0; i < packet.length; i++) { + System.out.printf("%02X ", packet[i]); + if ((i + 1) % 16 == 0) System.out.println(); // New line every 16 bytes + } + System.out.println("\n"); + } + + return; + }, "Packet data"); + } catch (PcapException e) { + System.out.println(e); + } + + } catch (PcapException e) { + System.out.println("Error: " + e.getMessage()); + } + } +}
\ No newline at end of file diff --git a/src/main/java/org/challman/pantheon_parser/Entity.java b/src/main/java/org/challman/pantheon_parser/Entity.java new file mode 100644 index 0000000..f43c731 --- /dev/null +++ b/src/main/java/org/challman/pantheon_parser/Entity.java @@ -0,0 +1,10 @@ +package org.challman.pantheon_parser; + +public class Entity { + public String id; + public String name; + public int type; + public float x, y, z; + public float heading; // in degrees + public long lastUpdated; +}
\ No newline at end of file diff --git a/src/main/java/org/challman/pantheon_parser/EntityMap.java b/src/main/java/org/challman/pantheon_parser/EntityMap.java new file mode 100644 index 0000000..36d96d6 --- /dev/null +++ b/src/main/java/org/challman/pantheon_parser/EntityMap.java @@ -0,0 +1,355 @@ +package org.challman.pantheon_parser; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class EntityMap extends JPanel { + private Map<String, Entity> entities = new ConcurrentHashMap<>(); + private Entity player; + private long entityTimeout = 10000; + private volatile boolean needsRepaint = false; + private double scale = 1.0; + private JButton zoomInButton, zoomOutButton; + private JButton closeButton; + private int controlPanelHeight = 25; + + // Drag variables + private Point dragStartPoint; + private Point windowStartPoint; + + public EntityMap() { + setOpaque(true); + setBackground(new Color(30, 30, 30)); + setPreferredSize(new Dimension(300, 300)); + setLayout(new BorderLayout()); + + // Create control panel + JPanel controlPanel = createControlPanel(); + add(controlPanel, BorderLayout.NORTH); + + // Add mouse listeners for dragging + addMouseListeners(); + + Timer timer = new Timer(33, e -> { + if (needsRepaint) { + repaint(); + needsRepaint = false; + } + }); + timer.start(); + } + + public Entity getEntity(String id) { + return entities.get(id); + } + + private JPanel createControlPanel() { + JPanel panel = new JPanel(new BorderLayout()); + panel.setOpaque(false); + panel.setPreferredSize(new Dimension(300, controlPanelHeight)); + panel.setBorder(BorderFactory.createEmptyBorder(2, 5, 2, 5)); + + // Left side: drag handle + JLabel dragHandle = new JLabel("≡ Minimap"); + dragHandle.setForeground(Color.LIGHT_GRAY); + dragHandle.setFont(new Font("Arial", Font.PLAIN, 10)); + dragHandle.setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR)); + + // Right side: control buttons + JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 3, 0)); + buttonPanel.setOpaque(false); + + zoomOutButton = new JButton("-"); + zoomInButton = new JButton("+"); + closeButton = new JButton("×"); + + styleButton(zoomOutButton); + styleButton(zoomInButton); + styleCloseButton(closeButton); + + zoomOutButton.addActionListener(e -> zoomOut()); + zoomInButton.addActionListener(e -> zoomIn()); + closeButton.addActionListener(e -> System.exit(0)); + + buttonPanel.add(zoomOutButton); + buttonPanel.add(zoomInButton); + buttonPanel.add(closeButton); + + panel.add(dragHandle, BorderLayout.WEST); + panel.add(buttonPanel, BorderLayout.EAST); + + return panel; + } + + private void styleButton(JButton button) { + button.setPreferredSize(new Dimension(25, 18)); + button.setFont(new Font("Arial", Font.BOLD, 10)); + button.setFocusable(false); + button.setBackground(new Color(60, 60, 60)); + button.setForeground(Color.WHITE); + button.setBorder(BorderFactory.createLineBorder(Color.GRAY)); + + button.addMouseListener(new MouseAdapter() { + @Override + public void mouseEntered(MouseEvent e) { + button.setBackground(new Color(80, 80, 80)); + } + + @Override + public void mouseExited(MouseEvent e) { + button.setBackground(new Color(60, 60, 60)); + } + }); + } + + private void styleCloseButton(JButton button) { + button.setPreferredSize(new Dimension(25, 18)); + button.setFont(new Font("Arial", Font.BOLD, 12)); + button.setFocusable(false); + button.setBackground(new Color(100, 60, 60)); + button.setForeground(Color.WHITE); + button.setBorder(BorderFactory.createLineBorder(new Color(120, 80, 80))); + + button.addMouseListener(new MouseAdapter() { + @Override + public void mouseEntered(MouseEvent e) { + button.setBackground(new Color(140, 80, 80)); + } + + @Override + public void mouseExited(MouseEvent e) { + button.setBackground(new Color(100, 60, 60)); + } + }); + } + + private void addMouseListeners() { + MouseAdapter dragAdapter = new MouseAdapter() { + @Override + public void mousePressed(MouseEvent e) { + dragStartPoint = e.getPoint(); + Window window = SwingUtilities.getWindowAncestor(EntityMap.this); + if (window != null) { + windowStartPoint = window.getLocation(); + } + } + + @Override + public void mouseDragged(MouseEvent e) { + if (dragStartPoint != null && windowStartPoint != null) { + Point currentPoint = e.getPoint(); + int deltaX = currentPoint.x - dragStartPoint.x; + int deltaY = currentPoint.y - dragStartPoint.y; + + Window window = SwingUtilities.getWindowAncestor(EntityMap.this); + if (window != null) { + Point newLocation = new Point( + windowStartPoint.x + deltaX, + windowStartPoint.y + deltaY + ); + window.setLocation(newLocation); + } + } + } + + @Override + public void mouseReleased(MouseEvent e) { + dragStartPoint = null; + windowStartPoint = null; + } + }; + + // Add listeners to the entire panel for dragging + addMouseListener(dragAdapter); + addMouseMotionListener(dragAdapter); + + // Also add to control panel for easier dragging + Component[] components = getComponents(); + for (Component comp : components) { + if (comp instanceof JPanel) { + comp.addMouseListener(dragAdapter); + comp.addMouseMotionListener(dragAdapter); + } + } + } + + private void zoomIn() { + scale *= 1.2; + if (scale > 10.0) scale = 10.0; + needsRepaint = true; + } + + private void zoomOut() { + scale /= 1.2; + if (scale < 0.1) scale = 0.1; + needsRepaint = true; + } + + public void updateEntity(Entity entity, Entity currentPlayer) { + entities.put(entity.id, entity); + if (currentPlayer != null) { + this.player = currentPlayer; + } + cleanupOldEntities(); + needsRepaint = true; + } + + public Collection<Entity> getAllEntities() { + return entities.values(); + } + + private void cleanupOldEntities() { + long currentTime = System.currentTimeMillis(); + entities.entrySet().removeIf(entry -> { + Entity entity = entry.getValue(); + return entity != player && entity.lastUpdated != -1 && (currentTime - entity.lastUpdated) > entityTimeout; + }); + } + + @Override + protected void paintComponent(Graphics g) { + super.paintComponent(g); + + Graphics2D g2d = (Graphics2D) g.create(); + g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + + if (player == null) { + g2d.dispose(); + return; + } + + // Calculate available space for the circle + int width = getWidth(); + int height = getHeight() - controlPanelHeight; + int size = Math.min(width, height); + int mapRadius = size / 2; + int centerX = width / 2; + int centerY = controlPanelHeight + (height / 2); + + // Draw circular map background (maximized) + g2d.setColor(new Color(20, 20, 20)); + g2d.fillOval(centerX - mapRadius, centerY - mapRadius, size, size); + + // Draw map border + g2d.setColor(new Color(100, 100, 100)); + g2d.setStroke(new BasicStroke(2)); + g2d.drawOval(centerX - mapRadius, centerY - mapRadius, size, size); + + // Draw grid lines + g2d.setColor(new Color(50, 50, 50)); + g2d.setStroke(new BasicStroke(1)); + g2d.drawLine(centerX, centerY - mapRadius, centerX, centerY + mapRadius); + g2d.drawLine(centerX - mapRadius, centerY, centerX + mapRadius, centerY); + + // Calculate scale with zoom factor + double maxDistance = 50.0 / scale; + double distanceScale = mapRadius / maxDistance; + + // Draw other entities + g2d.setFont(new Font("Arial", Font.PLAIN, 9)); + for (Entity entity : entities.values()) { + if (entity.id.equals(player.id)) continue; + + double dx = entity.x - player.x; + double dz = entity.z - player.z; + double distance = Math.sqrt(dx * dx + dz * dz); + + if (distance <= maxDistance) { + int x = centerX + (int)(dx * distanceScale); + int y = centerY - (int)(dz * distanceScale); + + double distFromCenter = Math.sqrt((x - centerX) * (x - centerX) + (y - centerY) * (y - centerY)); + if (distFromCenter <= mapRadius - 5) { + if (entity.type == 2) { + g2d.setColor(Color.CYAN); + } else if (entity.type == 3) { + g2d.setColor(Color.ORANGE); + } else { + g2d.setColor(Color.WHITE); + } + g2d.fillOval(x - 3, y - 3, 6, 6); + + if (distance < maxDistance * 0.9) { + g2d.setColor(Color.WHITE); + g2d.drawString(entity.id, x + 5, y); + } + } + } + } + + // Draw player + g2d.setColor(Color.RED); + g2d.fillOval(centerX - 4, centerY - 4, 8, 8); + + // Draw direction indicator + double directionAngle = Math.toRadians(player.heading); + int dirX = centerX + (int)((mapRadius - 10) * Math.sin(directionAngle)); + int dirY = centerY - (int)((mapRadius - 10) * Math.cos(directionAngle)); + g2d.setColor(Color.YELLOW); + g2d.setStroke(new BasicStroke(2)); + g2d.drawLine(centerX, centerY, dirX, dirY); + + // Draw UI elements in corners + drawCornerElements(g2d, centerX, centerY, mapRadius); + + g2d.dispose(); + } + + private void drawCornerElements(Graphics2D g2d, int centerX, int centerY, int mapRadius) { + g2d.setColor(Color.WHITE); + g2d.setFont(new Font("Arial", Font.PLAIN, 10)); + + // Top-left: Scale indicator + int topLeftX = centerX - mapRadius + 5; + int topLeftY = centerY - mapRadius + 15; + g2d.drawString(String.format("Zoom: %.1fx", scale), topLeftX, topLeftY); + + // Top-right: Entity count + int topRightX = centerX + mapRadius - 60; + int topRightY = centerY - mapRadius + 15; + int entityCount = entities.size() - (player != null ? 1 : 0); + g2d.drawString(String.format("Entities: %d", entityCount), topRightX, topRightY); + + // Bottom-left: Player coordinates + int bottomLeftX = centerX - mapRadius + 5; + int bottomLeftY = centerY + mapRadius - 5; + g2d.drawString(String.format("(%.1f, %.1f)", player.x, player.z), bottomLeftX - 10, bottomLeftY); + + // Bottom-right: Compass heading + int bottomRightX = centerX + mapRadius - 50; + int bottomRightY = centerY + mapRadius - 5; + g2d.drawString(String.format("%.0f°", player.heading), bottomRightX, bottomRightY); + + // Draw mini compass in top-right corner + drawMiniCompass(g2d, centerX + mapRadius - 20, centerY - mapRadius + 35, player.heading); + } + + private void drawMiniCompass(Graphics2D g2d, int x, int y, float heading) { + int radius = 8; + + g2d.setColor(new Color(50, 50, 50, 200)); + g2d.fillOval(x - radius, y - radius, radius * 2, radius * 2); + g2d.setColor(Color.WHITE); + g2d.drawOval(x - radius, y - radius, radius * 2, radius * 2); + + // Draw N indicator + g2d.setFont(new Font("Arial", Font.BOLD, 8)); + g2d.drawString("N", x - 2, y - radius - 1); + + double needleAngle = Math.toRadians(heading); + int needleX = x + (int)(radius * Math.sin(needleAngle)); + int needleY = y - (int)(radius * Math.cos(needleAngle)); + + g2d.setColor(Color.RED); + g2d.setStroke(new BasicStroke(1.5f)); + g2d.drawLine(x, y, needleX, needleY); + g2d.setStroke(new BasicStroke(1)); + } +}
\ No newline at end of file diff --git a/src/main/java/org/challman/pantheon_parser/algorithm/AhoCorasick.java b/src/main/java/org/challman/pantheon_parser/algorithm/AhoCorasick.java new file mode 100644 index 0000000..5a0e51b --- /dev/null +++ b/src/main/java/org/challman/pantheon_parser/algorithm/AhoCorasick.java @@ -0,0 +1,164 @@ +package org.challman.pantheon_parser.algorithm; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Queue; + +public class AhoCorasick { + private final Node root; + private boolean built; + private static final byte WILDCARD = (byte) 0xFF; // Use 0xFF as wildcard marker + + public AhoCorasick() { + this.root = new Node(); + this.built = false; + } + + public void addPattern(byte[] pattern) { + if (built) throw new IllegalStateException("Cannot add patterns after building"); + addPatternRecursive(root, pattern, 0); + } + private void addPatternRecursive(Node node, byte[] pattern, int index) { + if (index == pattern.length) { + node.patterns.add(pattern); + node.isEnd = true; + return; + } + + byte current = pattern[index]; + if (current == WILDCARD) { + // Wildcard: add transitions for all possible bytes (0-255) + for (int b = 0; b <= 255; b++) { + byte byteVal = (byte) b; + Node child = node.children.computeIfAbsent(byteVal, k -> new Node()); + addPatternRecursive(child, pattern, index + 1); + } + } else { + // Normal byte + Node child = node.children.computeIfAbsent(current, k -> new Node()); + addPatternRecursive(child, pattern, index + 1); + } + } + + public void addPattern(String hexPattern) { + // Replace 'xx' with wildcard marker + hexPattern = hexPattern.replaceAll("xx", "ff"); + addPattern(hexToBytes(hexPattern)); + } + + public void build() { + Queue<Node> queue = new LinkedList<>(); + + // Initialize root's children failure to root + for (Node child : root.children.values()) { + child.failure = root; + queue.add(child); + } + + // Build failure links using BFS + while (!queue.isEmpty()) { + Node current = queue.poll(); + + for (Map.Entry<Byte, Node> entry : current.children.entrySet()) { + byte b = entry.getKey(); + Node child = entry.getValue(); + + Node failure = current.failure; + while (failure != root && !failure.children.containsKey(b)) { + failure = failure.failure; + } + + if (failure.children.containsKey(b)) { + child.failure = failure.children.get(b); + } else { + child.failure = root; + } + + // Collect output links + child.output.addAll(child.failure.output); + if (child.isEnd) { + child.output.addAll(child.patterns); + } + + queue.add(child); + } + } + built = true; + } + + public List<MatchResult> search(byte[] data) { + if (!built) build(); + + List<MatchResult> results = new ArrayList<>(); + Node current = root; + + for (int i = 0; i < data.length; i++) { + byte b = data[i]; + + // Follow failure links until we find a valid transition + while (current != root && !current.children.containsKey(b)) { + current = current.failure; + } + + if (current.children.containsKey(b)) { + current = current.children.get(b); + } else { + current = root; + } + + // Collect all matches at this position + for (byte[] pattern : current.output) { + int startIndex = i - pattern.length + 1; + results.add(new MatchResult(pattern, startIndex, i)); + } + } + + return results; + } + + private static class Node { + Map<Byte, Node> children = new HashMap<>(); + Node failure = null; + boolean isEnd = false; + List<byte[]> patterns = new ArrayList<>(); + List<byte[]> output = new ArrayList<>(); + } + + public static class MatchResult { + public final byte[] pattern; + public final int startIndex; + public final int endIndex; + + public MatchResult(byte[] pattern, int startIndex, int endIndex) { + this.pattern = pattern; + this.startIndex = startIndex; + this.endIndex = endIndex; + } + + @Override + public String toString() { + return String.format(bytesToHex(pattern)); + } + } + + // Utility methods + private static byte[] hexToBytes(String hex) { + hex = hex.replaceAll("\\s+", ""); + byte[] data = new byte[hex.length() / 2]; + for (int i = 0; i < data.length; i++) { + data[i] = (byte) Integer.parseInt(hex.substring(i * 2, i * 2 + 2), 16); + } + return data; + } + + private static String bytesToHex(byte[] bytes) { + StringBuilder sb = new StringBuilder(); + for (byte b : bytes) { + sb.append(String.format("%02x ", b)); + } + return sb.toString().trim(); + } +}
\ No newline at end of file diff --git a/src/main/java/org/challman/pantheon_parser/algorithm/BooyerMoore.java b/src/main/java/org/challman/pantheon_parser/algorithm/BooyerMoore.java new file mode 100644 index 0000000..1f05652 --- /dev/null +++ b/src/main/java/org/challman/pantheon_parser/algorithm/BooyerMoore.java @@ -0,0 +1,194 @@ +package org.challman.pantheon_parser.algorithm; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + + +public class BooyerMoore { + private final List<byte[]> patterns = new ArrayList<>(); + + public void addPattern(byte[] pattern) { + patterns.add(pattern); + } + + public void addPattern(String hexPattern) { + hexPattern = hexPattern.replaceAll("xx", "fe"); + addPattern(hexToBytes(hexPattern)); + } + + public List<MatchResult> search(byte[] data) { + List<MatchResult> results = new ArrayList<>(); + + for (byte[] pattern : patterns) { + for (int i = 0; i <= data.length - pattern.length; i++) { + boolean match = true; + for (int j = 0; j < pattern.length; j++) { + if (pattern[j] != (byte) 0xFE && data[i + j] != pattern[j]) { + match = false; + break; + } + } + if (match) { + results.add(new MatchResult(data, pattern, i, i + pattern.length - 1)); + i += pattern.length - 1; // Skip ahead to avoid overlapping matches + } + } + } + + return results; + } + + public static class MatchResult { + public final byte[] packet; + public final byte[] pattern; + public final int startIndex; + public final int endIndex; + + public MatchResult(byte[] packet, byte[] pattern, int startIndex, int endIndex) { + this.packet = packet; + this.pattern = pattern; + this.startIndex = startIndex; + this.endIndex = endIndex; + } + + @Override + public String toString() { + return String.format(bytesToHex(pattern)); + } + } + + // Utility methods + private static byte[] hexToBytes(String hex) { + hex = hex.replaceAll("\\s+", ""); + byte[] data = new byte[hex.length() / 2]; + for (int i = 0; i < data.length; i++) { + data[i] = (byte) Integer.parseInt(hex.substring(i * 2, i * 2 + 2), 16); + } + return data; + } + + private static String bytesToHex(byte[] bytes) { + StringBuilder sb = new StringBuilder(); + for (byte b : bytes) { + sb.append(String.format("%02x ", b)); + } + return sb.toString().trim(); + } +} +// +//public class BooyerMoore { +// private final List<PatternInfo> patterns = new ArrayList<>(); +// private static final byte WILDCARD = (byte) 0xFE; +// +// public void addPattern(byte[] pattern) { +// patterns.add(new PatternInfo(pattern, buildBadCharTable(pattern))); +// } +// +// public void addPattern(String hexPattern) { +// hexPattern = hexPattern.replaceAll("xx", "fe"); +// addPattern(hexToBytes(hexPattern)); +// } +// +// public List<MatchResult> search(byte[] data) { +// List<MatchResult> results = new ArrayList<>(); +// +// for (PatternInfo patternInfo : patterns) { +// int i = 0; +// while (i <= data.length - patternInfo.pattern.length) { +// int j = patternInfo.pattern.length - 1; +// +// // Check from right to left +// while (j >= 0 && (patternInfo.pattern[j] == WILDCARD || +// data[i + j] == patternInfo.pattern[j])) { +// j--; +// } +// +// if (j < 0) { +// // Match found +// results.add(new MatchResult(data, patternInfo.pattern, i, i + patternInfo.pattern.length - 1)); +// i += patternInfo.pattern.length; // Move to next position +// } else { +// // Use bad character rule to skip ahead +// int skip = patternInfo.badCharTable.getOrDefault(data[i + j], patternInfo.pattern.length); +// // If the pattern byte at mismatch position is wildcard, we can't skip +// if (patternInfo.pattern[j] == WILDCARD) { +// i += 1; // Just move one position +// } else { +// i += Math.max(1, skip - (patternInfo.pattern.length - 1 - j)); +// } +// } +// } +// } +// +// return results; +// } +// +// private Map<Byte, Integer> buildBadCharTable(byte[] pattern) { +// Map<Byte, Integer> table = new HashMap<>(); +// for (int i = 0; i < pattern.length - 1; i++) { +// if (pattern[i] != WILDCARD) { +// // Only add non-wildcard bytes to the table +// table.put(pattern[i], pattern.length - 1 - i); +// } +// } +// return table; +// } +// +// private static class PatternInfo { +// final byte[] pattern; +// final Map<Byte, Integer> badCharTable; +// +// PatternInfo(byte[] pattern, Map<Byte, Integer> badCharTable) { +// this.pattern = pattern; +// this.badCharTable = badCharTable; +// } +// } +// +// +// private static class Node { +// Map<Byte, Node> children = new HashMap<>(); +// Node failure = null; +// boolean isEnd = false; +// List<byte[]> patterns = new ArrayList<>(); +// List<byte[]> output = new ArrayList<>(); +// } +// +// public static class MatchResult { +// public final byte[] packet; +// public final byte[] pattern; +// public final int startIndex; +// public final int endIndex; +// +// public MatchResult(byte[] packet, byte[] pattern, int startIndex, int endIndex) { +// this.packet = packet; +// this.pattern = pattern; +// this.startIndex = startIndex; +// this.endIndex = endIndex; +// } +// +// @Override +// public String toString() { +// return String.format(bytesToHex(pattern)); +// } +// } +// +// // Utility methods +// private static byte[] hexToBytes(String hex) { +// hex = hex.replaceAll("\\s+", ""); +// byte[] data = new byte[hex.length() / 2]; +// for (int i = 0; i < data.length; i++) { +// data[i] = (byte) Integer.parseInt(hex.substring(i * 2, i * 2 + 2), 16); +// } +// return data; +// } +// +// private static String bytesToHex(byte[] bytes) { +// StringBuilder sb = new StringBuilder(); +// for (byte b : bytes) { +// sb.append(String.format("%02x ", b)); +// } +// return sb.toString().trim(); +// } +//}
\ No newline at end of file diff --git a/src/test/java/org/challman/pantheon_parser/AppTest.java b/src/test/java/org/challman/pantheon_parser/AppTest.java new file mode 100644 index 0000000..1f81ad6 --- /dev/null +++ b/src/test/java/org/challman/pantheon_parser/AppTest.java @@ -0,0 +1,19 @@ +package org.challman.pantheon_parser; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +/** + * Unit test for simple App. + */ +public class AppTest { + + /** + * Rigorous Test :-) + */ + @Test + public void shouldAnswerWithTrue() { + assertTrue(true); + } +} |