@occultus/article-api 0.23.0 → 0.24.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@occultus/article-api",
3
- "version": "0.23.0",
3
+ "version": "0.24.0-beta.2",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://codeberg.org/TeamOccultus/StarTenonAPI"
@@ -10,6 +10,7 @@ import { FormLike } from "@occultus/core";
10
10
  * @since 1.0.0
11
11
  */
12
12
  export class Article extends FormLike {
13
+ static DYNAMIC_DATA_ID = "starock:unlocked_articles";
13
14
  /**
14
15
  * @param id 文章ID
15
16
  * @param title 文章标题
@@ -46,15 +47,18 @@ export class Article extends FormLike {
46
47
  .title(title)
47
48
  .body(body)
48
49
  .button({ translate: "gui.ok" });
49
- mainForm.show(player).then((response) => {
50
- if (response.canceled || response.selection === 0) {
51
- this.quit(player, backTo);
52
- return;
53
- }
54
- }).catch((err) => {
55
- console.error(`[Article] Failed to display article: ${err}`);
56
- this.quit(player, backTo as readonly FormLike[]);
57
- });
50
+ mainForm
51
+ .show(player)
52
+ .then((response) => {
53
+ if (response.canceled || response.selection === 0) {
54
+ this.quit(player, backTo);
55
+ return;
56
+ }
57
+ })
58
+ .catch((err) => {
59
+ console.error(`[Article] Failed to display article: ${err}`);
60
+ this.quit(player, backTo as readonly FormLike[]);
61
+ });
58
62
  }
59
63
  /**
60
64
  * 向玩家展示文章(文章有章节时)
@@ -71,25 +75,28 @@ export class Article extends FormLike {
71
75
  parseText(this.body, player)
72
76
  ];
73
77
  const contentsForm = new ActionFormData().title(title).body(body);
74
- this.chapters!.forEach((chapter) => {
78
+ this.chapters?.forEach((chapter) => {
75
79
  const chapterTitle = parseText(chapter.title, player);
76
80
  contentsForm.button(chapterTitle, chapter.iconPath);
77
81
  });
78
- contentsForm.show(player).then((response) => {
79
- if (response.canceled || response.selection === undefined) {
80
- this.quit(player, backTo);
81
- return;
82
- }
83
- const [chapterTitle, chapterBody] = [
84
- parseText(this.chapters![response.selection].title, player),
85
- parseText(this.chapters![response.selection].body, player)
86
- ];
87
- const chapter = new Article("temp:chapter", chapterTitle, chapterBody);
88
- this.jumpTo(player, chapter, backTo);
89
- }).catch((err) => {
90
- console.error(`[Article] Failed to display article: ${err}`);
91
- this.quit(player, backTo as readonly FormLike[]);
92
- });
82
+ contentsForm
83
+ .show(player)
84
+ .then((response) => {
85
+ if (response.canceled || response.selection === undefined) {
86
+ this.quit(player, backTo);
87
+ return;
88
+ }
89
+ const [chapterTitle, chapterBody] = [
90
+ parseText(this.chapters![response.selection].title, player),
91
+ parseText(this.chapters![response.selection].body, player)
92
+ ];
93
+ const chapter = new Article("temp:chapter", chapterTitle, chapterBody);
94
+ this.jumpTo(player, chapter, backTo);
95
+ })
96
+ .catch((err) => {
97
+ console.error(`[Article] Failed to display article: ${err}`);
98
+ this.quit(player, backTo as readonly FormLike[]);
99
+ });
93
100
  }
94
101
  /**
95
102
  * 向玩家展示文章
@@ -106,10 +113,26 @@ export class Article extends FormLike {
106
113
  /**
107
114
  * 在文章中心里解锁文章
108
115
  * @param player
116
+ * @return `true`如果文章需要解锁,并且成功解锁;否则返回`false`
109
117
  */
110
- unlock(player: Player): void {
111
- if (this.needUnlock) {
112
- player.addTag(`articleUnlock:${this.id}`);
118
+ unlock(player: Player): boolean {
119
+ if (!this.needUnlock || this.checkUnlock(player)) return false;
120
+ try {
121
+ const raw = player.getDynamicProperty(Article.DYNAMIC_DATA_ID);
122
+ let unlockedIds: string[] = [];
123
+ if (raw && typeof raw === "string") {
124
+ const parsed = JSON.parse(raw);
125
+ unlockedIds = Array.isArray(parsed) ? parsed : [];
126
+ }
127
+ player.setDynamicProperty(
128
+ Article.DYNAMIC_DATA_ID,
129
+ JSON.stringify([...unlockedIds, this.id])
130
+ );
131
+ return true;
132
+ } catch (err) {
133
+ console.error(`Failed to unlock article ${this.id}:`, err);
134
+ player.setDynamicProperty(Article.DYNAMIC_DATA_ID, "[]");
135
+ return false;
113
136
  }
114
137
  }
115
138
  /**
@@ -117,9 +140,16 @@ export class Article extends FormLike {
117
140
  * @param player
118
141
  */
119
142
  checkUnlock(player: Player): boolean {
120
- if (this.needUnlock) {
121
- return player.hasTag(`articleUnlock:${this.id}`);
143
+ if (!this.needUnlock) return true;
144
+ try {
145
+ const raw = player.getDynamicProperty(Article.DYNAMIC_DATA_ID);
146
+ if (!raw || typeof raw !== "string") return false;
147
+ const json = JSON.parse(raw);
148
+ if (!Array.isArray(json)) return false;
149
+ return json.includes(this.id);
150
+ } catch (err) {
151
+ console.error(`Failed to check status of article ${this.id}:`, err);
122
152
  }
123
- return true;
153
+ return false;
124
154
  }
125
155
  }
@@ -1,10 +1,9 @@
1
1
  import { Player } from "@minecraft/server";
2
2
  import { MessageFormData } from "@minecraft/server-ui";
3
3
  import { ArticleRegistries } from "./Base/ArticleRegistries";
4
- import { Article } from "./Article";
5
- import { generateContentForm } from "../lib/utils";
4
+ import { autoGetArticles, generateContentForm } from "../lib/utils";
6
5
  import { TextProvider } from "@occultus/text-api";
7
- import { FormLike } from "@occultus/core";
6
+ import { FormLike, OccultusSDKError } from "@occultus/core";
8
7
  import { ReadableArticle } from "../types/ReadableArticle";
9
8
 
10
9
  /**
@@ -32,14 +31,14 @@ export class ArticleCenter extends FormLike {
32
31
  * @param id 文章中心ID
33
32
  * @param title 文章中心标题
34
33
  * @param body 文章中心内容
35
- * @param articles 可用的文章,如果为`true`,则会从该脚本环境注册的所有文章中显示已经解锁的文章,反之则显示无文章提示,如果为文章列表,则只从提供的文章中显示已解锁的文章
34
+ * @param articleSource 文章来源,如果为`true`,则会从该脚本环境注册的所有文章中显示已经解锁的文章,反之则显示无文章提示,如果为文章列表,则只从提供的文章中显示已解锁的文章
36
35
  */
37
36
  constructor(
38
37
  public readonly id: string,
39
38
  public title: TextProvider,
40
39
  public body: TextProvider,
41
- public articles: Article[] | boolean,
42
- public readonly bindTo: ArticleRegistries,
40
+ public articleSource: ReadableArticle[] | "registry" | "auto",
41
+ public readonly bindTo?: ArticleRegistries,
43
42
  public iconPath?: string
44
43
  ) {
45
44
  super();
@@ -53,7 +52,7 @@ export class ArticleCenter extends FormLike {
53
52
  player,
54
53
  this.title,
55
54
  this.body,
56
- this.getAvailableArticles()
55
+ this.getAvailableArticles(player)
57
56
  );
58
57
 
59
58
  if (!contentForm) {
@@ -76,12 +75,19 @@ export class ArticleCenter extends FormLike {
76
75
  this.quit(player, backTo as readonly FormLike[]);
77
76
  });
78
77
  }
79
- private getAvailableArticles(): ReadableArticle[] {
80
- if (typeof this.articles === "boolean") {
81
- if (!this.articles) return [];
78
+ private getAvailableArticles(player: Player): ReadableArticle[] {
79
+ if (this.articleSource === "registry") {
80
+ if (!this.bindTo) {
81
+ throw new OccultusSDKError(
82
+ "You must bind article registries to the center when using the boolean parameter!"
83
+ );
84
+ }
82
85
  return this.bindTo.getAll();
83
86
  }
84
- return this.articles;
87
+ if (this.articleSource === "auto") {
88
+ return autoGetArticles(player);
89
+ }
90
+ return this.articleSource;
85
91
  }
86
92
  /**
87
93
  * 当没有可用的文章时,向玩家显示警告
@@ -0,0 +1,139 @@
1
+ import { StartupEvent, system } from "@minecraft/server";
2
+ import { ArticleBindingConfig } from "./BindingConfig";
3
+ import { OccultusSDKError } from "@occultus/core";
4
+ import { TextProvider } from "@occultus/text-api";
5
+ import { ChapterData } from "../../interface/ChapterData";
6
+ import { ReadableArticle } from "../../types/ReadableArticle";
7
+ import { ArticleCenter } from "../ArticleCenter";
8
+ import {
9
+ getArticle,
10
+ } from "../../lib/utils";
11
+
12
+ /**
13
+ * 文章内容类型别名
14
+ */
15
+ export type ArticleContent =
16
+ | TextProvider
17
+ | { chapter: ChapterData[]; description: TextProvider };
18
+
19
+ export class ArticleServerBindings {
20
+ private static instance: ArticleServerBindings | undefined;
21
+ private static center: ArticleCenter | undefined;
22
+ private preloadedContent: Map<string, ArticleContent> = new Map();
23
+ private articles: Map<string, ReadableArticle> = new Map();
24
+ private initCenter(config: ArticleBindingConfig) {
25
+ const center = new ArticleCenter(
26
+ "starock:bindings.article_center",
27
+ config.centerConfig.title,
28
+ config.centerConfig.description,
29
+ "auto",
30
+ undefined,
31
+ config.centerConfig.icon_path
32
+ );
33
+ ArticleServerBindings.center = center;
34
+ }
35
+ private registryArticleComponent(
36
+ event: StartupEvent,
37
+ config: ArticleBindingConfig
38
+ ) {
39
+ const self = this;
40
+ event.itemComponentRegistry.registerCustomComponent(config.componentName, {
41
+ onUse(event) {
42
+ const { itemStack, source } = event;
43
+ if (!itemStack) return;
44
+ if (self.articles.has(itemStack.typeId)) {
45
+ self.articles.get(itemStack.typeId)?.display(source, []);
46
+ return;
47
+ }
48
+ const article = getArticle(itemStack);
49
+ self.articles.set(itemStack.typeId, article);
50
+ article.display(source, []);
51
+ }
52
+ });
53
+ }
54
+ private registryArticleContentComponent(
55
+ event: StartupEvent,
56
+ config: ArticleBindingConfig
57
+ ) {
58
+ event.itemComponentRegistry.registerCustomComponent(
59
+ config.contentComponentName,
60
+ {}
61
+ );
62
+ }
63
+ private registryArticleCenterContent(
64
+ event: StartupEvent,
65
+ config: ArticleBindingConfig
66
+ ) {
67
+ event.itemComponentRegistry.registerCustomComponent(
68
+ config.centerComponentName,
69
+ {
70
+ onUse(event) {
71
+ const { itemStack, source } = event;
72
+ if (!itemStack) return;
73
+ const center = ArticleServerBindings.center;
74
+ if (!center) {
75
+ return;
76
+ }
77
+ center.display(source, []);
78
+ }
79
+ }
80
+ );
81
+ }
82
+ private constructor(public config: ArticleBindingConfig) {
83
+ if (!config) {
84
+ throw new OccultusSDKError("ArticleBindingConfig is required!");
85
+ }
86
+ system.beforeEvents.startup.subscribe((event) => {
87
+ this.registryArticleComponent(event, config);
88
+ this.registryArticleContentComponent(event, config);
89
+ this.initCenter(config);
90
+ this.registryArticleCenterContent(event, config);
91
+ });
92
+ }
93
+ static create(config: ArticleBindingConfig): void {
94
+ if (ArticleServerBindings.instance) {
95
+ throw new OccultusSDKError("ArticleServerBindings already created!");
96
+ }
97
+ ArticleServerBindings.instance = new ArticleServerBindings(config);
98
+ }
99
+ static getInstance(): ArticleServerBindings {
100
+ if (!ArticleServerBindings.instance) {
101
+ throw new OccultusSDKError("ArticleServerBindings not created!");
102
+ }
103
+ return ArticleServerBindings.instance;
104
+ }
105
+ static getCenter(): ArticleCenter {
106
+ if (!ArticleServerBindings.center) {
107
+ throw new OccultusSDKError("ArticleCenter not found!");
108
+ }
109
+ return ArticleServerBindings.center;
110
+ }
111
+ /**
112
+ * 预载文章内容
113
+ * @param id
114
+ * @param content
115
+ */
116
+ preloadContent(
117
+ id: string,
118
+ content:
119
+ | TextProvider
120
+ | { chapter: ChapterData[]; description: TextProvider }
121
+ ) {
122
+ if (!id || !content) {
123
+ console.warn(`Preload data failed with id: ${id}`);
124
+ return;
125
+ }
126
+ this.preloadedContent.set(id, content);
127
+ }
128
+ readPreloadedContent(
129
+ id: string
130
+ ):
131
+ | TextProvider
132
+ | { chapter: ChapterData[]; description: TextProvider }
133
+ | undefined {
134
+ return this.preloadedContent.get(id);
135
+ }
136
+ getArticle(id: string): ReadableArticle | undefined {
137
+ return this.articles.get(id);
138
+ }
139
+ }
@@ -0,0 +1,12 @@
1
+ import { RawMessage } from "@minecraft/server";
2
+
3
+ export type ArticleBindingConfig = {
4
+ componentName: string;
5
+ centerComponentName: string;
6
+ contentComponentName: string;
7
+ centerConfig: {
8
+ title: RawMessage | string;
9
+ description: RawMessage | string;
10
+ icon_path?: string;
11
+ };
12
+ };
@@ -0,0 +1,10 @@
1
+ import { RawMessage } from "@minecraft/server";
2
+
3
+ export type ArticleComponentParams = {
4
+ title: RawMessage | string;
5
+ icon_path?: string;
6
+ };
7
+
8
+ export type ArticleContentParams = RawMessage[] | string;
9
+
10
+ export type ArticleCenterComponentParams = {};
package/src/index.ts CHANGED
@@ -6,3 +6,6 @@ export * from "./api/Article";
6
6
  export * from "./api/ArticleCenter";
7
7
  export * from "./api/Base/ArticleRegistries";
8
8
  export * from "./interface/ChapterData";
9
+ export * from "./api/Bindings/ArticleServerBindings";
10
+ export * from "./api/Bindings/BindingConfig";
11
+ export * from "./api/Bindings/ComponentParams";
package/src/lib/utils.ts CHANGED
@@ -1,8 +1,19 @@
1
- import { Player, StartupEvent } from "@minecraft/server";
1
+ import { ItemStack, Player, RawMessage, StartupEvent } from "@minecraft/server";
2
2
  import { ActionFormData } from "@minecraft/server-ui";
3
3
  import { Article } from "../api/Article";
4
4
  import { ReadableArticle } from "../types/ReadableArticle";
5
5
  import { TextProvider, parseText } from "@occultus/text-api";
6
+ import { ArticleBindingConfig } from "../api/Bindings/BindingConfig";
7
+ import {
8
+ ArticleComponentParams,
9
+ ArticleContentParams
10
+ } from "../api/Bindings/ComponentParams";
11
+ import {
12
+ ArticleContent,
13
+ ArticleServerBindings
14
+ } from "../api/Bindings/ArticleServerBindings";
15
+ import { ArticleCenter } from "../api/ArticleCenter";
16
+ import { OccultusSDKError } from "@occultus/core";
6
17
 
7
18
  /**
8
19
  * 根据提供的资源生成目录表单
@@ -57,3 +68,79 @@ export function articleRegister(
57
68
  }
58
69
  });
59
70
  }
71
+
72
+ export function parseDescription(
73
+ text: ArticleContent | RawMessage[]
74
+ ): TextProvider {
75
+ if (typeof text === "string") return text;
76
+ if (Array.isArray(text)) {
77
+ return {
78
+ rawtext: text
79
+ };
80
+ }
81
+ if ("description" in text) {
82
+ return text.description;
83
+ }
84
+ return text;
85
+ }
86
+
87
+ export function getArticleContent(item: ItemStack) {
88
+ const config = ArticleServerBindings.getInstance().config;
89
+ const params = item.getComponent(config.contentComponentName)
90
+ ?.customComponentParameters.params as ArticleContentParams;
91
+ if (!params) return;
92
+ if (typeof params === "string") {
93
+ return ArticleServerBindings.getInstance().readPreloadedContent(params);
94
+ }
95
+ return params;
96
+ }
97
+
98
+ export function getArticle(item: ItemStack): Article {
99
+ const config = ArticleServerBindings.getInstance().config;
100
+ const mainParams = item.getComponent(config.componentName)
101
+ ?.customComponentParameters.params as ArticleComponentParams;
102
+ const content = getArticleContent(item);
103
+ let article: Article | undefined;
104
+ if (!content){
105
+ throw new OccultusSDKError("Article content not found!");
106
+ }
107
+ if (typeof content !== "string" && "chapter" in content) {
108
+ article = new Article(
109
+ item.typeId,
110
+ mainParams.title,
111
+ content.description,
112
+ content.chapter,
113
+ mainParams.icon_path
114
+ );
115
+ return article;
116
+ }
117
+ article = new Article(
118
+ item.typeId,
119
+ mainParams.title,
120
+ parseDescription(content),
121
+ undefined,
122
+ mainParams.icon_path
123
+ );
124
+ return article;
125
+ }
126
+
127
+ /**
128
+ * 自动获得文章
129
+ * @param player
130
+ */
131
+ export function autoGetArticles(player: Player): ReadableArticle[] {
132
+ try {
133
+ const raw = player.getDynamicProperty(Article.DYNAMIC_DATA_ID);
134
+ if (!raw || typeof raw !== "string") return [];
135
+ const json = JSON.parse(raw);
136
+ if (!Array.isArray(json)) return [];
137
+ return json.map(
138
+ (id: string) =>
139
+ ArticleServerBindings.getInstance().getArticle(id) ??
140
+ getArticle(new ItemStack(id))
141
+ );
142
+ } catch (e) {
143
+ console.error("Failed to get articles from player:" + e);
144
+ }
145
+ return [];
146
+ }