Skip to content

Commit

Permalink
add Archipelago Multiworld support
Browse files Browse the repository at this point in the history
  • Loading branch information
black-sliver committed Jan 31, 2022
1 parent e8fbb2a commit 86acb5d
Show file tree
Hide file tree
Showing 12 changed files with 550 additions and 8 deletions.
6 changes: 6 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
[submodule "lib/lua"]
path = lib/lua
url = git://github.com/lua/lua
[submodule "lib/apclientpp"]
path = lib/apclientpp
url = https://github.com/black-sliver/apclientpp.git
[submodule "lib/wswrap"]
path = lib/wswrap
url = https://github.com/black-sliver/wswrap.git
5 changes: 4 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ SRC = $(wildcard $(SRC_DIR)/*.cpp) \
$(wildcard $(SRC_DIR)/core/*.cpp) \
$(wildcard $(SRC_DIR)/usb2snes/*.cpp) \
$(wildcard $(SRC_DIR)/uat/*.cpp) \
$(wildcard $(SRC_DIR)/ap/*.cpp) \
$(wildcard $(LIB_DIR)/wswrap/src/*.cpp) \
$(wildcard $(SRC_DIR)/http/*.cpp) \
$(wildcard $(LIB_DIR)/tinyfiledialogs/*.cpp)
#lib/gifdec/gifdec.c
Expand All @@ -19,8 +21,9 @@ HDR = $(wildcard $(SRC_DIR)/*.h) \
$(wildcard $(SRC_DIR)/core/*.h) \
$(wildcard $(SRC_DIR)/usb2snes/*.h) \
$(wildcard $(SRC_DIR)/uat/*.h) \
$(wildcard $(SRC_DIR)/ap/*.h) \
$(wildcard $(SRC_DIR)/http/*.h)
INCLUDE_DIRS = -Ilib -Ilib/lua -Ilib/asio/include -DASIO_STANDALONE -Ilib/miniz -Ilib/json/include -Ilib/valijson/include -Ilib/tinyfiledialogs #-Ilib/gifdec
INCLUDE_DIRS = -Ilib -Ilib/lua -Ilib/asio/include -DASIO_STANDALONE -Ilib/miniz -Ilib/json/include -Ilib/valijson/include -Ilib/tinyfiledialogs -Ilib/wswrap/include #-Ilib/gifdec
WIN32_INCLUDE_DIRS = -Iwin32-lib/i686/include
WIN32_LIB_DIRS = -L./win32-lib/i686/bin -L./win32-lib/i686/lib
WIN64_INCLUDE_DIRS = -Iwin32-lib/x86_64/include
Expand Down
25 changes: 25 additions & 0 deletions doc/AUTOTRACKING.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
as provided by QUsb2Snes, Usb2Snes and SNI
* Variables
* [UAT Protocol](https://github.com/black-sliver/UAT)
* Archipelago Multiworld
* see [archipelago.gg](https://archipelago.gg)


## Atomicity
Expand Down Expand Up @@ -111,3 +113,26 @@ ScriptHost:AddVariableWatch("Alchemy", {"acid_rain"}, updateAlchemy)
```

See [examples/uat-example](../examples/uat-example) for a full example.


## Archipelago Interface

This is fully callback-driven.

Archipelago auto-tracking is enabled by adding "ap" to a variant's flags in
manifest.json and clicking on "AT" when the pack is loaded.

* when connection to a server is established, Clear handlers are called so the
Lua script can clear all locations and items.
* when an item is received, Item handlers are called
* when a location is checked, Location handlers are called
* when a location is scouted, Scout handlers are called
* when a bounce is received, Bounced handlers are called

### global Archipelago
* `.PlayerNumber` returns the slot number of the connected player or -1 if not connected
* `:AddClearHandler(name, callback)` called when connecting to a (new) server and state should be cleared
* `:AddItemHandler(name, callback)` called when an item is received; args: index, item_id, item_name
* `:AddLocationHandler(name, callback)` called when a location was checked; args: location_id, location_name
* `:AddScoutHandler(name, callback)` called when a location was scouted; args: location_id, location_name, item_id, item_name, item_player
* `:AddBouncedHandler(name, callback)` called when the server sends a bounce; args: json bounce message as table
1 change: 1 addition & 0 deletions lib/apclientpp
Submodule apclientpp added at c4a329
1 change: 1 addition & 0 deletions lib/wswrap
Submodule wswrap added at 4be367
13 changes: 13 additions & 0 deletions src/ap/aptracker.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#include "aptracker.h"
#include <map>
#include <string>


const std::map<std::string, std::string> APTracker::_errorMessages = {
{ "InvalidSlot", "Invalid Slot; please verify that you have connected to the correct world." },
{ "InvalidGame", "Invalid Game; please verify that you connected with the right game to the correct world." },
{ "SlotAlreadyTaken", "Player slot already in use for that team" },
{ "IncompatibleVersion", "Server reported your client version as incompatible" },
{ "InvalidPassword", "Wrong password" },
{ "InvalidItemsHandling", "The item handling flags requested by the client are not supported" }
};
229 changes: 229 additions & 0 deletions src/ap/aptracker.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
#ifndef _AP_APTRACKER_H
#define _AP_APTRACKER_H

// APTracker wraps APClient to provide a simpler interface to core/AutoTracker
#include <apclientpp/apclient.hpp>
#include <algorithm>
#include <string>
#include <map>
#include <list>
#include "../core/signal.h"
#include "../core/fileutil.h"
#include <stdlib.h>
#include <utime.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <time.h>


class APTracker final {
typedef nlohmann::json json;
public:
APTracker(const std::string& appname, const std::string& game="")
: _appname(appname), _game(game)
{
// AP uses an UUID to detect reconnects (replace old connection). If
// stored UUID is older than 60min, the connection should already be
// dropped, so we can generate a new one.
bool newUuid = false;
auto uuidFilename = getConfigPath(_appname, UUID_FILENAME);
struct stat st;
if (stat(uuidFilename.c_str(), &st) == 0) {
time_t elapsed = time(nullptr) - st.st_mtime;
newUuid = elapsed > 3600;
}
if (!newUuid) newUuid = !readFile(uuidFilename, _uuid);
if (!newUuid) newUuid = _uuid.empty();
if (newUuid) {
// generate a new uuid thread-safe
_uuid.clear();
struct timespec ts;
unsigned rng;
if (clock_gettime(CLOCK_REALTIME, &ts) == 0) {
rng = (unsigned)ts.tv_sec;
rng ^= (unsigned)ts.tv_nsec;
} else {
rng = (unsigned)time(nullptr);
}
const char uuid_chars[] = "0123456789abcdef";
for (int n=0; n<16; n++)
_uuid += uuid_chars[rand_r(&rng) % strlen(uuid_chars)];
writeFile(uuidFilename, _uuid);
} else {
// update mtime of uuid file
utime(uuidFilename.c_str(), nullptr);
}
std::string s;
if (readFile(getConfigPath(_appname, DATAPACKAGE_FILENAME), s)) {
try {
_datapackage = json::parse(s);
} catch (...) { }
}
}

virtual ~APTracker()
{
disconnect(true);
}

bool connect(const std::string& uri, const std::string& slot, const std::string& pw)
{
disconnect(true);

if (uri.empty()) {
fprintf(stderr, "APTracker: uri can't be empty!\n");
onError.emit(this, "Invalid server");
return false;
}
if (slot.empty()) {
fprintf(stderr, "APTracker: slot can't be empty!\n");
onError.emit(this, "Invalid slot name");
return false;
}

_ap = new APClient(_uuid, _game, uri);
_ap->set_data_package(_datapackage);
_ap->set_socket_connected_handler([this, slot, pw]() {
auto lock = EventLock(_event);
std::list<std::string> tags = {"Tracker"};
if (_game.empty()) tags.push_back("IgnoreGame");
return _ap->ConnectSlot(slot, pw, 0b111, tags);
});
_ap->set_slot_refused_handler([this](const std::list<std::string>& errs) {
auto lock = EventLock(_event);
auto contains = [](const std::list<std::string>& list, const std::string& element) {
return std::find(list.begin(), list.end(), element) != list.end();
};
if (errs.empty()) {
onError.emit(this, "Connection refused, no reason provided.");
goto clean_up;
}
for (const auto& pair : _errorMessages) {
if (contains(errs, pair.first)) {
onError.emit(this, pair.second);
goto clean_up;
}
}
onError.emit(this, "Connection refused: " + errs.front());
clean_up:
disconnect();
});
_ap->set_slot_connected_handler([this]() {
auto lock = EventLock(_event);
onStateChanged.emit(this, _ap->get_state());
printf("TODO: clean up state\n");
_itemIndex = 0;
onClear.emit(this);

// update mtime of uuid file
touchUUID();
});
_ap->set_slot_disconnected_handler([this]() {
auto lock = EventLock(_event);
onStateChanged.emit(this, _ap->get_state());
});
_ap->set_data_package_changed_handler([this](const json& datapackage) {
_datapackage = datapackage;
writeFile(getConfigPath(_appname, DATAPACKAGE_FILENAME), _datapackage.dump());
});
_ap->set_bounced_handler([this](const json& packet) {
auto lock = EventLock(_event);
onBounced.emit(this, packet);
});
_ap->set_items_received_handler([this](const std::list<APClient::NetworkItem>& items) {
auto lock = EventLock(_event);
for (const auto& item: items) {
if (item.index != _itemIndex) continue;
// TODO: Sync if item.index > _itemIndex
onItem.emit(this, item.index, item.item, _ap->get_item_name(item.item));
_itemIndex++;
}
});
_ap->set_location_checked_handler([this](const std::list<int>& locations) {
auto lock = EventLock(_event);
for (int location: locations) {
onLocationChecked.emit(this, location, _ap->get_location_name(location));
}
});
_ap->set_location_info_handler([this](const std::list<APClient::NetworkItem>& scouts) {
auto lock = EventLock(_event);
for (const auto& scout: scouts) {
onScout.emit(this, scout.location, _ap->get_location_name(scout.location),
scout.item, _ap->get_item_name(scout.item), scout.player);
}
});
return true;
}

void disconnect(bool force=false)
{
if (_event && !force) {
// we can not stop the service while a callback is being executed
_scheduleDisconnect = true;
return;
}
bool wasConnected = _ap && _ap->get_state() != APClient::State::DISCONNECTED;
if (_ap) delete _ap;
_ap = nullptr;
if (wasConnected)
onStateChanged.emit(this, APClient::State::DISCONNECTED);
_scheduleDisconnect = false;
}

bool poll()
{
if (_event) {
fprintf(stderr, "APTracker: polling from a signal is unsupported\n");
return false;
}
if (_scheduleDisconnect) disconnect();
if (_ap) _ap->poll();
return false;
}

void touchUUID()
{
// update mtime of the uuid file so we reuse the uuid on next connect
utime(getConfigPath(_appname, UUID_FILENAME).c_str(), nullptr);
}

int getPlayerNumber() const
{
return _ap ? _ap->get_player_number() : -1;
}

Signal<const std::string&> onError;
Signal<APClient::State> onStateChanged;
Signal<> onClear; // called when state has to be cleared
Signal<int, int, const std::string&> onItem; // index, item, item_name
Signal<int, const std::string&, int, const std::string&, int> onScout; // location, location_name, item, item_name, target player
Signal<int, const std::string&> onLocationChecked; // location, location_name
Signal<const json&> onBounced; // packet

private:
APClient* _ap = nullptr;
std::string _appname;
std::string _game;
std::string _uuid;
json _datapackage;
int _itemIndex = 0;
std::set<int> _checkedLocations;
std::set<int> _uncheckedLocations;
int _event = 0;
bool _scheduleDisconnect = false;

static const std::map<std::string, std::string> _errorMessages;

constexpr static char DATAPACKAGE_FILENAME[] = "ap-datapackage.json";
constexpr static char UUID_FILENAME[] = "ap-uuid.txt";

struct EventLock final
{
EventLock(int& event) : _event(event) { _event++; }
~EventLock() { _event--; }
private:
int& _event;
};
};

#endif // _AP_APTRACKER_H
Loading

0 comments on commit 86acb5d

Please sign in to comment.