Skip to content

FULL_WEAPON_RESOLUTION without short packets: server silently sends no torpedo data for other players #12

Description

@dabibbit

Note: I validated this playing on a 2.17.0 server, and playing netrek without seeing anyone else's torps is suboptimal. I delegated the writeup, so it is more comprehensive that I would have been on my own. I'm guessing that this behavior was not encountered before because most of the clients implemented short packets long before full weapon resolution was incorporated, but future new clients will likely hit it if they delay implementation of short packets and get full weapon resolution working earlier in development.

BTW: Thanks for adding PROTOCOL.MD. It is helpful.

Summary

Two client-negotiated options — the FULL_WEAPON_RESOLUTION feature (CP_FEATURE) and
short/variable packets (CP_S_REQ / SP_S_REPLY) — are negotiated independently and the
server accepts either without reference to the other. However, the torpedo emitter code
assumes that FULL_WEAPON_RESOLUTION implies short packets: with the feature on,
updateTorps() skips every torp that is not the client's own, on the stated assumption that
those torps will be carried by SupdateTorps() instead — but SupdateTorps() is only ever
called inside the send_short branch of the per-update dispatch.

The consequence is that a client which enables FULL_WEAPON_RESOLUTION but does not enable
short packets receives no torpedo packets at all for any player other than itself. Other
players' torpedoes exist server-side and damage and kill normally, but are never transmitted,
so they are invisible to that client. Nothing in the negotiation reports this: the feature
reply is a plain accept, and no warning, error, or degraded reply indicates that a category of
gameplay-critical data will be withheld for the life of the connection.

This is deterministic from the code path, not timing-dependent, and it is reachable by any
client that reads the feature list and enables an advertised feature without also implementing
short packets — a plausible position for a new or third-party client, since short packets are a
substantial separate protocol layer.

Trigger conditions

All of the following, and nothing else, are required:

  1. The server's feature file advertises FULL_WEAPON_RESOLUTION as a server-side ('S') feature
    with value ON. The shipped sample does this: docs/sample_features:79
    FULL_WEAPON_RESOLUTION S ON.
  2. The client sends a CP_FEATURE request for FULL_WEAPON_RESOLUTION with value == 1.
  3. The client never sends CP_S_REQ with SPK_VON (or sends SPK_VOFF), so send_short
    remains 0. send_short is 0 by default (ntserv/data.c:213, int send_short = 0;).
  4. The client is a normal player, i.e. me->p_status != POBSERV. Observers are not
    affected — see the observer special case below.

There is no ordering requirement between (2) and (3); the state is read fresh on every update.

Mechanism

1. The feature is accepted with no precondition

ntserv/feature.c:62 registers the feature against the server flag:

{"FULL_WEAPON_RESOLUTION",    &F_full_weapon_resolution,    NULL},

getFeature() (ntserv/feature.c:175) sets the flag directly from the client's requested
value, with no reference to send_short or to any other feature (ntserv/feature.c:212-221):

