<file_sep># Player finder
This is a shadowplay-friendly player finder for SAMP 0.3.7 that you can use to find other players while playing & recording without it showing up on your recordings. This only works on players that are in the same interior as you. 

On the SAMP side, a Lua script is used to read information such as real-time player coordinates, name, skin, current vehicle, gun, etc. Sockets are used to forward this information to a Java application. The Java application listens for updates at localhost:5230 and displays the received data on a map. To display the map over SAMP, an autohotkey script is used, which must be run with admin privileges. This solution is p hacky but I'm not rewriting this. You, however, are more than welcome to.
![img](https://i.imgur.com/AOXesbX.jpg)

# Requirements
* [JDK 11](https://jdk.java.net/archive/)
* [AutoHotkey](https://www.autohotkey.com/) - if you wish to compile the AHK script yourself.
* [MoonLoader](https://gtaforums.com/topic/890987-moonloader/) - MoonLoader requires an ASI loader, if you don't already have CLEO, install it.
* [SAMPFUNCS](https://blast.hk/threads/17/page-138#post-279414)
* [LuaSocket](https://blast.hk/threads/16031/)
* [SAMP.Lua](https://github.com/THE-FYP/SAMP.Lua)

# Installation
1) Install [CLEO](https://cleo.li/) and [MoonLoader](https://gtaforums.com/topic/890987-moonloader/) if you haven't already.
2) Download and unzip [THIS ARCHIVE](https://drive.google.com/file/d/1zBZlq9cK74Osc22R1kFN_iBukQSnncWU/view?usp=sharing). It contains SAMPFUNCS, SAMP.Lua, LuaSocket, playerfinder.lua, the java app to display a second map over your game, and a compiled AHK file to give the java app priority over SAMP.
3) Drag the contents of the archive's gta san andreas folder into your gta san andreas folder.
4) Run playerfinder.jar. An empty minimap should appear.
5) Run PrioritizeWindow.exe. It requires admin privileges to display the map over SAMP.
6) Run SAMP and you should see the minimap appear over your game.

If you would like to build the Java application manually, you will need to download the [skin & map images](https://www.upload.ee/files/11121136/gui.rar.html) and place all 313 of them into `src/main/resources` as follows: [img](https://i.imgur.com/TAF8HRe.png "Structure")

Running `gradlew jar` in the Java project root will then build the jar. Sources for everything else, including the lua script and the AHK script used to display the map over SAMP are present in this repo.

# Usage
* While in game with the Java application running, hold NUMPAD+ and type the ID of a player you would like to find. If they are connected and in the same interior as you, you should now begin to receive real-time location updates.
* When they are out of range, you will only receive basic information (coordinates, name).
* When they are close to you, you will also receive additional information such as their health/armor, current car/weapon and skin and more frequent location updates.
* To stop finding people, just press Numpad+ again.
* To close the Java application, make it active by clicking on it and then hit ESC.

# Credits
* [FYP](https://github.com/THE-FYP). All of this would have been way harder without the samp hacking tools he's graciously published over the past decade.
* [this jlink project i used as a starting point.](https://bitbucket.org/FlPe/javafx_jlink_example/src/master/)
<file_sep>package server;

import model.GameInformation;
import service.GuiUpdaterService;

import java.io.IOException;
import java.net.Socket;
import java.util.Scanner;

public class SocketConnection implements Runnable {
    private Socket socket;
    private GuiUpdaterService updaterService;

    public SocketConnection(Socket socket, GuiUpdaterService updaterService) {
        this.socket = socket;
        this.updaterService = updaterService;
    }

    @Override
    public void run() {
        System.out.println("Connected: " + socket);
        try {
            System.out.println("scanning");
            var in = new Scanner(socket.getInputStream());
            while (in.hasNextLine()) {
                String line = in.nextLine();
                System.out.println("received line: " + line);
                parseLine(line);
            }
        } catch (Exception e) {
            System.out.println("Error:" + socket);
        } finally {
            closeConnection();
        }
    }

    public void closeConnection() {
        try {
            socket.close();
        } catch (IOException e) {
            System.out.println("Unable to close connection");
        }
        System.out.println("Closed: " + socket);
    }
    private void parseLine(String line) {
        String[] splitLine = line.split(";");
        if (splitLine.length == 1) {
            updaterService.updateName(new GameInformation(splitLine[0]));
        } else if (splitLine.length == 7) {
            try {
                updaterService.updateValues(createGameInformation(splitLine));
            } catch (Exception e) {
                System.out.println("Error parsing line");
            }
        }
    }

    private GameInformation createGameInformation(String[] line) {
        return new GameInformation(Double.parseDouble(line[0]), Double.parseDouble(line[1]), line[2], line[3], line[4], line[5], line[6]);
    }
}
<file_sep>rootProject.name = 'playerfinder'<file_sep>package gui;

import javafx.geometry.Insets;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.Font;
import javafx.stage.Stage;
import javafx.stage.StageStyle;

import java.util.Objects;

public class GuiElementHolder {

    private static final String DEFAULT_SKIN = "74";
    private static final String MAP = "map";
    private static final String RESOURCE_ROOT = "gui/";
    private static final String PNG = ".png";
    private static final String SKIN_PREFIX = "Skin_";
    private static final String TITLE = "Playerfinder";

    private Label name = new Label();
    private Label distance = new Label();
    private Label hp = new Label();
    private Label weapon = new Label();
    private ImageView skin = new ImageView();
    private ImageView map = new ImageView();
    private BorderPane borderPane = new BorderPane();
    private VBox text = new VBox();
    private StackPane sp = new StackPane();
    private Rectangle marker;
    private String skinModel = "";


    public GuiElementHolder(Stage primaryStage, Group root) {
        setUpLabels();
        initMap();
        setUpPanes();
        setUpScene(primaryStage, root);
    }

    private Image loadImageByName(String name) {
        String path = RESOURCE_ROOT + name + PNG;
        return new Image(Objects.requireNonNull(getClass().getClassLoader().getResourceAsStream(path)));
    }

    private void setUpScene(Stage primaryStage, Group root) {
        Scene scene = new Scene(root);
        root.getChildren().add(sp);
        primaryStage.setResizable(false);
        primaryStage.setHeight(325.0);
        primaryStage.setWidth(300.0);
        primaryStage.setScene(scene);
        primaryStage.setTitle(TITLE);
        primaryStage.initStyle(StageStyle.UNDECORATED);
        primaryStage.setAlwaysOnTop(true);
    }

    private void initMap() {
        updateSkin(DEFAULT_SKIN);
        setImage(map, MAP);
        initMarker();
        sp.setMaxSize(300, 300);
        map.setScaleX(3);
        map.setScaleY(3);
        map.setTranslateX(-350);
        map.setTranslateY(-350);
    }

    private void initMarker() {
        marker = new Rectangle(145, 145, 10, 10);
        marker.setStroke(Color.BLACK);
        marker.setFill(Color.RED);
    }

    private void setUpLabels() {
        setUpLabel(name);
        setUpLabel(distance);
        setUpLabel(hp);
        setUpLabel(weapon);
    }

    private void setUpLabel(Label label) {
        label.setFont(Font.font(20));
        label.setStyle("-fx-font-weight: bold");
        label.setBackground(new Background(new BackgroundFill(Color.BLACK, CornerRadii.EMPTY, Insets.EMPTY)));
        label.setTextFill(Color.WHITE);
    }

    private void setUpPanes() {

        HBox hBox = new HBox();
        text.getChildren().addAll(name, distance, hp, weapon, skin);
        hBox.getChildren().addAll(text);

        borderPane.setTop(hBox);
        borderPane.getChildren().add(marker);
        sp.getChildren().addAll(map, borderPane);
    }

    public String getDistanceText() {
        return this.distance.getText();
    }

    public void setDistanceText(String distance) {
        this.distance.setText(distance);
    }

    public void setHpText(String hp) {
        this.hp.setText(hp);
    }

    public void setWeaponText(String weapon) {
        this.weapon.setText(weapon);
    }

    public void setNameText(String text) {
        this.name.setText(text);
    }

    public void updateMarkerPosition(Double x, Double y) {
        map.setTranslateX(x);
        map.setTranslateY(y);
    }

    public void updateSkin(String id) {
        if (id != null && !id.equals(skinModel) && id.chars().allMatch(Character::isDigit)) {
            skinModel = id;
            setImage(skin, SKIN_PREFIX + id);
        }
    }

    private void setImage(ImageView imageView, String img) {
        imageView.setImage(loadImageByName(img));
    }

    public void resetValues() {
        setHpText("");
        setDistanceText("");
        setWeaponText("");
        updateSkin(DEFAULT_SKIN);
        updateMarkerPosition(-350.0, -350.0);
    }
}
<file_sep>require "lib.moonloader"
require "lib.sampfuncs"

local host, port = "127.0.0.1", 5230
local socket = require("socket")
local weapons = require 'game.weapons'

local HOLD_NUMPAD = "Hold NUMPAD+ to begin"

function main()
  while not isSampAvailable() do wait(0) end

	local connected = false
	local id = nil
	local previous = ""

	while true do
		wait(100)
		if not connected then
			tcp = connect()
			connected = true
		else
			if isKeyDown(107) then
				id, connected = wait_for_id(tcp, previous)
			end
		end
		previous, connected = handle_id(id, tcp, previous)
	end
end

function handle_id(id, tcp, previous)
	if id ~= nil then
		if sampIsPlayerConnected(id) then
			return find_player(id, tcp, previous)
		else
			return send_message("Player " .. id .. " not connected", tcp, previous)
		end
	else
		return send_message(HOLD_NUMPAD, tcp, previous)
	end
end

function find_player(id, tcp, previous)
	local x1, y1, z1 = getCharCoordinates(PLAYER_PED)
	local streamed, ped = sampGetCharHandleBySampPlayerId(id)
	local name = sampGetPlayerNickname(id)
	if not streamed then
		return send_streamed_out_info(id, x1, y1, z1, name, tcp, previous)
	end
	return send_streamed_in_info(ped, id, x1, y1, z1, name, tcp, previous)
end

function send_streamed_out_info(id, x1, y1, z1, name, tcp, previous)
	local x, y, success, distance = get_streamed_out_info(id, x1, y1, z1)
	if not success then
		return send_message("Unable to locate player", tcp, previous)
	end
	return send_message(x .. ";" .. y .. ";" .. name .. ";" .. distance .. "; ; ; ", tcp, previous)
end

function send_streamed_in_info(ped, id, x1, y1, z1, name, tcp, previous)
	local x, y, distance, skin, health, additional_info = get_streamed_info(ped, id, x1, y1, z1)
	return send_message(x .. ";" .. y .. "; " .. name .. ";" .. distance .. ";" .. skin .. ";" .. health .. ";" .. additional_info, tcp, previous)
end

function get_streamed_out_info(i, x1, y1, z1)
	local success, x, y, z = sampGetStreamedOutPlayerPos(i)
	if success then
		distance = math.floor(getDistanceBetweenCoords3d(x1, y1, z1, x, y, z))
		direction = get_direction(x, y, z, x1, y1, z1)
		return x, y, success, distance .. "m " .. " " .. direction
	end
	return nil, nil, success, nil
end

function get_streamed_info(ped, id, x1, y1, z1)
	local x, y, z = getCharCoordinates(ped)
	local distance = math.floor(getDistanceBetweenCoords3d(x1, y1, z1, x, y, z))
	local direction = get_direction(x, y, z, x1, y1 ,z1)
	local skin = getCharModel(ped)
	local health = sampGetPlayerHealth(id)
	local armor = sampGetPlayerArmor(id)
	local additional_info = get_additional_info(ped)
	return x, y, distance .. "m " .. direction, skin, "HP: " .. health .. ", Armor: " ..  armor, additional_info
end

function get_additional_info(ped)
	if isCharInAnyCar(ped) then
		local model = getCarModel(getCarCharIsUsing(ped))
		return getNameOfVehicleModel(model)
	else
		local weapon, _, _ = getCurrentCharWeapon(ped)
		return weapons.get_name(weapon)
	end
end

function connect()
	local connected = false
	local tcp = assert(socket.tcp())
	while not connected do
		tcp:connect(host, port)
		tcp:settimeout(0)
		wait(1000)
		_, connected = send_message(HOLD_NUMPAD, tcp)
	end
	return tcp
end

function get_direction(x, y, z, x1, y1, z1)
	local long = ""
	local lat = ""
	if exceeds_long_threshold(x, y, x1, y1) then
		if y > y1 then long = "NORTH " else long = "SOUTH " end
	end
	if exceeds_lat_threshold(x, y, x1, y1) then
		if x > x1 then lat = "EAST" else lat = "WEST" end
	end
	return long .. lat
end

	function exceeds_long_threshold(x, y, x1, y1)
		return 3 * math.sqrt(math.pow((y - y1), 2)) >= math.sqrt(math.pow((x - x1), 2))
	end

function exceeds_lat_threshold(x, y, x1, y1)
	return math.sqrt(math.pow((y - y1), 2)) <= 3 * math.sqrt(math.pow((x - x1), 2))
end

function send_message(message, tcp, previous)
	if message ~= previous then
		return send_message(message, tcp)
	end
	return message, true
end

function send_message(message, tcp)
	local index, _ = tcp:send(message .. "\n")
	return message, index == string.len(message) + 1
end

function wait_for_id(tcp, previous)
	local input, connected = send_message("ID: ", tcp, previous)
	while isKeyDown(107) and connected do
		wait(10)
		if wasKeyPressed(96) or wasKeyPressed(48) then
			input, connected = send_message(input .. "0", tcp)
		elseif wasKeyPressed(97) or wasKeyPressed(49) then
			input, connected = send_message(input .. "1", tcp)
		elseif wasKeyPressed(98) or wasKeyPressed(50) then
			input, connected = send_message(input .. "2", tcp)
		elseif wasKeyPressed(99) or wasKeyPressed(51) then
			input, connected = send_message(input .. "3", tcp)
		elseif wasKeyPressed(100) or wasKeyPressed(52) then
			input, connected = send_message(input .. "4", tcp)
		elseif wasKeyPressed(101) or wasKeyPressed(53) then
			input, connected = send_message(input .. "5", tcp)
		elseif wasKeyPressed(102) or wasKeyPressed(54) then
			input, connected = send_message(input .. "6", tcp)
		elseif wasKeyPressed(103) or wasKeyPressed(55) then
			input, connected = send_message(input .. "7", tcp)
		elseif wasKeyPressed(104) or wasKeyPressed(56) then
			input, connected = send_message(input .. "8", tcp)
		elseif wasKeyPressed(105) or wasKeyPressed(57) then
			input, connected = send_message(input .. "9", tcp)
		elseif wasKeyPressed(8) then
			if string.len(input) > 4 then
				input, connected = send_message(input:sub(1, -2), tcp, previous)
			end
		end
	end
	if string.len(input) > 4 then
		return tonumber(input:sub(5)), connected
	end
	return nil, connected
end
<file_sep>plugins {
    id 'application'
    id 'org.openjfx.javafxplugin' version '0.0.5'
}

repositories {
    mavenCentral()
}

dependencies {
}

javafx {
    modules = ['javafx.controls']
}

mainClassName = 'gui.Main'

jar {
    manifest {
        attributes 'Main-Class': 'gui.Launcher'
    }
    from {
        configurations.runtime.collect {
            it.isDirectory() ? it : zipTree(it)
        }
        configurations.compile.collect {
            it.isDirectory() ? it : zipTree(it)
        }
    }
}

sourceSets {
    main {
        java {
            srcDirs 'src/main/java'
        }
        resources {
            srcDirs 'src/main/resources'
        }
    }
}