Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions Code/client/PluginAPI.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,9 @@ extern "C" __declspec(dllexport) SkyrimTogetherAPI::ChannelHandle SkyrimTogether
{
return PluginService::Get().RegisterScopedChannel(apName, aScope, aOnMessage, aOnPeersChanged);
}

// Also outside the V1 vtable, for the same reason.
extern "C" __declspec(dllexport) uint32_t SkyrimTogetherGetPartyLeaderId()
{
return PluginService::Get().GetPartyLeaderId();
}
8 changes: 8 additions & 0 deletions Code/client/Services/Generic/PluginService.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,12 @@ bool PluginService::IsInParty() const noexcept
return m_inParty;
}

uint32_t PluginService::GetPartyLeaderId() const noexcept
{
std::lock_guard cLock(m_lock);
return m_partyLeaderId;
}

SendResult PluginService::Queue(ChannelHandle aChannel, uint32_t aTargetPlayerId, const char* apBytes, size_t aLength) noexcept
{
if (aLength > kMaxMessageSize || (aLength != 0 && !apBytes))
Expand Down Expand Up @@ -138,6 +144,7 @@ void PluginService::Update() noexcept
const uint32_t cLocalId = cOnline ? world.GetTransport().GetLocalPlayerId() : 0;
const auto& party = world.GetPartyService();
const bool cInParty = cOnline && party.IsInParty();
const uint32_t cLeaderId = cInParty ? party.GetLeaderPlayerId() : 0;
Vector<uint32_t> members, nearby;
if (cInParty)
for (uint32_t id : party.GetPartyMembers())
Expand All @@ -162,6 +169,7 @@ void PluginService::Update() noexcept
std::lock_guard cLock(m_lock);
m_localPlayerId = cLocalId;
m_inParty = cInParty;
m_partyLeaderId = cLeaderId;
m_members = members;
m_nearby = nearby;
for (Channel& channel : m_channels)
Expand Down
3 changes: 3 additions & 0 deletions Code/client/Services/PluginService.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ struct PluginService final : SkyrimTogetherAPI::IVSkyrimTogether1
SkyrimTogetherAPI::SendResult SendTo(SkyrimTogetherAPI::ChannelHandle aChannel, uint32_t aPlayerId, const char* apBytes, size_t aLength) noexcept override;
uint32_t GetLocalPlayerId() const noexcept override;
bool IsInParty() const noexcept override;
// Not part of the V1 vtable, exported as SkyrimTogetherGetPartyLeaderId. 0 while not in a party.
[[nodiscard]] uint32_t GetPartyLeaderId() const noexcept;

/**
* @brief Hands a relayed Mod Message to the plugin on its channel, if any. Runs during the World update.
Expand Down Expand Up @@ -55,4 +57,5 @@ struct PluginService final : SkyrimTogetherAPI::IVSkyrimTogether1
Vector<uint32_t> m_nearby;
uint32_t m_localPlayerId{0};
bool m_inParty{false};
uint32_t m_partyLeaderId{0};
};
17 changes: 17 additions & 0 deletions Code/plugin_api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,23 @@ send means the target is not in the current nearby snapshot (the existing result
The server independently checks every recipient: party equality, or matching interior/worldspace
and loaded-grid range. Both client and server must run the new protocol build.

## Party leader

The party leader hosts the world: NPCs, scenes and quest progression run on the leader's client. Plugins
that move NPCs or start scenes, or that need one authoritative client, can look up
`SkyrimTogetherGetPartyLeaderId` using `GetPartyLeaderId_t` from the header:

```cpp
auto getLeader = reinterpret_cast<SkyrimTogetherAPI::GetPartyLeaderId_t>(
GetProcAddress(GetModuleHandleW(nullptr), SkyrimTogetherAPI::kGetPartyLeaderIdName));
const uint32_t leader = getLeader ? getLeader() : 0;
const bool isLeader = leader != 0 && leader == api->GetLocalPlayerId();
```

It returns 0 while not in a party, and is absent on older executables, so plugins need a fallback (for
example: act alone when not in a party). It reads the same locked snapshot as the other getters, so a
leadership change shows up after the next World update.

## Lifetime and resynchronization

The registration callback reports audience joins/leaves. Nearby membership follows loaded remote player
Expand Down
6 changes: 6 additions & 0 deletions Code/plugin_api/include/SkyrimTogetherAPI.h
Original file line number Diff line number Diff line change
Expand Up @@ -83,4 +83,10 @@ using RequestPluginAPI_t = void* (*)(InterfaceVersion aVersion);
// Callbacks run in the World update context; queue engine work through SKSE AddTask.
using RegisterScopedChannel_t = ChannelHandle (*)(const char*, ChannelScope, MessageCallback, PartyChangedCallback);
constexpr const char* kRegisterScopedChannelName = "SkyrimTogetherRegisterScopedChannel";
// Optional extension, independent of the V1 vtable. Older executables return no symbol.
// The player ID of the party leader, who hosts the world's NPCs and quest scenes; 0 while not in a party.
// Compare with GetLocalPlayerId() to know whether this client leads. Callable from any thread (locked snapshot,
// refreshed every World update), so it can briefly lag a leadership change.
using GetPartyLeaderId_t = uint32_t (*)();
constexpr const char* kGetPartyLeaderIdName = "SkyrimTogetherGetPartyLeaderId";
} // namespace SkyrimTogetherAPI