日历

日历

适用于模组的正规日历API,可能正在开发游戏内日历风格的网页,也许吧。¯\\(°_o)/¯

简介

日历(Calendar)是一个(主要)模组功能,用于追踪从“救命任务”(The Rescue)完成后的时间流逝。游戏本身会推进时间,但不会记录日期或天数,而且某些任务可能会让时间尴尬地前后跳动——有时甚至跳好几天——这使得依赖至少大致准确时间追踪的模组难以正常运作。

本模组会追踪游戏中向前推进的时间,并调整以覆盖那些各种任务造成的时间跳跃(无论是向前还是向后)。目标是通过一个 API 为需要精确日期的模组提供一个真正准确的日期。

日历采用完整的公历(格里高利历)。在适当的时候,游戏内电脑中会有一个附加程序,你可以确切地看到每个任务在哪个日期完成和开始,以及一个其他模组放入 V 的日历中的预定事件日志。

这个模组可能有很多用例,但就目前而言,它被视为其他模组可以查询的资源。

简而言之:让日历滴答滴答地走起来! ¯_(°o)

功能

公历日历 API

此模组目前的主要用途是提供一个 API,供其他模组查询当前游戏日期。日期始于 2077 年 4 月 10 日,只有在“救命任务”完成后才真正开始追踪日期。这是为了有一个预先指定的锚点,使中途安装模组的存档也能接上——日志会存储每个任务完成时的时间戳,无论日历模组是何时加入加载顺序的,因此桑德拉救援任务已经完成的存档会追溯性地将锚点设为 2077 年 4 月 10 日。

API

所有公共类型都位于 Calendar 模块中。在你的 .reds 文件中添加 import Calendar.*(或使用全限定名称)。

获取系统

let cal: wref<CalendarSystem> = CalendarSystem.Get(gameInstance);
if !IsDefined(cal) { return; }
if !cal.IsStarted() { return; }  // 桑德拉任务未完成 - 没有锚点

一旦本存档中“救命任务”成功完成,IsStarted() 返回 true。在此之前,每个日期获取器都会返回锚点日期(2077 年 4 月 10 日)。

读取当前日期

let date: CalendarDate = cal.GetDate();
let year:    Int32 = date.year;      // 2077, 2078, ...
let month:   Int32 = date.month;     // 1-12
let day:     Int32 = date.day;       // 1-31
let weekday: Int32 = date.weekday;   // 0=周一, 1=周二, ..., 5=周六, 6=周日 (ISO 8601)

对于只需要一个字段的调用者,也可以使用直接访问器:

cal.GetYear();        // Int32
cal.GetMonth();       // Int32, 1-12
cal.GetDayOfMonth();  // Int32, 1-31
cal.GetWeekday();     // Int32, 0=周一 ... 6=周日
cal.GetDaysSinceStart();  // Int32 - 自 2077 年 4 月 10 日以来的天数(= 第 0 天)

格式化日期

let d: CalendarDate = cal.GetDate();
CalendarMath.FormatLong(d);   // "Saturday, April 10, 2077"
CalendarMath.FormatShort(d);  // "Sat, 10 Apr 2077"
CalendarMath.FormatISO(d);    // "2077-04-10"

CalendarMath.WeekdayName(d.weekday);  // "Saturday"
CalendarMath.WeekdayShort(d.weekday); // "Sat"
CalendarMath.MonthName(d.month);      // "April"
CalendarMath.MonthShort(d.month);     // "Apr"

CalendarMath.DaysInMonth(d.year, d.month);  // Int32 - 处理闰年
CalendarMath.IsLeapYear(d.year);            // Bool
CalendarMath.DatesEqual(a, b);              // Bool - 比较年/月/日

监听日期变更

继承 CalendarDateListener 并注册一个实例。OnCalendarDateChanged 仅在跨天边界或引擎跳转取消后触发(如果自上次触发以来日期已更改,则也会在会话开始时触发一次)。

