FarLands API for other mods
===========================

FarLands draws the land past the loaded zones: its own far land, far forest, rocks and buildings.
It builds that land from the world generator and the players' saved terrain edits. A mod that
changes the land some other way (rivers dug from a plan, roads laid through zones no player has
visited) can use this API to show its change in the far land too.

Two public static classes in FarLands.dll, namespace FarLands:

  FarLandsReach   where FarLands' far land is now
  FarLandsLand    your mod's changes of the far land: heights, paint, cleared forest

Reach both by reflection, by name. Your mod then needs no reference to FarLands.dll and keeps
working when FarLands is not installed. The names below are published: they stay. If what one of
them means changes, FarLandsLand.ApiVersion goes up; check it before you use the API.

The doc comments in src/Core/FarLandsLand.cs and src/FarLandsReach.cs are the exact contract. This
file explains it and shows an example.


1. Reaching FarLands
-------------------

FarLands' plugin GUID is "ru.ivest.farlands". Declare a soft dependency so FarLands loads before
your plugin:

  [BepInDependency("ru.ivest.farlands", BepInDependency.DependencyFlags.SoftDependency)]

Then, in your plugin's Awake:

  if (!Chainloader.PluginInfos.TryGetValue("ru.ivest.farlands", out PluginInfo farLands))
      -> FarLands is not installed.
  Type land = farLands.Instance.GetType().Assembly.GetType("FarLands.FarLandsLand");
  (int)land.GetField("ApiVersion").GetValue(null) must be 1. Otherwise leave FarLands alone and
  log a warning. (FarLandsReach the same way: "FarLands.FarLandsReach".)


2. FarLandsReach
----------------

The square FarLands' far land covers right now:

  bool  Active       false while FarLands draws nothing. The game's own distant terrain is in
                     place then.
  float CenterX      centre of the square, world metres
  float CenterZ
  float Size         side of the square, metres. The far land reaches Size / 2 from the centre.
  float UpdateStep   the square moves when the player strays this far from its centre

Read these each frame you need them; they change as the player moves. Use them, for example, to
draw your own far water over FarLands' land, or to fade something at the land's edge.


3. FarLandsLand
---------------

ApiVersion = 1

object Add(string owner,
           Func<float, float, float, float> delta,
           Func<float, float, float, uint, uint> paint,
           Func<float, float, float, float, float, uint> print,
           Func<float, float, bool> cleared)

  Adds your change to the far land and returns a handle for Remove. All parts of the far land
  already shown are looked at again. owner is your mod's name, used in FarLands' log. print is
  required; delta, paint and cleared may be null.

  delta(x, z, spacing) -> metres
    Metres your change adds to the land at world point (x, z): negative digs, positive raises.
    Added after the players' saved edits.

  paint(x, z, spacing, mask) -> mask
    The paint mask at (x, z), given the mask FarLands has there: the land's own, or the
    players' paint. Return mask itself for no change. Packed as the game's Heightmap paint
    mask: r | g << 8 | b << 16 | a << 24 (r dirt, g cultivated, b paved, a grass cleared),
    each 0..255.

  spacing
    How far apart the far land's samples lie where it asks: from 2 m next to the loaded zones
    to 512 m far off. A feature narrower than the spacing can fall between two samples. To show
    it anyway, take it from within spacing / 2 of the point (the deepest bed there, the
    strongest paint). 0 means one exact point (a tree being set on the far land).

  print(minX, minZ, maxX, maxZ, spacing) -> uint
    One number for everything your change does inside the rectangle: delta, paint and cleared.
    Return 0 where it does nothing. FarLands keeps its far land and forest on disk across
    sessions and builds a piece again only when its print changes. So:
    - the same change must always give the same print;
    - a new change must give a new print (put a version of your data into it);
    - without a print, land cached before your change stays as it was.
    The forest asks print over its 512 m cells with spacing 0.

  cleared(x, z) -> bool
    True where the world generator's trees, rocks and plants must not stand: a river's bed, a
    road. Applies only in zones no player has visited (and, on a world this game holds, that
    the game has not made yet). Zones the game has made show the trees really there, which your
    mod has already dealt with in the game. The forest is kept on disk without your clearing,
    so the trees come back when your mod is removed.

void Remove(object handle)
  Takes your change out. The far land and forest it touched are built again. Removing twice
  does nothing.

void ZoneChanged(int zoneX, int zoneZ)
  Call it when your change inside a zone changes (a new river, a road moved). FarLands then
  compares the prints of the far tiles and the forest cell over that zone and builds again
  those that differ. A zone is 64 m; zone (x, z) is centred at (x * 64, z * 64), as
  ZoneSystem.GetZone. Any thread. FarLands takes up to 256 zones a frame, so report the
  nearest first (see FarLandsReach).

