FORESTRY SQUAD - UPDATE NOTES
Version 1.1.0.0
Singleplayer only

===============================================================================
SUMMARY
===============================================================================

This update adds the realistic workday simulation, employee skills, equipment
recognition and live harvest tracking. Nothing from version 1.0 was removed or
redesigned. The vehicle stasis system, the pending-delivery system, the team
limits and the save/load layer all work exactly as before; the workday state
machine has been layered on top of them.

All 48 automated tests pass, including every existing v1.0 test.


===============================================================================
1. MENU LOGO
===============================================================================

The supplied artwork is now used in three places:

    gui/logo.dds                Page header logo, 256x256
    gui/forestrySquadTab.dds    Menu tab icon, 64x64
    icon.dds                    Mod list icon, 256x256

The source PNG was converted rather than simply resized. The chalk drawing was
cropped to its content so it fills the frame instead of floating in empty
space, and the dark navy field was converted to transparency by mapping
luminance to alpha. This matters for two reasons:

  - The logo sits on the menu as white artwork rather than as a dark rectangle
    pasted onto the FS25 background.
  - The tab icon can be tinted by the game the same way every other tab icon
    is tinted, so it highlights correctly on selection.

REPLACING THE LOGO LATER

Overwrite gui/logo.dds. Nothing else needs editing. The path is configurable at
FSConfig.GUI.LOGO_FILENAME, and the size is set by the fsq_logo profile in
gui/ForestrySquadFrame.xml (currently 132x132).

If the file is missing, the logo element hides itself and the rest of the page
is unaffected. The logo occupies its own column to the right of the detail
text, so it can never overlap the list, the tabs or the button bar.

The regeneration script is kept at tools/make_logo.py if the artwork is
updated later.


===============================================================================
2. THE WORKING DAY
===============================================================================

A team now moves through thirteen timed stages instead of simply harvesting
from 07:00 to 15:30.

    PREPARING              30 min    vehicles stay parked
    TRAVELLING_TO_SITE     45 min    vehicles deploy here
    ARRIVING_AT_SITE        5 min
    SETTING_UP             25 min
    HARVESTING_MORNING    105 min    producing
    BREAK_MORNING          15 min
    HARVESTING_MIDDAY      75 min    producing
    LUNCH                  30 min
    HARVESTING_AFTERNOON  105 min    producing
    BREAK_AFTERNOON        15 min
    FINAL_HARVESTING       30 min    producing
    PACKING_UP             15 min
    RETURNING_TO_BASE      15 min    vehicles return at the end of this

Total 510 minutes, which is exactly 07:00 to 15:30. Actual harvesting time is
315 minutes, so a team spends about 62% of its day cutting and the rest
travelling, setting up, on breaks and packing away.

DAILY VARIATION

Each stage is jittered by up to 18% every day, then the whole schedule is
scaled back so it still totals a normal shift. The result is that no two days
run to the same clock while an ordinary day still ends at 15:30. A sample day
from the test run:

    07:00 - 07:30  PREPARING
    07:30 - 08:21  TRAVELLING_TO_SITE
    08:21 - 08:26  ARRIVING_AT_SITE
    08:26 - 08:52  SETTING_UP
    08:52 - 10:27  HARVESTING_MORNING
    10:27 - 10:42  BREAK_MORNING
    10:42 - 11:48  HARVESTING_MIDDAY
    11:48 - 12:22  LUNCH
    12:22 - 14:14  HARVESTING_AFTERNOON
    14:14 - 14:30  BREAK_AFTERNOON
    14:30 - 15:00  FINAL_HARVESTING
    15:00 - 15:16  PACKING_UP
    15:16 - 15:30  RETURNING_TO_BASE

All stage durations are in FSConfig.WORKDAY.STAGES.


===============================================================================
3. VEHICLE RULE (the critical one)
===============================================================================

Vehicle deployment is now driven entirely by the team's work state. The clock
never triggers it.

At 07:00 the shift begins and the crew starts PREPARING. The vehicles do not
move. They are still parked exactly where the player left them, still visible,
still in physics, still enterable.

Only when TRAVELLING_TO_SITE becomes active does the mod record position,
rotation and owning farm, mark the vehicles deployed, and take them out of the
world. They stay out through travel, setup, harvesting, both breaks, lunch,
pack-up and the return journey, and come back only once RETURNING_TO_BASE has
completed, however late that is.

There is exactly one function that changes vehicle deployment
(FSShiftManager.syncVehicles) and exactly one thing that calls it: a stage
change. It is not possible for a vehicle to go out or come back for any other
reason.

Verified by test. From the test run:

    vehicles stay parked during preparation                PASS
    vehicles leave exactly at TRAVELLING_TO_SITE           PASS
    vehicles stay away over lunch                          PASS
    vehicles stay away during the return journey           PASS
    vehicles are back in physics at the end                PASS
    departed 07:26, returned 15:30

The underlying stasis method is unchanged from v1.0: removeFromPhysics plus
setVisibility(false), never delete and re-create, so the vehicle never moves
and the base game still saves it at its real position.


===============================================================================
4. LIVE HARVEST PROGRESS
===============================================================================