public class MyDateListener extends CalendarDateListener {
  public func OnCalendarDateChanged(prevDate: CalendarDate, newDate: CalendarDate) -> Void {
    FTLog("[MyMod] New day: " + CalendarMath.FormatLong(newDate));
  }
}

// 注册(通常在你的系统的 OnAttach 中或桑德拉任务锚定之后)
let listener: ref<MyDateListener> = new MyDateListener();
cal.RegisterDateListener(listener);

// 在销毁时注销
cal.UnregisterDateListener(listener);

重复注册同一实例是无效操作。监听器作为强引用存储,因此如果你的监听器需要与会话一起结束,请在会话结束时注销。

任务完成日志

日历记录每个转为“成功”状态的任务,包括引擎日戳和计算出的日历日期。这是持久性的(在保存/加载后仍然存在),并按条目标识符去重,因此在重新加载后重复触发相同的“成功”事件是无害的。

public struct CalendarQuestCompletion {
  public let path: String;        // 条目标识符,例如 "q001_intro", "sq030_panam_a_life_apart"
  public let editorName: String;  // 编辑器名称(通常与路径相同)
  public let questType: Int32;    // 参见下方的 CalendarQuestType 辅助函数
  public let engineDay: Int32;    // 完成任务时的原始 GameTime.Days()
  public let calendarDay: Int32;  // 自 2077 年 4 月 10 日以来的天数
  public let date: CalendarDate;  // 完整的日期,可直接格式化
}

访问方式:

let count: Int32 = cal.GetQuestCompletionCount();
let entry: CalendarQuestCompletion = cal.GetQuestCompletion(0);  // 按索引

let all: array<CalendarQuestCompletion> = cal.GetAllQuestCompletions();
let onDay: array<CalendarQuestCompletion> = cal.GetQuestCompletionsForDay(42);
let onDate: array<CalendarQuestCompletion> = cal.GetQuestCompletionsForDate(date);

任务类型查询:

CalendarQuestType.Name(entry.questType);   // "Main Job", "Side Job", "Gig", ...
CalendarQuestType.Short(entry.questType);  // "MJ", "SJ", "G", ...

CalendarQuestType.MainQuest();      // 0
CalendarQuestType.SideQuest();      // 1
CalendarQuestType.MinorQuest();     // 2
CalendarQuestType.StreetStory();    // 3
CalendarQuestType.CyberPsycho();    // 4
CalendarQuestType.Contract();       // 5
CalendarQuestType.VehicleQuest();   // 6
CalendarQuestType.ApartmentQuest(); // 7
CalendarQuestType.CourierQuest();   // 8
CalendarQuestType.CourierSideQuest(); // 9

预定事件(模组集成)

日历 3.3.0 增加了一个按 ID 更新插入(upsert)的事件注册表,使模组可以将带日期的条目放到玩家的日历上——“贷款还款到期”、“抢劫任务截止日期”、“q001 锚点纪念日”等。在保存/加载后保持持久性;可按天、类别或即将到来的时间窗口进行查询。

public struct CalendarEvent {
  public let id: CName;                // 唯一,使用命名空间:n"MyMod.Thing.123"
  public let targetCalendarDay: Int32; // 自 2077 年 4 月 10 日以来的天数
  public let date: CalendarDate;       // 用于显示的完整日期
  public let label: CName;             // 简短稳定的标签,例如 n"Payment due"
  public let category: CName;          // 过滤键,例如 n"Bank"
  public let fired: Bool;              // 一旦今天 >= targetCalendarDay 则变为 true
}

安排和取消:

let target: Int32 = cal.GetDaysSinceStart() + 7;
cal.ScheduleEvent(n"MyMod.PayRent.house5", target, n"Pay rent", n"Apartment");
cal.CancelEvent(n"MyMod.PayRent.house5");

查询:

cal.GetEventCount();                      // Int32
cal.GetAllEvents();                       // array<CalendarEvent>
cal.GetEvent(n"MyMod.PayRent.house5");    // 单个 CalendarEvent
cal.GetEventsForDay(targetCalendarDay);   // array<CalendarEvent>
cal.GetEventsForCategory(n"Bank");        // array<CalendarEvent>
cal.GetUpcomingEvents(7);                 // 未来 7 天(含今天)

监听器:

public class MyEventListener extends CalendarEventListener {
  public func OnCalendarEventFired(event: CalendarEvent) -> Void {
    if Equals(event.category, n"Bank") {
      FTLog("[MyMod] Bank event fired: " + NameToString(event.label));
    }
  }
}

cal.RegisterEventListener(new MyEventListener());
cal.UnregisterEventListener(myListenerRef);  // 在销毁时

与日期监听器相同的幂等强引用规则。

锚点常量

如果你需要原始锚点(例如绘制一个从 2077 年 4 月 10 日开始的日历网格):

CalendarMath.AnchorYear();     // 2077
CalendarMath.AnchorMonth();    // 4 (April)
CalendarMath.AnchorDay();      // 10
CalendarMath.AnchorWeekday();  // 5 (Saturday)

关于计时的说明

DateChanged 监听器和锚点本身是由回调驱动的(日志状态变更事件),因此它们会在相关任务完成时立即响应。周期性轮询(轮询间隔选项,默认 30 秒)仅用于检测日期变更和引擎跳转取消。读取 GetDate() 开销很小。

选项

可在游戏内的 Mod Settings -> Calendar 中进行配置。

启用模组:开关。默认开启。 开启或关闭,随你意。

显示选项:开关。默认关闭。 显示下方所有调整选项。

02. 日历

抢劫任务后昏迷(天):滑块,0-60。默认 12。 当你(V)在维克多的诊所醒来时,日历向前跳转的天数。原版游戏在昏迷过场中本身已经会推进约 2 天,因此默认值 12 补偿了剩余的差距,总共约 14 天。

03. 任务匹配器(高级)

更改这些设置需要在代码中直接修改!

桑德拉救援任务匹配:基于代码的字符串。默认:"q001_intro" 对每个成功任务条目的 id 或编辑器名称进行子字符串匹配(区分大小写)。第一个匹配项将成为 4 月 10 日的锚点。默认的 'q001_intro' 是“救命任务”的 id。

抢劫任务匹配:基于代码的字符串。默认:"q005_heist" 对每个成功任务进行子字符串匹配。第一个匹配项会触发昏迷天数跳转。默认的 'q005_heist' 是“抢劫任务”(The Heist)。

任务类名:基于代码的字符串。默认:"gameJournalQuest" GetEntryByString 用于任务条目的类名。原版是 'gameJournalQuest'。

04. 调整(高级)

轮询间隔(秒):滑块,10.0-120.0。默认:30.0 系统轮询日期变更并向监听器触发 DateChanged 事件的频率。锚点本身是回调驱动的,不依赖于此。

05. 调试

显示调试信息:开关。默认关闭。 将日历事件输出到 GameLog。

要求

此模组需要你安装以下内容: Redscript Codeware Redscript Configuration Framework

可选但推荐: RedLogger - 为日志添加更详细的信息。

对于 3.5.0 之前的版本: DigitalVixen Core Mod Settings

兼容性

日历被其他需要日期计算或事件调度的 DV 套件模组使用:

任何其他需要准确游戏内日期或想要显示预定条目的模组都可以通过上述 API 进行接入。

安装

  1. 安装所有依赖项。
  2. 将下载的文件解压到你的主游戏目录。
  3. 启动游戏并在 Mod Settings -> Calendar 下进行配置。

卸载

删除 \Cyberpunk 2077\r6\scripts\Calendar\ 目录

不会修改任何存档数据。

致谢

感谢 CET 和 redscript 社区提供的工具和文档。

感谢所有出色的模组作者,他们持续为我们创作免费内容供大家享受!

特别感谢 NightlyNow 推动我做得更好,并敢于更多尝试。

中心对齐:DigitalVixen 模组套件的一部分。 中心对齐:你现在可以在 Discord 这里 找到我!