for(i=0; feature_vars[i].name ; i++){
   if(feature_cmp(cpack->name, feature_vars[i].name)) {
    if(feature_vars[i].var){
       if(cpack->value == 1)
          *feature_vars[i].var = (spack->value == 1);

handleFeature() (ntserv/socket.c:2149-2166) calls getFeature() and returns the reply
immediately; there is no validation stage and no deferred check. The reply the client receives
is an ordinary accept with value 1. The default is off: ntserv/data.c:400,
int F_full_weapon_resolution = 0;.

2. updateTorps() drops all non-own torps when the flag is set

ntserv/genspkt.c:1400 updateTorps(). Own torps (and observers) are emitted first
(:1414-1419):

if (myTorp(t)
    || (F_full_weapon_resolution && me->p_status == POBSERV)
) {
    sndTorp(tpi, tp, t, i, UPDT_ALL);
    continue;
}

Everything else is then discarded outright when the flag is on, before any of the in-view /
out-of-view logic runs (:1421-1427):

/*
 * Not my torp.
 * If not in view, only send limited information
 * Full weapon resolution torps are skipped, they are instead
 * sent via SupdateTorps */
if (F_full_weapon_resolution)
    continue;

The comment is the design assumption made explicit: non-own torps are expected to be picked up
by SupdateTorps(). Note also that this continue precedes the UPDT_LITTLE path used to
signal TFREE/out-of-view, so not even torp-teardown information is sent.

The observer case at :1415 short-circuits before the skip, so an observer with the feature on
and short packets off still receives all torps at UPDT_ALL. The failure is specific to normal
players.

3. SupdateTorps() is unreachable without short packets

ntserv/genspkt.c:1926 SupdateTorps() is the short-packet torp emitter, and it is the
mirror image of the above — it skips the client's own torps when the feature is on
(:1969-1973):

if (torp->t_owner==me->p_no) {
    /* With full weapon resolution, self torps sent via
       updateTorps */
    if (F_full_weapon_resolution)
        continue;

So the two emitters are designed as a matched pair, each covering what the other skips. But the
per-update dispatch in updateClient() (ntserv/socket.c:611-642) only ever pairs them inside
the short-packet branch:

if(send_short) {
    updatePlasmas();
    updateSelf(FALSE);
    updatePhasers();
    updateShips();
    if (F_full_weapon_resolution) {
        SupdateTorps();
        updateTorps();
    }
    else
        SupdateTorps();
    ...
} else {
    updateTorps();
    updatePlasmas();
    ...
}

The else branch — the one taken when send_short == 0 — calls updateTorps() and never
SupdateTorps(). SupdateTorps() has no other call site. With F_full_weapon_resolution == 1
and send_short == 0, non-own torps are skipped by updateTorps() at genspkt.c:1426 and
there is no second emitter to pick them up. They are never written to either the TCP or the UDP
buffer, in any update, for the duration of the connection or until the client turns the feature
off or short packets on.

4. How a client reaches send_short == 0

send_short is only assigned in handleShortReq() (ntserv/socket.c:3114), on CP_S_REQ:
SPK_VOFF sets it to 0 (:3120), SPK_VON sets it to 2 or 1 depending on the client's
declared short-protocol version (:3136-3139), and a version mismatch on SPK_VON is rejected
back to SPK_VOFF (:3126-3131). A client that never sends CP_S_REQ at all simply keeps the
data.c:213 default of 0. PROTOCOL.md:437 states this expectation directly: "The server
defaults to old fixed-size packets (send_short starts 0 in ntserv/socket.c); short/variable
packets are strictly opt-in via CP_S_REQ/CP_FEATURE, so a client that never negotiates them
… is never sent one."

Scope of affected data

Read against the emitters, the withholding is confined to torpedoes:

  • Torpedoes, other players' — withheld. As above. Both position/direction updates
    (SP_TORP) and status/war info (SP_TORP_INFO) are produced by the same sndTorp() calls
    that are skipped, so no torp state of any kind is emitted for other players.
  • Torpedoes, own — sent normally, via the myTorp(t) branch at genspkt.c:1414.
  • Plasma torpedoes — not affected. updatePlasmas() (genspkt.c:1445) references
    F_full_weapon_resolution only in the observer special case at :1454-1456; it has no
    equivalent skip for non-own plasmas, and there is no SupdatePlasmas() anywhere in the tree.
    updatePlasmas() is called in both dispatch branches (socket.c:612 and :633), so plasmas
    are emitted normally in all combinations.
  • Phasers — not affected. updatePhasers() (genspkt.c:1481) uses the flag only to widen
    the view test for observers (:1501).
  • Ships, planets, self, status, messages, player stats — not affected; none of these
    emitters is gated on F_full_weapon_resolution in a way that removes data, and all are called
    in both branches.

So the affected set is exactly "torpedoes owned by players other than the receiving client, for
a non-observer client."

Silence and existing guards

  • The CP_FEATURE reply is a normal accept. No new_warning() or other diagnostic is emitted
    at negotiation time or at any point afterward.
  • There is no re-check when the state later becomes inconsistent. handleShortReq()'s
    SPK_VOFF path (socket.c:3119-3123) sets send_short = 0 without consulting
    F_full_weapon_resolution, so a client that had both enabled can turn short packets off and
    enter the same state mid-game, again silently.
  • The server does emit a diagnostic for an analogous dependency elsewhere in the same function:
    SPK_SALL and SPK_ALL respond "Activate SHORT Packets first!" when send_short is 0
    (socket.c:3177-3195). No comparable check exists for the feature.
  • I could not find the dependency documented anywhere outside the code. PROTOCOL.md does not
    mention FULL_WEAPON_RESOLUTION at all. docs/README.SHORT describes SupdateTorps() and
    the short-packet update ordering but predates the feature and does not mention it.
    docs/sample_features:79 lists the feature with no annotation. NEWS:561-562 and NEWS:656
    record its introduction ("Full weapon resolution, reduce bandwidth use", "Add full weapon
    resolution exception to UDP packet size check", "FULL_WEAPON_RESOLUTION [balcerski]") without
    stating a prerequisite. The only statement of the dependency in the tree is the code comment
    at genspkt.c:1424-1425.
  • The dependency is therefore neither documented in the protocol material nor enforced at
    negotiation time; it exists only as an assumption inside the emitter pair.

Nothing here is timing- or load-dependent: the branch is evaluated from two boolean globals on
every update, so the outcome is fully determined by the negotiated state.

Server revision examined

  • Tree: netrek-server (vanilla), local read-only checkout.
  • Git HEAD: 9d31c99dc220331b941b887b9bebd7dc18497bba — "Add client-to-server protocol test
    harness (join, outfit, orbit)", 2026-07-12.
  • Most recent release marker in ChangeLog: netrek-server-vanilla-2.19.0 released
    (2016-12-06, James Cameron).
  • Files cited: ntserv/feature.c, ntserv/data.c, ntserv/genspkt.c, ntserv/socket.c,
    include/data.h, docs/sample_features, docs/README.SHORT, PROTOCOL.md, NEWS.
    Line numbers are from this revision.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions