Repository: mcbeet/beet (language: python)

Issue:
Support for datapack advancements
Datapack advancements are a convention from the [Minecraft Datapacks](https://discord.gg/SnJQcfq) Discord server ([their article](https://mc-datapacks.github.io/en/conventions/datapack_advancement.html) about the issue). The aim is to replace installation messages in an easily viewable and non-obstructive way by putting them on a single advancement page.

The convention is by no means standardized, but a couple of people are already using it (myself included) and I think adding built-in support would be a helpfull feature. Wether this feature can be implemented as is or should be altered is open to discussion.

## Configuration
An example configuration:
```yaml
name: Apple
description: An example datapack
author: Oran9eUtan
pipeline:
  - beet.contrib.datapack_advancement
meta:
  datapack_advancement:
    author_namespace: Oran9eUtan
    author_skull_owner: oran9eutan
    icon:
        item: "minecraft:apple"
        nbt: "{...}"
```

I'm still unsure how to exactly configure the datapack namespace, since other plugins could benefit from it. For now I put it in the datapack advancement config, but making it a top-level setting would be possible as well

## Implementation
### Root advancement:
![](https://mc-datapacks.github.io/en/conventions/advancement/root.png)

Path: `global:root`
```json
{
    "display": {
        "title": "Installed Datapacks",
        "description": "",
        "icon": {
            "item": "minecraft:knowledge_book"
        },
        "background": "minecraft:textures/block/gray_concrete.png",
        "show_toast": false,
        "announce_to_chat": false
    },
    "criteria": {
        "trigger": {
            "trigger": "minecraft:tick"
        }
    }
}
```

### Author namespace advancement:
![](https://mc-datapacks.github.io/en/conventions/advancement/namespace.png)

Requires:
- `author`: Name of author
- `author_namespace`: Unique namespace for author (e.g. `oran9eutan`)
- `author_skull_owner`: Skull owner of author

Path: `global:<author_namespace>`
```json
{
    "display": {
        "title": "<author>",
        "description": "",
        "icon": {
            "item": "minecraft:player_head",
            "nbt": "{SkullOwner: '<author_skull_owner>'}"
        },
        "show_toast": false,
        "announce_to_chat": false
    },
    "parent": "global:root",
    "criteria": {
        "trigger": {
            "trigger": "minecraft:tick"
        }
    }
}
```

### Project / Datapack advancement
![](https://mc-datapacks.github.io/en/conventions/advancement/datapack.png)

Requires:
- `project_name`: Name of the project
- `project_description`: Description of the project
- `icon`: The icon of the project. A json object
- `author_namespace`

```json
{
    "display": {
        "title": "<project_name>",
        "description": "<project_description>",
        "icon": <icon>,
        "announce_to_chat": false,
        "show_toast": false
    },
    "parent": "global:<author_namespace>",
    "criteria": {
        "trigger": {
            "trigger": "minecraft:tick"
        }
    }
}
```
This advancement can be put anywhere. Different user may want different locations, but a default location could be decided uppon.
There is the issue of namespace clashes, so I prefer `<author_namespace>:<project_namespace>/installed`. (e.g. `oran9eutan:apple/installed`)


Discussion / hints:
I wasn't familiar with this but it looks like it would fit pretty nicely in `beet.contrib`. Should be pretty easy to implement too.
I already implemented this with a custom plugin in another project using jinja templates ([here](https://github.com/OrangeUtan/Hats/blob/main/hats/plugins/datapack_advancement.py)). 
My only concern is the exact placement of the project advancement and the configuration of the namespaces.

I can create a pull-request and we can work it out from there

Fix patch:
diff --git a/beet/contrib/project_advancement.py b/beet/contrib/project_advancement.py
new file mode 100644
index 00000000..b8c29dba
--- /dev/null
+++ b/beet/contrib/project_advancement.py
@@ -0,0 +1,115 @@
+"""
+    Plugin that generates a advancement for this project.
+
+    The aim of a project advancement is to replace installation messages in an easily viewable and non-obstructive way by putting them on a single advancement page.
+"""
+
+from typing import Any, Optional
+
+from pydantic import BaseModel
+
+from beet import Context, configurable
+from beet.core.utils import TextComponent
+from beet.library.data_pack import Advancement
+
+AdvancementIcon = dict[str, Any]
+
+
+class ProjectAdvancementOptions(BaseModel):
+    icon: AdvancementIcon = {"item": "minecraft:apple"}
+    author_namespace: Optional[str]
+    author_description: str = ""
+    author_skull_owner: Optional[str]
+    project_namespace: Optional[str]
+    project_advancement_path: Optional[str]
+
+
+def beet_default(ctx: Context):
+    ctx.require(project_advancement)
+
+
+@configurable(validator=ProjectAdvancementOptions)
+def project_advancement(ctx: Context, opts: ProjectAdvancementOptions):
+    author_namespace = opts.author_namespace or ctx.project_author.lower()
+    project_namespace = opts.project_namespace or ctx.project_id
+    project_advancement_path = (
+        opts.project_advancement_path
+        or f"{author_namespace}:{project_namespace}/installed"
+    )
+    skull_owner = opts.author_skull_owner or ctx.project_author
+
+    if not author_namespace:
+        raise ValueError(
+            "Missing author namespace. Either author or author_namespace need to be configured"
+        )
+
+    if not skull_owner:
+        raise ValueError(
+            "Missing skull owner. Either author or author_skull_owner need to be configured"
+        )
+
+    ctx.data["global:root"] = create_root_advancement()
+    ctx.data[f"global:{author_namespace}"] = create_author_advancment(
+        ctx.project_author, opts.author_description, skull_owner
+    )
+    ctx.data[project_advancement_path] = create_project_advancement(
+        ctx.project_name, ctx.project_description, author_namespace, opts.icon
+    )
+
+
+def create_root_advancement():
+    return Advancement(
+        {
+            "display": {
+                "title": "Installed Datapacks",
+                "description": "",
+                "icon": {"item": "minecraft:knowledge_book"},
+                "background": "minecraft:textures/block/gray_concrete.png",
+                "show_toast": False,
+                "announce_to_chat": False,
+            },
+            "criteria": {"trigger": {"trigger": "minecraft:tick"}},
+        }
+    )
+
+
+def create_author_advancment(
+    author: TextComponent, author_description: TextComponent, skull_owner: str
+):
+    return Advancement(
+        {
+            "display": {
+                "title": author,
+                "description": author_description,
+                "icon": {
+                    "item": "minecraft:player_head",
+                    "nbt": f"{{'SkullOwner': '{skull_owner}'}}",
+                },
+                "show_toast": False,
+                "announce_to_chat": False,
+            },
+            "parent": "global:root",
+            "criteria": {"trigger": {"trigger": "minecraft:tick"}},
+        }
+    )
+
+
+def create_project_advancement(
+    project_name: TextComponent,
+    project_description: TextComponent,
+    author_namespace: str,
+    icon: AdvancementIcon,
+):
+    return Advancement(
+        {
+            "display": {
+                "title": project_name,
+                "description": project_description,
+                "icon": icon,
+                "announce_to_chat": False,
+                "show_toast": False,
+            },
+            "parent": f"global:{author_namespace}",
+            "criteria": {"trigger": {"trigger": "minecraft:tick"}},
+        }
+    )
diff --git a/examples/project_advancement/beet.json b/examples/project_advancement/beet.json
new file mode 100644
index 00000000..4f7c2e52
--- /dev/null
+++ b/examples/project_advancement/beet.json
@@ -0,0 +1,17 @@
+{
+	"author": "mcbeet",
+	"name": "Example",
+	"description": "An example project",
+	"pipeline": [
+		"beet.contrib.project_advancement"
+	],
+	"meta": {
+		"project_advancement": {
+			"icon": {
+				"item": "minecraft:red_mushroom",
+				"nbt": "{Enchantments:[{}]}"
+			},
+			"author_description": "Organization behind the beet project"
+		}
+	}
+}
\ No newline at end of file


Test patch:
diff --git a/tests/snapshots/examples__build_project_advancement__0.data_pack/data/global/advancements/mcbeet.json b/tests/snapshots/examples__build_project_advancement__0.data_pack/data/global/advancements/mcbeet.json
new file mode 100644
index 00000000..e117e675
--- /dev/null
+++ b/tests/snapshots/examples__build_project_advancement__0.data_pack/data/global/advancements/mcbeet.json
@@ -0,0 +1,18 @@
+{
+  "display": {
+    "title": "mcbeet",
+    "description": "Organization behind the beet project",
+    "icon": {
+      "item": "minecraft:player_head",
+      "nbt": "{'SkullOwner': 'mcbeet'}"
+    },
+    "show_toast": false,
+    "announce_to_chat": false
+  },
+  "parent": "global:root",
+  "criteria": {
+    "trigger": {
+      "trigger": "minecraft:tick"
+    }
+  }
+}
diff --git a/tests/snapshots/examples__build_project_advancement__0.data_pack/data/global/advancements/root.json b/tests/snapshots/examples__build_project_advancement__0.data_pack/data/global/advancements/root.json
new file mode 100644
index 00000000..6de742b6
--- /dev/null
+++ b/tests/snapshots/examples__build_project_advancement__0.data_pack/data/global/advancements/root.json
@@ -0,0 +1,17 @@
+{
+  "display": {
+    "title": "Installed Datapacks",
+    "description": "",
+    "icon": {
+      "item": "minecraft:knowledge_book"
+    },
+    "background": "minecraft:textures/block/gray_concrete.png",
+    "show_toast": false,
+    "announce_to_chat": false
+  },
+  "criteria": {
+    "trigger": {
+      "trigger": "minecraft:tick"
+    }
+  }
+}
diff --git a/tests/snapshots/examples__build_project_advancement__0.data_pack/data/mcbeet/advancements/example/installed.json b/tests/snapshots/examples__build_project_advancement__0.data_pack/data/mcbeet/advancements/example/installed.json
new file mode 100644
index 00000000..8c378257
--- /dev/null
+++ b/tests/snapshots/examples__build_project_advancement__0.data_pack/data/mcbeet/advancements/example/installed.json
@@ -0,0 +1,18 @@
+{
+  "display": {
+    "title": "Example",
+    "description": "An example project",
+    "icon": {
+      "item": "minecraft:red_mushroom",
+      "nbt": "{Enchantments:[{}]}"
+    },
+    "announce_to_chat": false,
+    "show_toast": false
+  },
+  "parent": "global:mcbeet",
+  "criteria": {
+    "trigger": {
+      "trigger": "minecraft:tick"
+    }
+  }
+}
diff --git a/tests/snapshots/examples__build_project_advancement__0.data_pack/pack.mcmeta b/tests/snapshots/examples__build_project_advancement__0.data_pack/pack.mcmeta
new file mode 100644
index 00000000..6856a11f
--- /dev/null
+++ b/tests/snapshots/examples__build_project_advancement__0.data_pack/pack.mcmeta
@@ -0,0 +1,6 @@
+{
+  "pack": {
+    "pack_format": 7,
+    "description": "An example project\nAuthor: mcbeet"
+  }
+}
diff --git a/tests/snapshots/examples__build_project_advancement__1.resource_pack/pack.mcmeta b/tests/snapshots/examples__build_project_advancement__1.resource_pack/pack.mcmeta
new file mode 100644
index 00000000..6856a11f
--- /dev/null
+++ b/tests/snapshots/examples__build_project_advancement__1.resource_pack/pack.mcmeta
@@ -0,0 +1,6 @@
+{
+  "pack": {
+    "pack_format": 7,
+    "description": "An example project\nAuthor: mcbeet"
+  }
+}