Wood now accumulates through the day instead of appearing at the end. It only
accumulates during the four harvesting stages, so travel, setup, breaks and
lunch genuinely cost the team output.

A player opening the menu mid-morning and again mid-afternoon sees the total
climbing. Real readings from a 3/4-skilled team with a harvester:

    09:30    2,232 L   (13%)
    12:45    9,610 L   (57%)
    15:15   15,487 L   (92%)
    final   15,487 L of a 16,877 L target (92%)

IMPLEMENTATION NOTE

The total is computed as a pure function of the schedule and the current time,
not accumulated tick by tick. Harvested litres equals the harvest rate times
the harvesting minutes elapsed, both of which are fixed when the shift starts.

This is deliberate. It means the figure is identical however the clock arrived
at the current time, so a save and reload mid-shift, a large time-scale jump or
a paused game can never double count or lose wood. It also means the menu
cannot drift out of step with the simulation.

The page refreshes once a second while it is open, and only when a team is
actually working. The list is never rebuilt during that refresh, so the
player's selection and scroll position are left alone.


===============================================================================
5. PROGRESS BAR
===============================================================================

Selecting a team shows a horizontal progress bar under the detail panel, in the
same spirit as the food bars on the animals page.

    Team 1 - Today's harvest
    [############################--------]
    13,640 L of 20,000 L target  -  68%

Behaviour:

  - While a team is working, the bar tracks today's live harvest against
    today's target.
  - Once a team has finished, it keeps showing what that team actually managed,
    so the player can see the result without having watched the clock.
  - Before a team has started, it previews what this crew and kit should manage.
  - The fill turns a brighter green once the target is reached.
  - When the selected team has no meaningful target, the bar hides itself
    rather than showing an empty frame.

The bar is drawn from a plain white image (gui/bar.dds) that the mod tints and
resizes, rather than depending on a base game bar profile existing. That was a
deliberate robustness choice: base game GUI profile names can change between
patches, and a bar that owns its own asset cannot break that way. Colours are
in FSConfig.GUI.


===============================================================================
6. EMPLOYEE SKILLS
===============================================================================

Every worker now has four skills, each rated 1/4 to 4/4:

    Felling
    Machine Operation
    Efficiency
    Experience

Skills improve by completing workdays. Only employees who were on a team that
finished a shift gain anything.

Progression is deliberately slow: 12 workdays for level 1 to 2, 24 for 2 to 3,
and 40 for 3 to 4. That is 76 workdays to fully train a worker. Measured in the
test run: 75 days from 1/4 to 4/4, and a visible improvement after 10 days. A
crew develops over a season, never in a week.

New applicants arrive with their own skills, weighted heavily toward the
inexperienced: 55% chance of level 1, 30% level 2, 13% level 3, 2% level 4.

Wages now follow skill. A 1/4 worker costs 600 per day, a 4/4 worker 950. Since
a 4/4 crew produces roughly double the wood of a 1/4 crew, experience is worth
paying for, which gives the hire screen a real decision.

Skills are saved and loaded, including for applicants, so the offered crew does
not reshuffle when a save is reloaded.


===============================================================================
7. PRODUCTIVITY MODEL
===============================================================================

Output per harvesting hour is the product of four independent factors:

    base rate
      x  sum of each employee's individual skill factor
      x  crew size factor (diminishing returns)
      x  vehicle count multiplier
      x  equipment factor (scaled by crew skill)

Nothing dominates. A large untrained crew, a small expert crew and a well
equipped average crew all land in believable and different places.

MEASURED RESULTS, 4 workers with 2 vehicles including a harvester:

    4 workers at 1/4    10,356 L     (target band 10,000-13,000)
    4 workers at 2/4    13,441 L     (target band 13,000-16,000)
    4 workers at 3/4    16,654 L     (target band 16,000-18,000)
    4 workers at 4/4    19,995 L     (target band 18,000-20,000)

Every tier lands inside the specified band, with the fully skilled crew on the
20,000 L benchmark.

    4 workers at 4/4, basic kit (no harvester)    14,811 L

CREW SIZE VERSUS SKILL

Four inexperienced workers produce 8,203 L. Two highly experienced workers
produce 8,071 L. Headcount alone does not beat experience; the crew factor
(1.00 / 0.97 / 0.93 / 0.89) is what holds that line.

A hard ceiling of 24,000 L per team per day sits above everything as a backstop
against any configuration change turning the mod into a wood generator.


===============================================================================
8. EQUIPMENT
===============================================================================

Only the forestry harvester carries a bonus in this version, as specified.

Detection covers wood harvesters, light harvesters and feller bunchers, by
specialization (spec_woodHarvester, spec_woodHarvesterLight, spec_fellerBuncher)
with a type-name fallback for modded machines. Attached implements are checked
as well as the carrier, so a harvester head on a base carrier is recognised.

The bonus is up to +35%, but it is NOT an unconditional multiplier. An
untrained crew realises 75% of it; a fully trained crew realises all of it. The
same machine is therefore worth more in experienced hands, which is confirmed
by test.

FUTURE EQUIPMENT

FSEquipment.CLASSES is the extension point. Adding a forwarder, log trailer,
skidder, chipper or stump grinder later means appending one entry with an id, a
display name, a bonus, a skill floor and a detect function. No calculation code
changes. A commented example for a forwarder is in the file. None of these are
implemented now, as specified.


===============================================================================
9. DAILY TARGET AND LATE FINISHES
===============================================================================

Each team gets a target when its shift starts, calculated from crew size, crew
skill, vehicles, equipment and the harvesting time the rolled schedule actually
leaves. Actual output is the target times a daily factor of 0.90 to 1.04, so a
team usually finishes slightly under target and occasionally on it. The team
does not have to hit the target.

Late finishes work as before at roughly an 18% chance per team per day, capped
absolutely at 20:30, but they now have a stated cause drawn from the workday:

    Delayed departure                 (adds to PREPARING)
    Longer travel time                (adds to TRAVELLING_TO_SITE)
    Difficult setup                   (adds to SETTING_UP)
    Equipment-related delay           (adds to SETTING_UP)
    Lower productivity than expected  (adds to HARVESTING_AFTERNOON)
    Extended harvesting operation     (adds to FINAL_HARVESTING)
    Longer pack-up than planned       (adds to PACKING_UP)
    Operational delay                 (adds to RETURNING_TO_BASE)

The delay is added to that specific stage, so it shows up in the team's
activity as well as its return time. If the cause is extended harvesting, the
team genuinely harvests longer and brings back more wood.

The reason is shown in the team detail panel and in a notification when the
team passes 15:30 still working.

Verified over 20,000 simulated days: latest return 20:30, never later, late on
17.7% of days.


===============================================================================
10. TEAM SCREEN
===============================================================================

Selecting a team now shows:

    Status              WORKING
    Destination         Blacks Sawmill
    Current activity    Harvesting timber

    Employees           4 / 4
       Aaron Fell         (3.2/4)
       Britta Holt        (3.7/4)
       Karl Moser         (3.5/4)
       Nora Sawyer        (4.0/4)
    Team skill          3.6/4

    Vehicles            2 / 2
       John Deere 1270G
       Ponsse Buffalo
    Equipment           Forestry Harvester

    Daily target        20,000 L
    Harvested           13,640 L
    Progress            68%
    Expected return     15:30

    [progress bar]

The team list itself shows live progress and current activity for working
teams, so the player can see the state of the whole operation at a glance
without selecting each team.

Destination assignment per team is unchanged from v1.0 and still works: it is
selected with the Change destination button on the Teams tab, can only be
changed while the team is not working, and is shown in both the list and the
detail panel.


===============================================================================
11. WHAT DID NOT CHANGE
===============================================================================

Deliberately untouched, because they were working:

  - Vehicle stasis method (removeFromPhysics + setVisibility)
  - Pending delivery system and free-capacity check before transfer
  - Production detection by wood input fill type
  - Team, employee and vehicle limits and their enforcement
  - Save/load architecture and the savegame file location
  - Menu page injection
  - Wage payment timing and the money type used

Delivery safety was re-tested after the changes: filling a deliberately
undersized 4,000 L sawmill from an 11,000 L pending load moved exactly what
fitted, held the remainder, and across staged deliveries the total moved
matched the total held to within a litre. Wood is still held rather than lost
when the destination is sold.


===============================================================================
12. FILES
===============================================================================

New:

    scripts/FSSkills.lua        skill definitions and progression
    scripts/FSWorkday.lua       stage schedule, late causes, activity
    scripts/FSEquipment.lua     equipment registry and detection
    gui/logo.dds                page logo
    gui/bar.dds                 progress bar image

Changed:

    scripts/FSConfig.lua        workday stages, skills, equipment, productivity
    scripts/FSEmployee.lua      skills, workday counter, skill-based wage
    scripts/FSTeam.lua          schedule, target, progress, skill-aware output
    scripts/FSShiftManager.lua  stage-driven, live accrual, skill gain
    scripts/gui/ForestrySquadFrame.lua   logo, bar, skills, live refresh
    gui/ForestrySquadFrame.xml  logo and bar elements
    l10n/l10n_en.xml            all new text
    modDesc.xml                 new source files, version 1.1.0.0
    icon.dds, gui/forestrySquadTab.dds   regenerated from the supplied artwork


===============================================================================
13. STILL TO TEST IN GAME
===============================================================================

The automated tests run against stubbed engine objects and cover the logic. The
following need checking in a real save:

  [ ] The logo displays correctly and does not overlap anything
  [ ] The tab icon renders and tints on selection
  [ ] The progress bar draws and fills correctly
  [ ] Vehicles are still parked and enterable at 07:15
  [ ] Vehicles disappear around 07:30 when travel starts
  [ ] Harvest total climbs when reopening the menu through the day
  [ ] Activity text changes through the day
  [ ] A late finish shows its reason
  [ ] Vehicles return to their exact positions after a late return
  [ ] Save mid-shift during PREPARING, reload, confirm vehicles still parked
  [ ] Save mid-shift during HARVESTING, reload, confirm vehicles still away
        and the harvest total continues rather than restarting
  [ ] Skills increase after a completed day
  [ ] A team with a harvester out-produces the same team without one

The save/load path for the new workday schedule is the part with the least
automated coverage, since it needs the real XML layer. The two save-mid-shift
checks above are the highest value things to try first.


===============================================================================
14. VERSION 1.1.1.0 - WOOD DELIVERY FIX AND TRANSFER BUTTON
===============================================================================

THE BUG

Wood was not reaching the sawmill at the end of a shift. Three faults in the
delivery path, any one of which caused it:

1. DESTINATIONS WITH NO UNIQUE ID (the main cause)

   Destinations were identified by Placeable:getUniqueId(). That returns nil
   for placeables the game never assigned an id to, which includes sawmills
   pre-placed on the map. Selecting one stored nil, so the destination could
   never be resolved again and the wood was held indefinitely with nothing to
   deliver it to.

   Fixed: when a placeable has no unique id, a stable synthetic id is derived
   from its world position ("fsq:512.0:0.0:733.0"). A building does not move,
   so that id resolves correctly in this session and every session after.

2. UNOWNED PRODUCTIONS WERE EXCLUDED

   Only productions registered to the player's farm were searched. A sawmill
   on the map that the player has not bought was therefore unreachable, even
   though hauling logs to it is perfectly normal.

   Fixed: unowned production points are now included as valid destinations.

3. FAILURES WERE SILENT

   Storage:setFillLevel ignores any fill type its storage has not registered,
   and does so without complaint. The old code assumed the call worked.

   Fixed: the result is verified against the storage afterwards, and every
   failure path now returns a specific reason that is shown to the player and
   written to the log.

Additionally, on load, any team whose saved destination no longer resolves now
logs a warning naming the team and the id, instead of quietly holding wood.

THE TRANSFER BUTTON

A "Transfer to sawmill" button has been added to the Teams tab. Select a team
and press it to send that team's wood immediately.

  - Works mid-shift as well as after one. Anything cut so far is banked and
    sent, so you do not have to wait for 15:30.
  - Always reports what happened. On success it names the amount and the
    destination. On failure it says exactly why: no destination selected,
    destination not found, destination full, destination does not accept wood,
    or nothing waiting to send.
  - On a partial delivery it reports how much fitted and how much is still
    being held.
  - Never loses wood. A failed transfer leaves the pending amount untouched.

MID-SHIFT TRANSFERS CANNOT DOUBLE COUNT

Because the harvest total is recomputed from the schedule rather than
accumulated, banking wood mid-shift would have been recounted at the end of the
day. A separate banked total is now tracked and subtracted from the recomputed
figure.

Verified by test: transferring at 12:30 and then running the day to completion
leaves the sawmill holding exactly the day's harvest, not more, and the progress
percentage does not jump backwards at the moment of transfer.

TESTS

72 automated tests now pass, up from 48. The new ones reproduce each fault
above and confirm the fix, including a sawmill with no unique id receiving a
full day's harvest end to end.

IF WOOD STILL DOES NOT ARRIVE

Select the team and press Transfer to sawmill. The message names the cause. If
it reports that the destination could not be found, reselect it with Change
destination, which will now store a resolvable id. Setting FSConfig.DEBUG to
true adds per-delivery logging.


===============================================================================
15. VERSION 1.1.2.0 - SAVEGAME FIX (ROOT CAUSE OF BOTH REPORTED FAULTS)
===============================================================================

THE BUG

Forestry Squad data was never written to disk. Not corrupted, not partially
saved: the save function was never called at all.

The mod hooked the wrong function:

    FSBaseMission.saveSavegame          <- does not exist in FS25

Assigning to a nil field does not fail. Utils.prependedFunction returned the
new function, Lua happily created FSBaseMission.saveSavegame, and nothing in
the game ever called it. There was no error in the log because nothing went
wrong from Lua's point of view.

The correct hook, confirmed against a working FS25 mod (Courseplay), is:

    FSCareerMissionInfo.saveToXMLFile   <- what FS25 actually calls

THIS EXPLAINS BOTH REPORTED PROBLEMS

They were the same bug:

  1. Hired workers disappeared. Nothing was saved, so on load the mod found no
     file and started with an empty workforce and no teams.

  2. No wood reached the sawmill. A team whose crew had vanished had zero
     employees, so getCanStartShift() refused to run the shift, so no wood was
     ever produced. The transfer button then correctly reported that there was
     no wood waiting, which was true but useless, because it described the
     symptom rather than the cause.

The delivery hardening shipped in 1.1.1.0 was still worth having, but it was
not the fault behind either report. This was.

WHAT IS NOW TESTED

The test suite gained a mock XML layer so the real save and load code runs in
memory. 109 tests pass, up from 72. The new ones prove:

  - The mod registers against FSCareerMissionInfo.saveToXMLFile
  - A save triggered through the game's own call reaches the mod and writes
    a file
  - No value is silently dropped for want of a schema entry
  - Business name, all four workers, the team, its name, its roster, both
    assigned vehicles and the destination all survive a reload
  - Earned skill and workday counts survive
  - Applicants survive, so the hire list does not reshuffle
  - The reloaded team can start a shift and has a non-zero target
  - A second save and load cycle duplicates nothing

That last group is the important one: previously a reloaded team could not
start a shift because it had no crew.

ALSO IN THIS VERSION

Saving is wrapped so a fault in this mod can never interfere with the base
game writing the player's savegame; any error is logged instead.

Saving and loading now log at info level rather than debug, so the game log
always states how many workers and teams were written, and where. If the log
does not show that line after saving, the mod is not saving.

The transfer button no longer just says there is no wood. It now explains why:
the team is still out working and what it has cut so far, or it already
finished and delivered today, or it has not started yet and shifts begin at
07:00, or it cannot start and the specific reason, plus a summary line showing
employee count, vehicle count and destination.

IMPORTANT FOR EXISTING SAVES

Workers hired before this version were never written to disk and cannot be
recovered. Hire your crew again, rebuild the team, assign vehicles and pick a
destination, then save. After that, check the log for a line reading:

    [ForestrySquad] Saved N employees, M teams and K applicants to ...

Once that line appears, the workforce is persisting.


===============================================================================
16. VERSION 1.1.3.0 - ACTIVE TEAM SHOWING NO WOOD
===============================================================================

Reported: a team that was already active in the save would not transfer wood,
and the button said there was no wood waiting.

FINDING 1: THERE IS A REAL WINDOW WITH NO WOOD, AND IT WAS NEVER EXPLAINED

The workday added in 1.1.0 means a team does not start cutting the moment the
shift begins. It prepares, drives to the site and sets up first. On a typical
day the first harvesting stage begins around 08:50, nearly two hours after the
07:00 start.

So between 07:00 and roughly 08:50 a team is genuinely working and genuinely
has zero wood. The transfer button reported that truthfully but uselessly, and
the team panel showed 0 L with no explanation, which reads exactly like a
broken mod.

This is the most likely thing that was hit.

Fixed, three ways:

  - The transfer button now says the crew has not started cutting yet and
    gives the time harvesting begins, rather than just "no wood waiting".
  - The team panel shows a "Cutting begins" line with that time whenever the
    team is on shift but has not started cutting.
  - The shift start is now logged at info level including the cutting time:
        Team 'North Forestry' started. Target 16877 l, cutting from 08:52,
        finish 15:30

FINDING 2: A TEAM COULD GET STUCK OFF SHIFT

Only teams in the IDLE state were eligible to start. A team left in OFF_SHIFT
became eligible again only when the day rollover fired and reset it. If that
rollover was missed for any reason, the team would sit off shift indefinitely
and never work again, with no explanation anywhere.

Fixed: a team that is off shift but has not worked today is now also eligible
to start. Verified by test.

ALSO FIXED

  - A shift can no longer be closed out twice. Because the harvest total is
    recomputed from the schedule, a second close-out would have recounted and
    banked the whole day again, duplicating wood.

  - A team that fails to start is now logged once per day with the reason:
        Team 'North Forestry' did not start today: no destination has been
        selected.
    Previously this was debug-only, so a team that never went out was silent.

  - A team whose output works out at zero is logged as a warning at shift
    start with its employee and vehicle counts.

  - Shift completion is logged with the numbers:
        Team 'North Forestry' finished with 15487 l of 16877 l target (92%).
        Pending 0 l.

NEW: CONSOLE COMMAND

Open the console and run:

    fsSquadStatus

It prints the full internal state of every team: state, stage, whether it
worked today, whether it can start and why not, the destination id, whether
that destination resolves and how much space it has, the daily target, the
harvest rate in litres per minute, hourly output, when cutting starts, planned
finish, harvesting minutes elapsed, and the accumulated, banked and pending
wood totals. It also lists every compatible wood production found, with its id
and free capacity.

If a team is still not producing or delivering, that output identifies the
cause precisely. It is written to the log as well as the console.

TESTS

121 pass, up from 109. The new ones prove a team on shift at 07:45 has cut
nothing and that the schedule can state when cutting begins, that wood appears
shortly after it does, that an off-shift team which has not worked today still
starts, and that closing a shift twice delivers nothing extra.


===============================================================================
17. VERSION 1.1.4.0 - DELIVERY RECORD
===============================================================================

Reported: both teams were out, both finished for the day, and transferring
still said there was no wood waiting.

WHAT THAT TELLS US

If both teams finished and neither has pending wood, then either the automatic
delivery at the end of the shift succeeded, or the teams finished having
harvested nothing. There was no way to tell which, because a finished team kept
no record of what happened to its wood. That is the real gap, and it is now
closed.

A full-day simulation of two freshly hired crews on the current build delivers
18,938 L to the sawmill, so the path itself works here. Something in the actual
save differs from that, and the changes below are designed to say what.

EVERY DELIVERY IS NOW RECORDED

Each team stores its most recent delivery attempt: how much moved, to where, at
what time, and if nothing moved, why. It is shown in the team panel:

    Last delivery: 9,007 L delivered to Blacks Sawmill at 15:30

or

    Last delivery: Delivery at 15:30 moved nothing: The selected destination
    could not be found. Reselect it with Change destination.

The record survives a save and reload, so it is still there next session.

This answers the question directly. If it says wood was delivered, the wood is
in the sawmill and the problem is elsewhere. If it says the delivery failed, it
names the cause.

A ZERO-HARVEST DAY IS NOW AN ERROR, NOT A SILENCE

A team that completes a full shift and produces nothing was previously
indistinguishable from one that worked normally. It now writes an error to the
log with everything needed to diagnose it:

    Team 'North Forestry' completed a shift but harvested nothing.
    employees=4 vehicles=2 target=0 rate=0.000 harvestMinutes=315

and notifies in game. If the teams really are producing nothing, this line will
say why within one in-game day.

EVERY DELIVERY ATTEMPT IS LOGGED

    Team 'North Forestry' delivered 9007 l to Blacks Sawmill (0 l still pending).

or

    Team 'North Forestry' could not deliver 9007 l to Blacks Sawmill:
    fsq_transfer_full

TRANSFER BUTTON

For a team that has already finished, it now states what became of the wood
instead of only that there is none waiting.

STATUS REPORT

fsSquadStatus now includes the last delivery for each team, or states that one
was never attempted.

TESTS

133 pass, up from 121. The new ones confirm a successful delivery is recorded
with the right amount and destination and matches what the sawmill actually
holds, that a failed delivery is recorded with its reason while the wood stays
held, and that the record survives a save and reload.

WHAT TO SEND IF IT STILL FAILS

Either the "Last delivery" line from the team panel, or the output of
fsSquadStatus from the console. Either one identifies the cause outright.


===============================================================================
18. VERSION 1.1.5.0 - OWNED PRODUCTIONS ONLY
===============================================================================

Reported: pressing transfer moves no wood to the player's own sawmills, and
deliveries should only ever go to the player's own processing, never to
anything unowned on the map.

THE FAULT WAS MINE, INTRODUCED IN 1.1.1

In 1.1.1 I widened the destination search to include unowned production points,
reasoning that hauling logs to a map sawmill you have not bought is normal.
That was wrong for this mod and it caused the problem.

Checking the engine source settles it. ProductionChainManager:addProductionPoint
only files a production under a farm when:

    local farmId = productionPoint:getOwnerFarmId()
    if farmId ~= AccessHandler.EVERYONE then

Anything owned by nobody is registered as EVERYONE and deliberately kept out of
every farm's list. By pulling those in through getUnownedProductionPoints I was
offering destinations the game itself excludes, so a team could be pointed at a
mill the player does not own and whose contents they cannot see.

FIXED

  - Destinations are now the player's own productions only. The unowned search
    is removed entirely.
  - Ownership is verified directly with productionPoint:getOwnerFarmId(), not
    just inferred from the farm grouping, because ownership changes when a
    placeable is bought or sold mid-game.
  - Anything owned by AccessHandler.EVERYONE is rejected outright.

TWO RELATED FAULTS FIXED AT THE SAME TIME

1. FARM ID RESOLUTION

   The player's farm id was read only through g_currentMission:getFarmId(),
   with a fallback that indexed FarmManager without checking it existed, which
   could throw. It now tries g_localPlayer.farmId first, which is the accessor
   the base game itself uses, then g_currentMission:getFarmId() inside a pcall,
   then FarmManager.SINGLEPLAYER_FARM_ID, then 1. It can no longer throw.

   If this was resolving to the wrong farm, every ownership lookup returned an
   empty list and no destination could ever resolve.

2. THE INPUT FILL TYPE CHECK WAS TOO STRICT

   Compatibility required inputFillTypeIds[wood] == true. The game only
   guarantees the key is present, not that the value is boolean true; its own
   code iterates the table with pairs() and never inspects the values. A
   different value would have excluded every sawmill.

   The storage test is now primary, since that is what a delivery actually
   writes into, and the input declaration is checked for presence only.

WHAT YOU NEED TO DO

If a team was pointed at an unowned production under 1.1.1 to 1.1.4, that
destination will no longer resolve. The team panel shows

    Destination: Unavailable - reselect it

and the log names the team. Select the team, press Change destination and pick
your own sawmill. It will now only ever cycle through productions you own.

Destination selection is also logged now:

    Team 'North Forestry' destination set to 'Blacks Sawmill'
    (id=..., owned by farm 1).

TESTS

146 pass, up from 133. The new ones confirm only owned productions are offered,
that an unowned one is rejected even if it somehow appears in the farm list,
that another farm's production is rejected, that a non-boolean input marker
still counts as compatible, and a full day end to end where the owned sawmill
receives the wood and an unowned one alongside it receives nothing.

One test from 1.1.1 asserted the opposite behaviour and has been inverted.


===============================================================================
19. VERSION 1.1.6.0 - DELIVERY VIA THE GAME'S OWN UNLOADING ROUTE
===============================================================================

THE LAST UNVERIFIED ASSUMPTION

Every version so far delivered by writing straight into the production's
storage with Storage:setFillLevel(). That was an assumption I never checked
against how the game itself puts wood into a sawmill.

It does not do that. A trailer tipping logs calls:

    unloadingStation:addFillLevelFromTool(farmId, delta, fillType,
                                          fillPositionData, toolType)

UnloadTrigger asserts every unload target provides that method, and
PlaceableHusbandry and PlaceableTrainSystem both use it. It returns the amount
actually applied and carries the game's own bookkeeping with it.

Writing directly to storage bypassed all of that. It may well have set the
number without the production noticing, which would explain wood that never
appears where the player is looking.

FIXED: TWO ROUTES, TRIED IN ORDER

  1. unloadingStation:addFillLevelFromTool() - the exact call a tipping trailer
     makes, so the wood arrives the same way a player-hauled load would.
  2. storage:setFillLevel() - the previous direct write, used only if the
     station route is missing or moves nothing.

The storage level is measured before and after either way, so the amount
reported is always what the production actually took, never what was asked for.
The route used is recorded and logged:

    Team 'North Forestry' delivered 9007 l to Blacks Sawmill via
    unloadingStation (0 l still pending).

Tested against four awkward cases: a normal station, a production with no
station at all, a station that silently accepts nothing, and a station that
throws an error. All four deliver the wood.

NEW: PROVE IT IN FIVE SECONDS

Open the console and run:

    fsSquadTestDelivery

It pushes 100 l of wood into each team's destination and prints the sawmill's
level before and after:

    === Forestry Squad test delivery (100 l) ===
    Farm id: 1, wood fill type: 42
    Owned wood productions: 1
    North Forestry -> Blacks Sawmill: 3200 -> 3300  delivered=100
        route=unloadingStation
    If a level rose above, open that production and the wood will be there.

You do not have to wait for a shift. If the level rises, open that sawmill and
the wood is there. If it does not, the line says exactly why: the destination
does not resolve, the production is full, or no owned wood production exists.

Pass an amount if you want a larger test: fsSquadTestDelivery 2000

Note that this does add wood you did not earn, which is why it defaults to a
token 100 l. It is a diagnostic, not a feature.

TESTS

159 pass, up from 146.


===============================================================================
20. VERSION 1.2.0.0 - TONNES, AND A 50 TONNE BENCHMARK
===============================================================================

WOOD IS NOW SHOWN IN TONNES

Every wood figure the player sees is in tonnes: the daily target, the running
harvest, the progress bar label, pending deliveries, the transfer messages, the
delivery record and the destination's free space.

Internally the mod still works in litres, because that is what productions
store and what a delivery writes. The conversion uses the game's own density
for the wood fill type, read at runtime:

    g_fillTypeManager:getFillTypeByIndex(FillType.WOOD).massPerLiter

That matters. Hard-coding a density would have made the mod's figures disagree
with the rest of the game the moment a map or mod changed it. A configurable
fallback of 0.0007 t/l is used only if the density is asked for before fill
types are loaded, and it is never cached, so the real value is picked up as
soon as it exists.

THE BENCHMARK IS NOW 50 TONNES PER DAY

A full crew of four fully skilled workers with two vehicles including a
forestry harvester now targets 50 tonnes for a normal day.

Measured, with the density above:

    4 workers at 1/4 + harvester    25.9 t
    4 workers at 2/4 + harvester    33.6 t
    4 workers at 3/4 + harvester    41.6 t
    4 workers at 4/4 + harvester    50.0 t   <- benchmark
    4 workers at 4/4, basic kit     37.0 t

Crew size still does not beat experience: four green workers manage 20.5 t
against two experts on 20.2 t.

THE RATE IS NOW DERIVED, NOT HAND-TUNED

Previously the per-employee rate was a tuned constant and the benchmark was
whatever fell out of it. That is backwards, and it meant any change to the
skill range, crew factors, harvester bonus or the length of the working day
quietly moved the benchmark.

The rate is now solved from the benchmark instead:

    FSConfig.PRODUCTIVITY.BENCHMARK_TONNES_PER_DAY = 50

Change that one number and the whole balance follows. Change the working day,
the skill range or the harvester bonus and the reference team stays on the
benchmark automatically. The old BASE_LITRES_PER_EMPLOYEE_HOUR is gone.

The safety ceiling is now expressed in tonnes too:

    FSConfig.PRODUCTIVITY.HARD_DAILY_CAP_TONNES = 60

WORTH KNOWING BEFORE YOU PLAY

50 tonnes is roughly 71,400 litres per team per day at the standard wood
density, which is around three and a half times the previous output. Sawmill
input storage is usually a good deal smaller than that.

This will not lose you any wood: anything that does not fit is held as a
pending delivery and goes in as space frees up. But expect a full team to
outpace a single sawmill, and expect the pending figure to sit above zero
during a busy week. If you would rather it matched your mill, lower
BENCHMARK_TONNES_PER_DAY.

DIAGNOSTICS

fsSquadStatus now reports the wood density in use, the benchmark and cap, and
shows each team's figures in tonnes with the raw litres alongside for checking
against the production's own storage.

TESTS

160 pass. The tier tests now assert tonnes, confirm the fully skilled crew
lands on 50.0 t, that tonnes and litres round-trip consistently, and that wood
is displayed with a tonne unit.


===============================================================================
21. VERSION 1.2.1.0 - 50 t IS A CEILING, NOT A DAILY FIGURE
===============================================================================

Corrected: 1.2.0.0 put the best team on 50 t as its normal result. 50 t is the
absolute maximum, and ordinary deliveries should vary between 20 t and 45 t
depending on work rate and equipment.

MEASURED OVER 60 SIMULATED DAYS PER COMPOSITION

    4 x 1/4, basic kit        20.0 - 24.2 t     avg 22.6 t
    4 x 1/4, + harvester      26.2 - 30.3 t     avg 27.9 t
    4 x 2/4, + harvester      28.8 - 35.3 t     avg 32.6 t
    4 x 3/4, + harvester      33.3 - 39.6 t     avg 36.8 t
    4 x 4/4, basic kit        28.4 - 33.6 t     avg 31.3 t
    4 x 4/4, + harvester      38.0 - 44.6 t     avg 41.5 t

Ordinary days span 20.0 t to 44.6 t across every composition. The highest
figure seen anywhere, including late days, was 48.0 t. Nothing reached 50 t.

HOW 50 t STAYS MEANINGFUL

50 t is now an absolute ceiling that normal work does not approach. The only
route near it is a top team that also rolls a late finish on extended
harvesting: it genuinely works longer, so it genuinely brings back more. That
makes a near-50 t day a rare event with a cause behind it, rather than a number
teams post every evening.

WHAT CHANGED

  - BEST_TEAM_MAX_TONNES = 43.9 replaces the old benchmark. It is what a
    perfect team reaches on a good ordinary day, not what it does daily.
  - HARD_DAILY_CAP_TONNES = 50, the absolute ceiling.
  - Daily variation is now 0.88 to 1.00 of a team's own potential, so a team
    rarely reaches exactly what it is capable of and never repeats a figure.
  - The skill spread was compressed (SKILL_FACTOR_MIN 0.72 to 0.945) so the
    weakest properly crewed team lands on 20 t rather than well below it,
    while skill and equipment both still clearly matter.

Output still varies for three independent reasons, which is why the same crew
never posts the same number twice: the daily factor, the jittered workday
schedule changing how many minutes are actually spent cutting, and late
finishes adding real harvesting time.

TESTS

162 pass. The new band test runs 60 full days for each of six team
compositions, 360 simulated days in total, and asserts that no ordinary day
falls below 20 t or above 45 t, that nothing ever exceeds the 50 t ceiling,
that the same crew produces more than 20 distinct results, that the best team
does not average near its ceiling, and that skill and equipment each still
raise output.


===============================================================================
22. VERSION 1.2.2.0 - MENU TAB ICON WAS BLANK
===============================================================================

Reported: the Forestry Workers tab in the pause menu had no icon.

ROOT CAUSE

The tab icon was registered by passing a raw DDS filename and pixel UV
coordinates directly to TabbedMenu:addPageTab(). That call signature exists in
the engine and accepts those arguments without error, but it is not the code
path that reliably draws an icon in the tab bar.

Checked against Courseplay, a real, currently shipping FS25 mod with a working
custom menu tab: it defines that same raw filename+UV path but never actually
calls it. Every tab it registers, including its own custom one, goes through a
slice id resolved from a texture atlas it registers up front:

    g_overlayManager:addTextureConfigFile("img/ui_courseplay.xml", "cpUi")
    ...
    inGameMenu:addPageTab(page, nil, nil, "cpUi.navigation")

This is also how the base game's own default tabs work: their icons are slice
ids like "gui.icon_options_help2", not raw files. The raw filename+UV path was
the wrong mechanism from the start.

FIXED

  - New gui/forestrySquad_icons.xml registers the existing 64x64 tab artwork
    as a one-slice texture atlas, in the same schema Courseplay's own working
    atlas file uses.
  - The atlas is registered with g_overlayManager:addTextureConfigFile() as
    early as possible, the first time the menu page is set up, which is always
    before the tab itself is created.
  - The tab is now added with a slice id ("forestrySquad.tab") instead of a
    filename and UVs.
  - If the atlas fails to register for any reason, the tab still registers
    successfully with no icon rather than failing outright, and the log says
    why.

A BUG FOUND WHILE FIXING THIS

Partway through this fix, a mismatch between an updated function signature and
its call sites left the new slice id parameter referring to nothing, which
would have silently kept the icon blank even after the fix above. It was
caught by inspection before packaging, not by chance: every changed file was
re-read in full and checked against its actual current content rather than
assumed, specifically because a previous silent no-op elsewhere in this
project had already shown that an edit which does not find its target fails
quietly rather than with an error.

A regression test now exercises the real addInGameMenuPage function against a
mock in-game menu and asserts that a slice id, and only a slice id, reaches
addPageTab. That test would have caught this exact fault and will catch it
again if it recurs.

TESTS

172 pass, up from 162. The new ones cover atlas registration succeeding, a
missing atlas file failing safely rather than throwing, a full page
registration confirming the slice id reaches addPageTab and that no raw
filename does, the tab title still resolving correctly, and a page with no
icon still registering cleanly.

IF THE ICON IS STILL MISSING

Check the log for:

    [ForestrySquad] Icon atlas not found at .../gui/forestrySquad_icons.xml

which would mean the file did not make it into the installed mod, or:

    [ForestrySquad] Could not register icon atlas 'forestrySquad': ...

which names the underlying engine error. Either line pinpoints it precisely.
