add death link support, add needed archipelago item messages to language yamls

Archipelago items will still be green rupees for now until we decide how we want to handle custom game assets
This commit is contained in:
CraftyBoss
2026-06-24 03:22:42 -07:00
parent 5a1368b355
commit 7ee39edee7
12 changed files with 186 additions and 11 deletions
+11
View File
@@ -23,6 +23,8 @@
#include "d/actor/d_a_npc_tkc.h"
#include <cstring>
#include "dusk/archipelago/archipelago_context.hpp"
#if TARGET_PC
#include "dusk/imgui/ImGuiConsole.hpp"
#include "dusk/settings.h"
@@ -3302,6 +3304,15 @@ int daAlink_c::procCoDeadInit(int param_0) {
field_0x3080 = 0;
mProcVar5.field_0x3012 = 0x3F;
field_0x3198 = -1;
#if TARGET_PC
// trigger archipelago death link if applicable
if (dusk::archi::ArchipelagoContext::IsConnected()) {
dusk::archi::ArchipelagoContext::TryHandleDeathLink();
}
#endif
return 1;
}
+5 -2
View File
@@ -16,6 +16,7 @@
#include "dusk/string.hpp"
#include "dusk/version.hpp"
#include "dusk/archipelago/archipelago_context.hpp"
dFile_info_c::dFile_info_c(JKRArchive* i_archive, u8 param_1) {
mArchive = i_archive;
@@ -120,9 +121,11 @@ int dFile_info_c::setSaveData(dSv_save_c* i_savedata, BOOL i_validChksum, u8 i_d
// If this is a randomizer file
auto curFileSeedHash = dusk::getSettings().randomizer.seedHashes.at(i_dataNo).getValue();
if (!curFileSeedHash.empty()) {
// Overwrite "Save time" text with "Randomizer"
bool isArchipelago = dusk::archi::ArchipelagoContext::IsSeedHashArchipelago(curFileSeedHash);
// Overwrite "Save time" text with "Randomizer" or "Archipelago"
auto saveTimeText = (J2DTextBox*)mFileInfo.Scr->search(MULTI_CHAR('f_s_t_02'));
dusk::SafeStringCopy(saveTimeText->getStringPtr(), "Randomizer");
dusk::SafeStringCopy(saveTimeText->getStringPtr(), isArchipelago ? "Archipelago" : "Randomizer");
saveTimeText->setHBinding(J2DTextBoxHBinding::HBIND_LEFT);
// Overwrite the "Total play time" text with the seed hash
+81 -8
View File
@@ -4,6 +4,7 @@
#include "Archipelago.h"
#include "d/d_item.h"
#include "d/actor/d_a_alink.h"
#include "dusk/config.hpp"
#include "dusk/logging.h"
#include "dusk/randomizer/game/tools.h"
@@ -136,6 +137,17 @@ void ParseMessageData() {
auto msg = AP_GetLatestMessage();
switch (msg->type) {
case AP_MessageType::ItemSend: {
auto sendMsg = (AP_ItemSendMessage*)msg;
ui::push_toast({
.title = "Item Sent",
.content = fmt::format("Sent {} to {}", sendMsg->item, sendMsg->recvPlayer),
.duration = std::chrono::seconds(3),
});
DuskLog.info("[{}] {}", getMessageTypeName(msg->type), msg->text);
break;
}
case AP_MessageType::ItemRecv: {
auto recvMsg = (AP_ItemRecvMessage*)msg;
@@ -147,7 +159,6 @@ void ParseMessageData() {
// fallthrough for debug logging text contents
}
case AP_MessageType::Plaintext:
case AP_MessageType::ItemSend:
case AP_MessageType::Hint:
case AP_MessageType::Countdown:
DuskLog.info("[{}] {}", getMessageTypeName(msg->type), msg->text);
@@ -266,6 +277,45 @@ std::string ArchipelagoContext::getLocationNameFromApId(int apId) const {
return "";
}
bool ArchipelagoContext::tryKillPlayer() {
if (!m_isNeedPlayerDeath)
return false;
auto linkActor = daAlink_getAlinkActorClass();
if (!linkActor)
return false;
switch (linkActor->mProcID) {
case daAlink_c::PROC_WAIT:
case daAlink_c::PROC_TIRED_WAIT:
case daAlink_c::PROC_MOVE:
case daAlink_c::PROC_WOLF_WAIT:
case daAlink_c::PROC_WOLF_TIRED_WAIT:
case daAlink_c::PROC_WOLF_MOVE:
case daAlink_c::PROC_ATN_MOVE:
case daAlink_c::PROC_WOLF_ATN_AC_MOVE: {
// Check if link is currently in a cutscene
if (linkActor->checkEventRun())
break;
// Ensure that link is not currently in a message-based event.
if (linkActor->getEventId() != 0)
break;
dComIfGs_setLife(0);
m_isNeedPlayerDeath = false;
return true;
}
default:
break;
}
return false;
}
ArchipelagoContext::ArchipelagoContext() = default;
void ArchipelagoContext::SetServerIp(const std::string_view& ip) {
@@ -295,7 +345,7 @@ const std::string& ArchipelagoContext::GetPassword() {
std::string ArchipelagoContext::GetArchipelagoSeedName() {
if (IsConnected()) {
auto& roomInfo = instance().m_roomInfo;
return fmt::format("AP_{}_{}", GetSlotName(), roomInfo.seed_name);
return fmt::format("AP_{}", roomInfo.seed_name);
}else {
DuskLog.fatal("Archipelago was not connected when attempting to get seed name!");
}
@@ -332,6 +382,13 @@ bool ArchipelagoContext::ConnectToServer(bool isBlocking) {
AP_NetworkVersion ver{0, 6,7};
AP_SetClientVersion(&ver);
AP_SetDeathLinkSupported(true);
AP_SetDeathLinkRecvCallback([](std::string source, std::string cause) {
DuskLog.info("Player {} sent death link. Cause: {}", source, cause);
RequestPlayerDeath(true);
});
AP_SetItemClearCallback([]() {
DuskLog.info("Item Clear Callback Called!");
instance().m_isNeedResetInv = true;
@@ -395,14 +452,11 @@ bool ArchipelagoContext::IsConnected() {
}
void ArchipelagoContext::MessageThreadFunc() {
// wait a bit before checking connection state, as websocket is probably not connected yet
// (i really am not liking APCpp, why cant I check if the websocket is in the process of connecting???)
std::this_thread::sleep_for(std::chrono::seconds(2));
DuskLog.info("AP Thread started.");
if (IsConnected()) {
AP_GetRoomInfo(&instance().m_roomInfo);
instance().m_isEnableDeathLink = AP_IsDeathLinkEnabled();
RequestAllLocationScout();
}
@@ -425,6 +479,12 @@ void ArchipelagoContext::Execute() {
return; // end execution early so next frame can re-add inventory if needed
}
// process death links
if (instance().tryKillPlayer()) {
// if successful, don't bother processing item queue or location checks
return;
}
// drain pending item queue here
instance().m_queueMutex.lock();
if (!instance().m_receivedItemsQueue.empty()) {
@@ -541,7 +601,8 @@ void ArchipelagoContext::HandleReceiveLocationScout(const std::vector<AP_Network
};
}
}
// TODO: atm this is a sort of lazy solution to not having direct access to what location was checked when an execItemGet is called
// so eventually finding a way to properly associate locations with their respective item get funcs would benefit this system
void ArchipelagoContext::UpdateCheckedLocations() {
auto& world = instance().m_archiWorld;
@@ -556,7 +617,7 @@ void ArchipelagoContext::UpdateCheckedLocations() {
auto locName = location->GetName();
if (!instance().m_locationItemInfo.contains(locName)) {
DuskLog.warn("No item found for ({}).", locName);
DuskLog.debug("No item found for ({}).", locName);
continue;
}
@@ -659,6 +720,13 @@ bool ArchipelagoContext::IsReceivedLocationScouts() {
return !instance().m_locationItemInfo.empty();
}
void ArchipelagoContext::TryHandleDeathLink() {
if (instance().m_isEnableDeathLink && !instance().m_isFromDeathLink) {
// TODO: come up with better death messages
AP_DeathLinkSend("%YOU% was unable to become the Hero of Twilight.");
}
}
void ArchipelagoContext::RequestAllLocationScout(bool isHint) {
std::set<int64_t> locations;
// TEMP: apworld has 475 locations with ids in sequential order, so add them all individually to location set
@@ -670,6 +738,11 @@ void ArchipelagoContext::RequestAllLocationScout(bool isHint) {
AP_SendLocationScouts(locations, isHint);
}
void ArchipelagoContext::RequestPlayerDeath(bool isDeathLink) {
instance().m_isNeedPlayerDeath = true;
instance().m_isFromDeathLink = isDeathLink;
}
bool ArchipelagoContext::GenerateConfigFromAP(randomizer::seedgen::config::Config& config, const std::string& settingsStr) {
YAML::Node apConfigYaml;
try {
@@ -37,12 +37,15 @@ namespace dusk::archi
bool m_isUpdateLocations = false;
bool m_isNeedResetInv = false;
bool m_isAllowUpdateLocations = false;
bool m_isEnableDeathLink = false;
// AP Data
std::unordered_map<std::string, GameLocationInfo> m_locationItemInfo;
std::map<int, bool> m_initLocationCollectState;
AP_RoomInfo m_roomInfo;
std::string m_SettingsFile;
bool m_isNeedPlayerDeath = false;
bool m_isFromDeathLink = false;
// TEMP
std::map<int, TEMP_GameItemInfo> m_apItemToGameItem;
@@ -57,6 +60,8 @@ namespace dusk::archi
int getItemIdFromApId(int apId);
std::string getLocationNameFromApId(int apId) const;
bool tryKillPlayer();
public:
ArchipelagoContext();
@@ -112,10 +117,14 @@ namespace dusk::archi
static bool IsReceivedLocationScouts();
static void TryHandleDeathLink();
// State Requesters
static void RequestAllLocationScout(bool isHint = false);
static void RequestPlayerDeath(bool isDeathLink = false);
// AP -> Internal Rando Converters
static bool GenerateConfigFromAP(randomizer::seedgen::config::Config& config, const std::string& settingsStr);
+10
View File
@@ -62,6 +62,16 @@ void ImGuiArchipelagoDebug::drawWindow() {
}
}
ImGui::SeparatorText("Debug Buttons");
static bool isSimulateDeathLink = false;
if (ImGui::Button("Request Death Link")) {
archi::ArchipelagoContext::RequestPlayerDeath(isSimulateDeathLink);
}
ImGui::SameLine();
ImGui::Checkbox("Simulate Death Link", &isSimulateDeathLink);
ImGui::End();
}
}
@@ -288,6 +288,14 @@ Progressive Wallet:
Cryptic:
Text: a {money bag}
Archipelago Item:
Standard:
Text: Archipelago Item
Pretty:
Text: an {Archipelago Item}
Cryptic:
Text: an {item from another world}
Upper Zoras River Portal:
Standard:
Text: Upper Zoras River Portal
@@ -1604,6 +1612,11 @@ No Required Dungeons Text:
Text: No Required Dungeons
# ITEM GET TEXT
Archipelago Item Get Item Text:
Standard:
Text: |-
<fast>You got an <red>Archipelago Item<white>!
Foolish Get Item Text:
Standard:
Text: |-
@@ -288,6 +288,14 @@ Progressive Wallet:
Cryptic:
Text: a {money bag}
Archipelago Item:
Standard:
Text: Archipelago Item
Pretty:
Text: an {Archipelago Item}
Cryptic:
Text: an {item from another world}
Upper Zoras River Portal:
Standard:
Text: Upper Zoras River Portal
@@ -1604,6 +1612,11 @@ No Required Dungeons Text:
Text: No Required Dungeons
# ITEM GET TEXT
Archipelago Item Get Item Text:
Standard:
Text: |-
<fast>You got an <red>Archipelago Item<white>!
Foolish Get Item Text:
Standard:
Text: |-
@@ -288,6 +288,14 @@ Progressive Wallet:
Cryptic:
Text: a {money bag}
Archipelago Item:
Standard:
Text: Archipelago Item
Pretty:
Text: an {Archipelago Item}
Cryptic:
Text: an {item from another world}
Upper Zoras River Portal:
Standard:
Text: Upper Zoras River Portal
@@ -1604,6 +1612,11 @@ No Required Dungeons Text:
Text: No Required Dungeons
# ITEM GET TEXT
Archipelago Item Get Item Text:
Standard:
Text: |-
<fast>You got an <red>Archipelago Item<white>!
Foolish Get Item Text:
Standard:
Text: |-
@@ -288,6 +288,14 @@ Progressive Wallet:
Cryptic:
Text: a {money bag}
Archipelago Item:
Standard:
Text: Archipelago Item
Pretty:
Text: an {Archipelago Item}
Cryptic:
Text: an {item from another world}
Upper Zoras River Portal:
Standard:
Text: Upper Zoras River Portal
@@ -1604,6 +1612,11 @@ No Required Dungeons Text:
Text: No Required Dungeons
# ITEM GET TEXT
Archipelago Item Get Item Text:
Standard:
Text: |-
<fast>You got an <red>Archipelago Item<white>!
Foolish Get Item Text:
Standard:
Text: |-
@@ -288,6 +288,14 @@ Progressive Wallet:
Cryptic:
Text: a {money bag}
Archipelago Item:
Standard:
Text: Archipelago Item
Pretty:
Text: an {Archipelago Item}
Cryptic:
Text: an {item from another world}
Upper Zoras River Portal:
Standard:
Text: Upper Zoras River Portal
@@ -1604,6 +1612,11 @@ No Required Dungeons Text:
Text: No Required Dungeons
# ITEM GET TEXT
Archipelago Item Get Item Text:
Standard:
Text: |-
<fast>You got an <red>Archipelago Item<white>!
Foolish Get Item Text:
Standard:
Text: |-
@@ -11,6 +11,10 @@
# Group: 0
# Message Id: 122
- Name: Archipelago Item Get Item Text
Group: 0
Message Id: 321
- Name: Shadow Crystal Get Item Text
Group: 0
Message Id: 151
+1 -1
View File
@@ -56,7 +56,7 @@ message(STATUS "randomizer: Fetching APCpp")
FetchContent_Declare(
APCpp
GIT_REPOSITORY https://github.com/CraftyBoss/APCpp.git
GIT_TAG 3557bbb
GIT_TAG 2d92f75
)
FetchContent_MakeAvailable(yaml-cpp base64pp battery-embed APCpp)