File added.
Preview size limit exceeded, changes collapsed.
File changed.
Preview size limit exceeded, changes collapsed.
+204
−0
File added.
Preview size limit exceeded, changes collapsed.
Loading
/invite <player> posts a chat card to the invitee with a green [Accept] and a red [Deny] button, and Accept walks them to the server the inviter is standing on. It is a UX wrapper over `/felis go` and nothing more: the accept runs the same doGo path on the ACCEPTING player's own verified uuid, so a stored invite carries a server name and never an identity to act as, and the prompt needs no unguessable token. Why it can be this simple: an invite can only name the server its sender is currently on, so the target is running by construction, and a running felis server already admits any linked player through <name>.<root-domain> on the link check alone (WaitingRouter.onServerPreConnect) — no wake, no autostartPolicy consultation. Hence enqueueFromInvite: a READY backend is joined directly, and only the not-ready case falls through to the policy-gated wake path unchanged. Routing an accept through wakeAndWaitLinked would have asked the API to wake a server that needs no waking, and autostartPolicy defaults to ownerOnly, so the API would answer 403 and the green button would do nothing for exactly the people you would invite. It is not consequence-free, and the inviter is told so at send time rather than in a comment only we read. Landing on a felis server records the player in its allowlist (onServerConnected -> join-event -> RecordJoin); on an autostartPolicy=allowlist server that row is what lets them come back and START the thing later. The same row they would earn by walking in unaided — the invite shortened the walk, it did not widen the door — but it outlives the invite, so accessNotice says so. The wording follows the policy: only under allowlist does it claim they will be able to start the server themselves, because under the ownerOnly default (and the empty string the API reports for an unset field) that row grants no waking and the claim would be a lie. InviteBook also holds a 30s per-sender cooldown, because the one capability /invite genuinely adds is "make a chat card appear on any online player", and unrated that is a way to follow someone around their own chat log. It is charged in put() rather than at the top of the command, so an invite refused for an offline name or a player already on the server costs the sender nothing, and the gate sits after every other validation for the same reason. The stamp is global per sender on purpose: a per-(sender, invitee) key would wave through one player papering the whole proxy, which is the thing being limited. The card lives in InviteCard as a pure function so the buttons — the whole point of the feature — can be asserted without a live proxy, and the button clicks are pinned to the server the card named, so a stale card cannot answer a newer invite (checked with peek before the invite is spent, so refusing a superseded card leaves the live one answerable). Verified: production gradle 8.14 + JDK 21 build; jar carries the plugin classes and no test classes; InviteBookTest (36 checks) and InviteCardTest (48 checks); and a live Velocity 3.5.1 that loads the jar, registers /invite <player> and answers /invite accept <server>, with a malformed subcommand as a negative control.
File added.
Preview size limit exceeded, changes collapsed.
File changed.
Preview size limit exceeded, changes collapsed.
File added.
Preview size limit exceeded, changes collapsed.