bool PlayerEdited(int zoneX, int zoneZ)
  Whether FarLands holds the players' saved terrain edits of the zone: the land as the game
  last saved it. If your mod writes its change into the zone's edits when the zone loads, those
  edits already show it; return 0 from delta there, or it counts twice. This turns true once
  FarLands has read the zone (then it builds that land again, and your delta is asked again),
  so ask on every call and do not cache it. Any thread.


4. Rules
--------

- Threads. delta, paint and cleared run on FarLands' builder threads; print runs on any thread,
  the main thread too. All four must be safe to call from any thread and must not touch Unity
  objects. Prepare your data on the main thread and read it from an immutable snapshot or a
  concurrent collection.
- Speed. One far tile asks delta and paint about 20 000 times; one forest cell asks cleared
  about as many times. Make them a lookup, not a search. Return early where your change does
  nothing, and return 0 / mask / false for spacings too coarse for your feature.
- Errors. A delegate that throws is taken out of FarLands for the session: logged once as an
  error with your owner name, and the land and forest are built again without your change.
- Lifetime. Add once, when your data for the world is ready, and Remove when your mod or the
  world goes away. A change stays added across worlds until removed; if your data is per world,
  return 0 / mask / false while it is not ready and report its zones (ZoneChanged) when it is.
- Water is yours to draw. FarLands draws no water for your rivers; use FarLandsReach to know
  where its land is.


5. Example
----------

A canal: a straight trench 8 m wide and 3 m deep at x = 1000..1008, z = -500..500, paved, with
no trees in it. Copy it, and replace the canal with your data.

using System;
using BepInEx;
using BepInEx.Bootstrap;

[BepInPlugin("example.canal", "Canal", "1.0.0")]
[BepInDependency("ru.ivest.farlands", BepInDependency.DependencyFlags.SoftDependency)]
public class CanalPlugin : BaseUnityPlugin
{
    private object _farLandsHandle;
    private Action<object> _farLandsRemove;

    // The canal. Bump the version whenever it changes, and report its zones.
    private static volatile int s_version = 1;
    private static bool In(float x, float z) => x >= 1000f && x <= 1008f && z >= -500f && z <= 500f;

    private void Awake()
    {
        if (!Chainloader.PluginInfos.TryGetValue("ru.ivest.farlands", out PluginInfo farLands)) return;
        try
        {
            Type land = farLands.Instance.GetType().Assembly.GetType("FarLands.FarLandsLand");
            if (land == null || (int)land.GetField("ApiVersion").GetValue(null) != 1)
            {
                Logger.LogWarning("This FarLands has another API: the canal is not shown in its far land.");
                return;
            }
            _farLandsHandle = land.GetMethod("Add").Invoke(null, new object[]
            {
                "Canal",
                (Func<float, float, float, float>)Delta,
                (Func<float, float, float, uint, uint>)Paint,
                (Func<float, float, float, float, float, uint>)Print,
                (Func<float, float, bool>)Cleared,
            });
            _farLandsRemove = (Action<object>)Delegate.CreateDelegate(typeof(Action<object>), land.GetMethod("Remove"));
        }
        catch (Exception e)
        {
            Logger.LogWarning($"FarLands could not be reached: {e.Message}");
        }
    }

    private void OnDestroy() => _farLandsRemove?.Invoke(_farLandsHandle);

    // Within half a sample spacing of the canal, so a 4 m or 8 m spacing does not miss it.
    private static bool Near(float x, float z, float spacing)
    {
        float w = spacing * 0.5f;
        return x + w >= 1000f && x - w <= 1008f && z + w >= -500f && z - w <= 500f;
    }

    private static float Delta(float x, float z, float spacing) =>
        spacing <= 32f && Near(x, z, spacing) ? -3f : 0f;

    private static uint Paint(float x, float z, float spacing, uint mask) =>
        spacing <= 32f && Near(x, z, spacing) ? (mask & 0xFF00FFFFu) | 0x00FF0000u : mask;   // b = paved

    private static bool Cleared(float x, float z) => In(x, z);

    private static uint Print(float minX, float minZ, float maxX, float maxZ, float spacing)
    {
        bool meets = maxX >= 1000f - 16f && minX <= 1008f + 16f && maxZ >= -500f - 16f && minZ <= 500f + 16f;
        if (!meets || spacing > 32f) return 0u;
        return (uint)s_version * 2654435761u | 1u;   // never 0 where the canal is
    }
}

When the canal changes: increment s_version, then call ZoneChanged for each zone it touches,
before and after the change (by reflection, as Add above: land.GetMethod("ZoneChanged")).
